From d6e7e0997660452e0b45e353b9c5d3f5ec516f4a Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 4 Nov 2024 15:55:17 +0100 Subject: [PATCH 01/71] fix: storage corruption on null metadata --- internal/README.md | 2 +- internal/account.go | 2 +- test/e2e/api_accounts_metadata_test.go | 55 +++++++++++++++++++++++++- 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/internal/README.md b/internal/README.md index 3976f0484..626af7ae1 100644 --- a/internal/README.md +++ b/internal/README.md @@ -220,7 +220,7 @@ type Account struct { bun.BaseModel `bun:"table:accounts"` Address string `json:"address" bun:"address"` - Metadata metadata.Metadata `json:"metadata" bun:"metadata,type:jsonb"` + Metadata metadata.Metadata `json:"metadata" bun:"metadata,type:jsonb,default:'{}'"` FirstUsage time.Time `json:"-" bun:"first_usage,nullzero"` InsertionDate time.Time `json:"_" bun:"insertion_date,nullzero"` UpdatedAt time.Time `json:"-" bun:"updated_at,nullzero"` diff --git a/internal/account.go b/internal/account.go index 8bd517d4b..cb3b21515 100644 --- a/internal/account.go +++ b/internal/account.go @@ -15,7 +15,7 @@ type Account struct { bun.BaseModel `bun:"table:accounts"` Address string `json:"address" bun:"address"` - Metadata metadata.Metadata `json:"metadata" bun:"metadata,type:jsonb"` + Metadata metadata.Metadata `json:"metadata" bun:"metadata,type:jsonb,default:'{}'"` FirstUsage time.Time `json:"-" bun:"first_usage,nullzero"` InsertionDate time.Time `json:"_" bun:"insertion_date,nullzero"` UpdatedAt time.Time `json:"-" bun:"updated_at,nullzero"` diff --git a/test/e2e/api_accounts_metadata_test.go b/test/e2e/api_accounts_metadata_test.go index 79e46e839..7e8f53d4a 100644 --- a/test/e2e/api_accounts_metadata_test.go +++ b/test/e2e/api_accounts_metadata_test.go @@ -44,7 +44,7 @@ var _ = Context("Ledger accounts list API tests", func() { "clientType": "gold", } ) - BeforeEach(func() { + JustBeforeEach(func() { err := AddMetadataToAccount( ctx, testServer.GetValue(), @@ -75,5 +75,58 @@ var _ = Context("Ledger accounts list API tests", func() { It("should trigger a new event", func() { Eventually(events).Should(Receive(Event(ledgerevents.EventTypeSavedMetadata))) }) + Context("with empty metadata", func() { + BeforeEach(func() { + metadata = nil + }) + It("should be OK", func() { + response, err := GetAccount( + ctx, + testServer.GetValue(), + operations.V2GetAccountRequest{ + Address: "foo", + Ledger: "default", + }, + ) + Expect(err).ToNot(HaveOccurred()) + + Expect(*response).Should(Equal(components.V2Account{ + Address: "foo", + Metadata: map[string]string{}, + })) + }) + Context("then adding with empty metadata", func() { + It("should be OK", func() { + + // The first call created the row in the database, + // the second call should not change the metadata, and checks than updates works. + err := AddMetadataToAccount( + ctx, + testServer.GetValue(), + operations.V2AddMetadataToAccountRequest{ + RequestBody: metadata, + Address: "foo", + Ledger: "default", + }, + ) + Expect(err).ToNot(HaveOccurred()) + + response, err := GetAccount( + ctx, + testServer.GetValue(), + operations.V2GetAccountRequest{ + Address: "foo", + Ledger: "default", + }, + ) + Expect(err).ToNot(HaveOccurred()) + + Expect(*response).Should(Equal(components.V2Account{ + Address: "foo", + Metadata: map[string]string{}, + })) + }) + }) + }) }) }) From 6bddcd15d0388a746b578bfcf1ffd8da9b79bdb0 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 30 Oct 2024 15:07:54 +0100 Subject: [PATCH 02/71] feat: move long migrations after the minimal migration --- .../migrations/11-make-stateless/up.sql | 24 ------------- .../12-transaction-reference-index/notes.yaml | 1 + .../12-transaction-reference-index/up.sql | 1 + .../13-create-ledger-indexes/notes.yaml | 1 + .../13-create-ledger-indexes/up.sql | 36 +++++++++++++++++++ 5 files changed, 39 insertions(+), 24 deletions(-) create mode 100644 internal/storage/bucket/migrations/12-transaction-reference-index/notes.yaml create mode 100644 internal/storage/bucket/migrations/12-transaction-reference-index/up.sql create mode 100644 internal/storage/bucket/migrations/13-create-ledger-indexes/notes.yaml create mode 100644 internal/storage/bucket/migrations/13-create-ledger-indexes/up.sql diff --git a/internal/storage/bucket/migrations/11-make-stateless/up.sql b/internal/storage/bucket/migrations/11-make-stateless/up.sql index 1a2462f12..9b92f4282 100644 --- a/internal/storage/bucket/migrations/11-make-stateless/up.sql +++ b/internal/storage/bucket/migrations/11-make-stateless/up.sql @@ -205,8 +205,6 @@ add column inserted_at timestamp without time zone default (transaction_date() a alter column timestamp set default (transaction_date() at time zone 'utc'), alter column id type bigint; -drop index transactions_reference; -create unique index transactions_reference on transactions (ledger, reference); create index transactions_sequences on transactions (id, seq); alter table logs @@ -482,10 +480,6 @@ $do$ vsql = 'select setval(''"log_id_' || ledger.id || '"'', coalesce((select max(id) + 1 from logs where ledger = ''' || ledger.name || '''), 1)::bigint, false)'; execute vsql; - -- enable post commit effective volumes synchronously - vsql = 'create index "pcev_' || ledger.id || '" on moves (accounts_address, asset, effective_date desc) where ledger = ''' || ledger.name || ''''; - execute vsql; - vsql = 'create trigger "set_effective_volumes_' || ledger.id || '" before insert on moves for each row when (new.ledger = ''' || ledger.name || ''') execute procedure set_effective_volumes()'; execute vsql; @@ -508,30 +502,12 @@ $do$ vsql = 'create trigger "insert_transaction_metadata_history_' || ledger.id || '" after insert on "transactions" for each row when (new.ledger = ''' || ledger.name || ''') execute procedure insert_transaction_metadata_history()'; execute vsql; - vsql = 'create index "transactions_sources_' || ledger.id || '" on transactions using gin (sources jsonb_path_ops) where ledger = ''' || ledger.name || ''''; - execute vsql; - - vsql = 'create index "transactions_destinations_' || ledger.id || '" on transactions using gin (destinations jsonb_path_ops) where ledger = ''' || ledger.name || ''''; - execute vsql; - vsql = 'create trigger "transaction_set_addresses_' || ledger.id || '" before insert on transactions for each row when (new.ledger = ''' || ledger.name || ''') execute procedure set_transaction_addresses()'; execute vsql; - vsql = 'create index "accounts_address_array_' || ledger.id || '" on accounts using gin (address_array jsonb_ops) where ledger = ''' || ledger.name || ''''; - execute vsql; - - vsql = 'create index "accounts_address_array_length_' || ledger.id || '" on accounts (jsonb_array_length(address_array)) where ledger = ''' || ledger.name || ''''; - execute vsql; - vsql = 'create trigger "accounts_set_address_array_' || ledger.id || '" before insert on accounts for each row when (new.ledger = ''' || ledger.name || ''') execute procedure set_address_array_for_account()'; execute vsql; - vsql = 'create index "transactions_sources_arrays_' || ledger.id || '" on transactions using gin (sources_arrays jsonb_path_ops) where ledger = ''' || ledger.name || ''''; - execute vsql; - - vsql = 'create index "transactions_destinations_arrays_' || ledger.id || '" on transactions using gin (destinations_arrays jsonb_path_ops) where ledger = ''' || ledger.name || ''''; - execute vsql; - vsql = 'create trigger "transaction_set_addresses_segments_' || ledger.id || '" before insert on "transactions" for each row when (new.ledger = ''' || ledger.name || ''') execute procedure set_transaction_addresses_segments()'; execute vsql; end loop; diff --git a/internal/storage/bucket/migrations/12-transaction-reference-index/notes.yaml b/internal/storage/bucket/migrations/12-transaction-reference-index/notes.yaml new file mode 100644 index 000000000..a76875651 --- /dev/null +++ b/internal/storage/bucket/migrations/12-transaction-reference-index/notes.yaml @@ -0,0 +1 @@ +name: Create transaction reference index concurrently diff --git a/internal/storage/bucket/migrations/12-transaction-reference-index/up.sql b/internal/storage/bucket/migrations/12-transaction-reference-index/up.sql new file mode 100644 index 000000000..98fa61296 --- /dev/null +++ b/internal/storage/bucket/migrations/12-transaction-reference-index/up.sql @@ -0,0 +1 @@ +create unique index concurrently transactions_reference2 on "{{.Schema}}".transactions (ledger, reference); \ No newline at end of file diff --git a/internal/storage/bucket/migrations/13-create-ledger-indexes/notes.yaml b/internal/storage/bucket/migrations/13-create-ledger-indexes/notes.yaml new file mode 100644 index 000000000..652f242b6 --- /dev/null +++ b/internal/storage/bucket/migrations/13-create-ledger-indexes/notes.yaml @@ -0,0 +1 @@ +name: Create ledger indexes diff --git a/internal/storage/bucket/migrations/13-create-ledger-indexes/up.sql b/internal/storage/bucket/migrations/13-create-ledger-indexes/up.sql new file mode 100644 index 000000000..1efb24192 --- /dev/null +++ b/internal/storage/bucket/migrations/13-create-ledger-indexes/up.sql @@ -0,0 +1,36 @@ +set search_path = '{{.Schema}}'; + +drop index transactions_reference; +alter index transactions_reference2 rename to transactions_reference; + +DO +$do$ + declare + ledger record; + vsql text; + BEGIN + for ledger in select * from _system.ledgers where bucket = current_schema loop + -- enable post commit effective volumes synchronously + vsql = 'create index "pcev_' || ledger.id || '" on moves (accounts_address, asset, effective_date desc) where ledger = ''' || ledger.name || ''''; + execute vsql; + + vsql = 'create index "transactions_sources_' || ledger.id || '" on transactions using gin (sources jsonb_path_ops) where ledger = ''' || ledger.name || ''''; + execute vsql; + + vsql = 'create index "transactions_destinations_' || ledger.id || '" on transactions using gin (destinations jsonb_path_ops) where ledger = ''' || ledger.name || ''''; + execute vsql; + + vsql = 'create index "accounts_address_array_' || ledger.id || '" on accounts using gin (address_array jsonb_ops) where ledger = ''' || ledger.name || ''''; + execute vsql; + + vsql = 'create index "accounts_address_array_length_' || ledger.id || '" on accounts (jsonb_array_length(address_array)) where ledger = ''' || ledger.name || ''''; + execute vsql; + + vsql = 'create index "transactions_sources_arrays_' || ledger.id || '" on transactions using gin (sources_arrays jsonb_path_ops) where ledger = ''' || ledger.name || ''''; + execute vsql; + + vsql = 'create index "transactions_destinations_arrays_' || ledger.id || '" on transactions using gin (destinations_arrays jsonb_path_ops) where ledger = ''' || ledger.name || ''''; + execute vsql; + end loop; + END +$do$; From 0b6ab5fc5b47f9a8c4d9b8377f351ef921473dc7 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 30 Oct 2024 16:04:20 +0100 Subject: [PATCH 03/71] feat: enforce uniqueness of reference when not fully migrated --- go.mod | 2 +- .../controller/ledger/store_generated_test.go | 4 +- internal/storage/bucket/bucket.go | 2 +- .../migrations/11-make-stateless/up.sql | 32 ++++++++ .../13-create-ledger-indexes/up.sql | 3 + internal/storage/driver/driver.go | 4 + internal/storage/ledger/legacy/adapters.go | 2 +- internal/storage/ledger/main_test.go | 25 +++--- internal/storage/ledger/store.go | 4 +- internal/storage/ledger/transactions.go | 24 ++++++ internal/storage/ledger/transactions_test.go | 81 +++++++++++++++++++ 11 files changed, 165 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 57507d0fe..b7dc25055 100644 --- a/go.mod +++ b/go.mod @@ -86,7 +86,7 @@ require ( github.com/buger/jsonparser v1.1.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/containerd/continuity v0.4.3 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 // indirect github.com/docker/cli v27.3.1+incompatible // indirect diff --git a/internal/controller/ledger/store_generated_test.go b/internal/controller/ledger/store_generated_test.go index 1618ae7c3..59479b0da 100644 --- a/internal/controller/ledger/store_generated_test.go +++ b/internal/controller/ledger/store_generated_test.go @@ -364,7 +364,7 @@ func (mr *MockStoreMockRecorder) GetVolumesWithBalances(ctx, q any) *gomock.Call // IsUpToDate mocks base method. func (m *MockStore) IsUpToDate(ctx context.Context) (bool, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsUpToDate", ctx) + ret := m.ctrl.Call(m, "HasMinimalVersion", ctx) ret0, _ := ret[0].(bool) ret1, _ := ret[1].(error) return ret0, ret1 @@ -373,7 +373,7 @@ func (m *MockStore) IsUpToDate(ctx context.Context) (bool, error) { // IsUpToDate indicates an expected call of IsUpToDate. func (mr *MockStoreMockRecorder) IsUpToDate(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsUpToDate", reflect.TypeOf((*MockStore)(nil).IsUpToDate), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasMinimalVersion", reflect.TypeOf((*MockStore)(nil).IsUpToDate), ctx) } // ListAccounts mocks base method. diff --git a/internal/storage/bucket/bucket.go b/internal/storage/bucket/bucket.go index ef1347243..8db0c7dda 100644 --- a/internal/storage/bucket/bucket.go +++ b/internal/storage/bucket/bucket.go @@ -24,7 +24,7 @@ func (b *Bucket) Migrate(ctx context.Context, tracer trace.Tracer) error { return migrate(ctx, tracer, b.db, b.name) } -func (b *Bucket) IsUpToDate(ctx context.Context) (bool, error) { +func (b *Bucket) HasMinimalVersion(ctx context.Context) (bool, error) { migrator := GetMigrator(b.db, b.name) lastVersion, err := migrator.GetLastVersion(ctx) if err != nil { diff --git a/internal/storage/bucket/migrations/11-make-stateless/up.sql b/internal/storage/bucket/migrations/11-make-stateless/up.sql index 9b92f4282..b8d2c9666 100644 --- a/internal/storage/bucket/migrations/11-make-stateless/up.sql +++ b/internal/storage/bucket/migrations/11-make-stateless/up.sql @@ -513,3 +513,35 @@ $do$ end loop; END $do$; + +-- following index will enforce uniqueness of transaction reference until the appropriate index is full built (see next migration) +create or replace function enforce_reference_uniqueness() returns trigger + security definer + language plpgsql +as +$$ +begin + -- Temporary magic number + -- The migration 13 will remove the trigger + perform pg_advisory_xact_lock(9999999); + + if exists( + select 1 + from transactions + where reference = new.reference + and ledger = new.ledger + and id != new.id + ) then + raise exception 'duplicate reference'; + end if; + + return new; +end +$$ set search_path from current; + +create constraint trigger enforce_reference_uniqueness +after insert on transactions +deferrable initially deferred +for each row +when ( new.reference is not null ) +execute procedure enforce_reference_uniqueness(); \ No newline at end of file diff --git a/internal/storage/bucket/migrations/13-create-ledger-indexes/up.sql b/internal/storage/bucket/migrations/13-create-ledger-indexes/up.sql index 1efb24192..5fa90936e 100644 --- a/internal/storage/bucket/migrations/13-create-ledger-indexes/up.sql +++ b/internal/storage/bucket/migrations/13-create-ledger-indexes/up.sql @@ -1,5 +1,8 @@ set search_path = '{{.Schema}}'; +drop trigger enforce_reference_uniqueness on transactions; +drop function enforce_reference_uniqueness(); + drop index transactions_reference; alter index transactions_reference2 rename to transactions_reference; diff --git a/internal/storage/driver/driver.go b/internal/storage/driver/driver.go index 7dd772242..a260fdfdf 100644 --- a/internal/storage/driver/driver.go +++ b/internal/storage/driver/driver.go @@ -216,6 +216,10 @@ func (d *Driver) UpgradeAllBuckets(ctx context.Context) error { return nil } +func (d *Driver) GetDB() *bun.DB { + return d.db +} + func New(db *bun.DB, opts ...Option) *Driver { ret := &Driver{ db: db, diff --git a/internal/storage/ledger/legacy/adapters.go b/internal/storage/ledger/legacy/adapters.go index 3d94a53f9..4fa415d2d 100644 --- a/internal/storage/ledger/legacy/adapters.go +++ b/internal/storage/ledger/legacy/adapters.go @@ -116,7 +116,7 @@ func (d *DefaultStoreAdapter) GetVolumesWithBalances(ctx context.Context, q ledg } func (d *DefaultStoreAdapter) IsUpToDate(ctx context.Context) (bool, error) { - return d.newStore.IsUpToDate(ctx) + return d.newStore.HasMinimalVersion(ctx) } func (d *DefaultStoreAdapter) GetMigrationsInfo(ctx context.Context) ([]migrations.Info, error) { diff --git a/internal/storage/ledger/main_test.go b/internal/storage/ledger/main_test.go index 7670169d9..21b923a44 100644 --- a/internal/storage/ledger/main_test.go +++ b/internal/storage/ledger/main_test.go @@ -8,16 +8,13 @@ import ( . "github.com/formancehq/go-libs/v2/testing/utils" "github.com/formancehq/ledger/internal/storage/driver" ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" - "go.opentelemetry.io/otel/trace/noop" "math/big" "os" - "sync/atomic" "testing" "github.com/formancehq/go-libs/v2/bun/bundebug" "github.com/formancehq/go-libs/v2/testing/docker" ledger "github.com/formancehq/ledger/internal" - "github.com/formancehq/ledger/internal/storage/bucket" "github.com/google/go-cmp/cmp" "github.com/uptrace/bun/dialect/pgdialect" @@ -33,7 +30,6 @@ import ( var ( srv = NewDeferred[*pgtesting.PostgresServer]() bunDB = NewDeferred[*bun.DB]() - ledgerCount = atomic.Int64{} ) func TestMain(m *testing.M) { @@ -68,10 +64,9 @@ type T interface { Cleanup(func()) } -func newLedgerStore(t T) *ledgerstore.Store { +func newDriver(t T) *driver.Driver { t.Helper() - ledgerName := uuid.NewString()[:8] ctx := logging.TestingContext() Wait(srv, bunDB) @@ -88,15 +83,23 @@ func newLedgerStore(t T) *ledgerstore.Store { require.NoError(t, driver.Migrate(ctx, db)) + return driver.New(bunDB.GetValue()) +} + +func newLedgerStore(t T) *ledgerstore.Store { + t.Helper() + + driver := newDriver(t) + ledgerName := uuid.NewString()[:8] + ctx := logging.TestingContext() + l := ledger.MustNewWithDefault(ledgerName) l.Bucket = ledgerName - l.ID = int(ledgerCount.Add(1)) - b := bucket.New(bunDB.GetValue(), ledgerName) - require.NoError(t, b.Migrate(ctx, noop.Tracer{})) - require.NoError(t, b.AddLedger(ctx, l, bunDB.GetValue())) + store, err := driver.CreateLedger(ctx, &l) + require.NoError(t, err) - return ledgerstore.New(bunDB.GetValue(), b, l) + return store } func bigIntComparer(v1 *big.Int, v2 *big.Int) bool { diff --git a/internal/storage/ledger/store.go b/internal/storage/ledger/store.go index d7611c114..26d2f6e05 100644 --- a/internal/storage/ledger/store.go +++ b/internal/storage/ledger/store.go @@ -186,8 +186,8 @@ func New(db bun.IDB, bucket *bucket.Bucket, ledger ledger.Ledger, opts ...Option return ret } -func (s *Store) IsUpToDate(ctx context.Context) (bool, error) { - return s.bucket.IsUpToDate(ctx) +func (s *Store) HasMinimalVersion(ctx context.Context) (bool, error) { + return s.bucket.HasMinimalVersion(ctx) } func (s *Store) GetMigrationsInfo(ctx context.Context) ([]migrations.Info, error) { diff --git a/internal/storage/ledger/transactions.go b/internal/storage/ledger/transactions.go index 801b1ef18..40c93c5fb 100644 --- a/internal/storage/ledger/transactions.go +++ b/internal/storage/ledger/transactions.go @@ -240,6 +240,25 @@ func (s *Store) selectTransactions(date *time.Time, expandVolumes, expandEffecti } func (s *Store) CommitTransaction(ctx context.Context, tx *ledger.Transaction) error { + + // todo(next-minor): remove that on ledger 2.3 when the corresponding index will be completely built (see migration 12) + //if tx.Reference != "" { + // // Magic number, as long as no other process try to take the same exact lock for another reason, it will be ok. + // // This code will be removed in the next minor by the way. + // _, err := s.db.ExecContext(ctx, `select pg_advisory_xact_lock(99999999999)`) + // if err != nil { + // return err + // } + // + // exists, err := s.db.NewSelect(). + // ModelTableExpr(s.GetPrefixedRelationName("transactions")). + // Where("reference = ?", tx.Reference). + // Exists(ctx) + // if exists { + // return ledgercontroller.NewErrTransactionReferenceConflict(tx.Reference) + // } + //} + postCommitVolumes, err := s.UpdateVolumes(ctx, tx.VolumeUpdates()...) if err != nil { return fmt.Errorf("failed to update balances: %w", err) @@ -400,6 +419,11 @@ func (s *Store) InsertTransaction(ctx context.Context, tx *ledger.Transaction) e if err.(postgres.ErrConstraintsFailed).GetConstraint() == "transactions_reference" { return nil, ledgercontroller.NewErrTransactionReferenceConflict(tx.Reference) } + case errors.Is(err, postgres.ErrRaisedException{}): + // todo(next-minor): remove this test + if err.(postgres.ErrRaisedException).GetMessage() == "duplicate reference" { + return nil, ledgercontroller.NewErrTransactionReferenceConflict(tx.Reference) + } default: return nil, err } diff --git a/internal/storage/ledger/transactions_test.go b/internal/storage/ledger/transactions_test.go index 129debaf0..5978f51a0 100644 --- a/internal/storage/ledger/transactions_test.go +++ b/internal/storage/ledger/transactions_test.go @@ -7,6 +7,9 @@ import ( "database/sql" "fmt" "github.com/alitto/pond" + "github.com/formancehq/ledger/internal/storage/bucket" + ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" + "github.com/google/uuid" "math/big" "slices" "testing" @@ -603,6 +606,84 @@ func TestTransactionsInsert(t *testing.T) { require.Error(t, err) require.True(t, errors.Is(err, ledgercontroller.ErrTransactionReferenceConflict{})) }) + // todo(next-minor): remove this test + t.Run("check reference conflict with minimal store version", func(t *testing.T) { + t.Parallel() + + driver := newDriver(t) + ledgerName := uuid.NewString()[:8] + + l := ledger.MustNewWithDefault(ledgerName) + l.Bucket = ledgerName + + migrator := bucket.GetMigrator(driver.GetDB(), ledgerName) + for i := 0; i < bucket.MinimalSchemaVersion; i++ { + require.NoError(t, migrator.UpByOne(ctx)) + } + + b := bucket.New(driver.GetDB(), ledgerName) + err := b.AddLedger(ctx, l, driver.GetDB()) + require.NoError(t, err) + + store := ledgerstore.New(driver.GetDB(), b, l) + + const nbTry = 100 + + for i := 0; i < nbTry; i++ { + errChan := make(chan error, 2) + + // Create a simple tx + tx1 := ledger.Transaction{ + TransactionData: ledger.TransactionData{ + Timestamp: now, + Reference: fmt.Sprintf("foo:%d", i), + Postings: []ledger.Posting{ + ledger.NewPosting("world", "bank", "USD/2", big.NewInt(100)), + }, + }, + } + go func() { + errChan <- store.InsertTransaction(ctx, &tx1) + }() + + // Create another tx with the same reference + tx2 := ledger.Transaction{ + TransactionData: ledger.TransactionData{ + Timestamp: now, + Reference: fmt.Sprintf("foo:%d", i), + Postings: []ledger.Posting{ + ledger.NewPosting("world", "bank", "USD/2", big.NewInt(100)), + }, + }, + } + go func() { + errChan <- store.InsertTransaction(ctx, &tx2) + }() + + select { + case err1 := <-errChan: + if err1 != nil { + require.True(t, errors.Is(err1, ledgercontroller.ErrTransactionReferenceConflict{})) + select { + case err2 := <-errChan: + require.NoError(t, err2) + case <-time.After(time.Second): + require.Fail(t, "should have received an error") + } + } else { + select { + case err2 := <-errChan: + require.Error(t, err2) + require.True(t, errors.Is(err2, ledgercontroller.ErrTransactionReferenceConflict{})) + case <-time.After(time.Second): + require.Fail(t, "should have received an error") + } + } + case <-time.After(time.Second): + require.Fail(t, "should have received an error") + } + } + }) t.Run("check denormalization", func(t *testing.T) { t.Parallel() From 5314238836a323691f5956a11b19df86bf532182 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 30 Oct 2024 18:32:27 +0100 Subject: [PATCH 04/71] feat: move long migrations in dedicated dir --- docker-compose.yml | 2 - go.mod | 2 +- go.sum | 4 ++ .../controller/ledger/store_generated_test.go | 4 +- .../migrations/11-make-stateless/up.sql | 57 ++++++++++++------- .../12-transaction-sequence-index/notes.yaml | 1 + .../12-transaction-sequence-index/up.sql | 1 + .../13-accounts-sequence-index/notes.yaml | 1 + .../13-accounts-sequence-index/up.sql | 1 + .../notes.yaml | 0 .../up.sql | 0 .../notes.yaml | 0 .../up.sql | 0 internal/storage/ledger/transactions.go | 20 +------ internal/storage/ledger/transactions_test.go | 2 +- 15 files changed, 50 insertions(+), 45 deletions(-) create mode 100644 internal/storage/bucket/migrations/12-transaction-sequence-index/notes.yaml create mode 100644 internal/storage/bucket/migrations/12-transaction-sequence-index/up.sql create mode 100644 internal/storage/bucket/migrations/13-accounts-sequence-index/notes.yaml create mode 100644 internal/storage/bucket/migrations/13-accounts-sequence-index/up.sql rename internal/storage/bucket/migrations/{12-transaction-reference-index => 14-transaction-reference-index}/notes.yaml (100%) rename internal/storage/bucket/migrations/{12-transaction-reference-index => 14-transaction-reference-index}/up.sql (100%) rename internal/storage/bucket/migrations/{13-create-ledger-indexes => 15-create-ledger-indexes}/notes.yaml (100%) rename internal/storage/bucket/migrations/{13-create-ledger-indexes => 15-create-ledger-indexes}/up.sql (100%) diff --git a/docker-compose.yml b/docker-compose.yml index 85ba8ec86..d315ca717 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.8' - volumes: postgres: {} diff --git a/go.mod b/go.mod index b7dc25055..57cdb46a0 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 github.com/bluele/gcache v0.0.2 github.com/dop251/goja v0.0.0-20241009100908-5f46f2705ca3 - github.com/formancehq/go-libs/v2 v2.0.1-0.20241029111513-edb146ee0db7 + github.com/formancehq/go-libs/v2 v2.0.1-0.20241030165600-9060fd311289 github.com/formancehq/ledger/pkg/client v0.0.0-00010101000000-000000000000 github.com/go-chi/chi/v5 v5.1.0 github.com/go-chi/cors v1.2.1 diff --git a/go.sum b/go.sum index e61022ae2..7370be6e3 100644 --- a/go.sum +++ b/go.sum @@ -106,6 +106,10 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/formancehq/go-libs/v2 v2.0.1-0.20241029111513-edb146ee0db7 h1:OZz4N9nIj814aIgpqIvojndtae+N9Vqj5MJgKPIzJ5Y= github.com/formancehq/go-libs/v2 v2.0.1-0.20241029111513-edb146ee0db7/go.mod h1:DTqSp28pYPZa4O1WrOg3kobhgTHdk9geGtxnws9EViM= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241030160027-898dbd1a42af h1:7POEnA2uHO+a8HNO+LGCuSVpBolflAFcNR0N1BhEuRA= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241030160027-898dbd1a42af/go.mod h1:DTqSp28pYPZa4O1WrOg3kobhgTHdk9geGtxnws9EViM= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241030165600-9060fd311289 h1:XPjN3V3ONd+rhoJN2Sv7aVa+4NZbEuWMM1HIfgm22NQ= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241030165600-9060fd311289/go.mod h1:DTqSp28pYPZa4O1WrOg3kobhgTHdk9geGtxnws9EViM= github.com/formancehq/numscript v0.0.9-0.20241009144012-1150c14a1417 h1:LOd5hxnXDIBcehFrpW1OnXk+VSs0yJXeu1iAOO+Hji4= github.com/formancehq/numscript v0.0.9-0.20241009144012-1150c14a1417/go.mod h1:btuSv05cYwi9BvLRxVs5zrunU+O1vTgigG1T6UsawcY= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= diff --git a/internal/controller/ledger/store_generated_test.go b/internal/controller/ledger/store_generated_test.go index 59479b0da..1618ae7c3 100644 --- a/internal/controller/ledger/store_generated_test.go +++ b/internal/controller/ledger/store_generated_test.go @@ -364,7 +364,7 @@ func (mr *MockStoreMockRecorder) GetVolumesWithBalances(ctx, q any) *gomock.Call // IsUpToDate mocks base method. func (m *MockStore) IsUpToDate(ctx context.Context) (bool, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HasMinimalVersion", ctx) + ret := m.ctrl.Call(m, "IsUpToDate", ctx) ret0, _ := ret[0].(bool) ret1, _ := ret[1].(error) return ret0, ret1 @@ -373,7 +373,7 @@ func (m *MockStore) IsUpToDate(ctx context.Context) (bool, error) { // IsUpToDate indicates an expected call of IsUpToDate. func (mr *MockStoreMockRecorder) IsUpToDate(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasMinimalVersion", reflect.TypeOf((*MockStore)(nil).IsUpToDate), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsUpToDate", reflect.TypeOf((*MockStore)(nil).IsUpToDate), ctx) } // ListAccounts mocks base method. diff --git a/internal/storage/bucket/migrations/11-make-stateless/up.sql b/internal/storage/bucket/migrations/11-make-stateless/up.sql index b8d2c9666..182ce53b4 100644 --- a/internal/storage/bucket/migrations/11-make-stateless/up.sql +++ b/internal/storage/bucket/migrations/11-make-stateless/up.sql @@ -19,7 +19,7 @@ create or replace function transaction_date() returns timestamp as $$ select ret; end if; - return ret; + return ret at time zone 'utc'; end $$ language plpgsql; @@ -33,8 +33,8 @@ alter table moves add column transactions_id bigint, alter column post_commit_volumes drop not null, alter column post_commit_effective_volumes drop not null, -alter column insertion_date set default (transaction_date() at time zone 'utc'), -alter column effective_date set default (transaction_date() at time zone 'utc'), +alter column insertion_date set default transaction_date(), +alter column effective_date set default transaction_date(), alter column account_address_array drop not null; alter table moves @@ -60,7 +60,6 @@ from ( group by move.accounts_address, move.asset ) data $$ set search_path from current; - create or replace function get_aggregated_effective_volumes_for_transaction(_ledger varchar, tx numeric) returns jsonb stable language sql @@ -201,23 +200,25 @@ execute procedure set_compat_on_transactions_metadata(); alter table transactions add column post_commit_volumes jsonb, -add column inserted_at timestamp without time zone default (transaction_date() at time zone 'utc'), -alter column timestamp set default (transaction_date() at time zone 'utc'), -alter column id type bigint; - -create index transactions_sequences on transactions (id, seq); +-- todo: set in subsequent migration `default transaction_date()`, +-- otherwise the function is called for every existing lines +add column inserted_at timestamp without time zone, +alter column timestamp set default transaction_date() +-- todo: we should change the type of this column, but actually it cause a full lock of the table +-- alter column id type bigint +; alter table logs add column memento bytea, add column idempotency_hash bytea, alter column hash drop not null, -alter column date set default (transaction_date() at time zone 'utc'); +alter column date set default transaction_date(); alter table accounts alter column address_array drop not null, -alter column first_usage set default (transaction_date() at time zone 'utc'), -alter column insertion_date set default (transaction_date() at time zone 'utc'), -alter column updated_at set default (transaction_date() at time zone 'utc') +alter column first_usage set default transaction_date(), +alter column insertion_date set default transaction_date(), +alter column updated_at set default transaction_date() ; create table accounts_volumes ( @@ -230,8 +231,6 @@ create table accounts_volumes ( primary key (ledger, accounts_address, asset) ); -create index accounts_sequences on accounts (address, seq); - alter table transactions_metadata add column transactions_id bigint; @@ -364,6 +363,25 @@ from (select row_number() over () as number, v.value select null) v) data $$ set search_path from current; +-- todo(next-minor): remove that on future version when the table will have this default value (need to fill nulls before) +create or replace function set_transaction_inserted_at() returns trigger + security definer + language plpgsql +as +$$ +begin + new.inserted_at = transaction_date(); + + return new; +end +$$ set search_path from current; + +create trigger set_transaction_inserted_at +before insert on transactions +for each row +when ( new.inserted_at is null ) +execute procedure set_transaction_inserted_at(); + create or replace function set_transaction_addresses() returns trigger security definer language plpgsql @@ -469,7 +487,6 @@ $do$ execute vsql; vsql = 'select setval(''"transaction_id_' || ledger.id || '"'', coalesce((select max(id) + 1 from transactions where ledger = ''' || ledger.name || '''), 1)::bigint, false)'; - raise info '%', vsql; execute vsql; -- create a sequence for logs by ledger instead of a sequence of the table as we want to have contiguous ids @@ -521,9 +538,7 @@ create or replace function enforce_reference_uniqueness() returns trigger as $$ begin - -- Temporary magic number - -- The migration 13 will remove the trigger - perform pg_advisory_xact_lock(9999999); + perform pg_advisory_xact_lock(hashtext('reference-check' || current_schema)); if exists( select 1 @@ -544,4 +559,6 @@ after insert on transactions deferrable initially deferred for each row when ( new.reference is not null ) -execute procedure enforce_reference_uniqueness(); \ No newline at end of file +execute procedure enforce_reference_uniqueness(); + + diff --git a/internal/storage/bucket/migrations/12-transaction-sequence-index/notes.yaml b/internal/storage/bucket/migrations/12-transaction-sequence-index/notes.yaml new file mode 100644 index 000000000..e1e842c81 --- /dev/null +++ b/internal/storage/bucket/migrations/12-transaction-sequence-index/notes.yaml @@ -0,0 +1 @@ +name: Create transaction sequences index concurrently diff --git a/internal/storage/bucket/migrations/12-transaction-sequence-index/up.sql b/internal/storage/bucket/migrations/12-transaction-sequence-index/up.sql new file mode 100644 index 000000000..53ecd3c68 --- /dev/null +++ b/internal/storage/bucket/migrations/12-transaction-sequence-index/up.sql @@ -0,0 +1 @@ +create index concurrently transactions_sequences on "{{.Schema}}".transactions (id, seq); \ No newline at end of file diff --git a/internal/storage/bucket/migrations/13-accounts-sequence-index/notes.yaml b/internal/storage/bucket/migrations/13-accounts-sequence-index/notes.yaml new file mode 100644 index 000000000..3b8dfcad1 --- /dev/null +++ b/internal/storage/bucket/migrations/13-accounts-sequence-index/notes.yaml @@ -0,0 +1 @@ +name: Create accounts sequences index concurrently diff --git a/internal/storage/bucket/migrations/13-accounts-sequence-index/up.sql b/internal/storage/bucket/migrations/13-accounts-sequence-index/up.sql new file mode 100644 index 000000000..b49646204 --- /dev/null +++ b/internal/storage/bucket/migrations/13-accounts-sequence-index/up.sql @@ -0,0 +1 @@ +create index concurrently accounts_sequences on "{{.Schema}}".accounts (address, seq); \ No newline at end of file diff --git a/internal/storage/bucket/migrations/12-transaction-reference-index/notes.yaml b/internal/storage/bucket/migrations/14-transaction-reference-index/notes.yaml similarity index 100% rename from internal/storage/bucket/migrations/12-transaction-reference-index/notes.yaml rename to internal/storage/bucket/migrations/14-transaction-reference-index/notes.yaml diff --git a/internal/storage/bucket/migrations/12-transaction-reference-index/up.sql b/internal/storage/bucket/migrations/14-transaction-reference-index/up.sql similarity index 100% rename from internal/storage/bucket/migrations/12-transaction-reference-index/up.sql rename to internal/storage/bucket/migrations/14-transaction-reference-index/up.sql diff --git a/internal/storage/bucket/migrations/13-create-ledger-indexes/notes.yaml b/internal/storage/bucket/migrations/15-create-ledger-indexes/notes.yaml similarity index 100% rename from internal/storage/bucket/migrations/13-create-ledger-indexes/notes.yaml rename to internal/storage/bucket/migrations/15-create-ledger-indexes/notes.yaml diff --git a/internal/storage/bucket/migrations/13-create-ledger-indexes/up.sql b/internal/storage/bucket/migrations/15-create-ledger-indexes/up.sql similarity index 100% rename from internal/storage/bucket/migrations/13-create-ledger-indexes/up.sql rename to internal/storage/bucket/migrations/15-create-ledger-indexes/up.sql diff --git a/internal/storage/ledger/transactions.go b/internal/storage/ledger/transactions.go index 40c93c5fb..a8b522e7d 100644 --- a/internal/storage/ledger/transactions.go +++ b/internal/storage/ledger/transactions.go @@ -158,7 +158,7 @@ func (s *Store) selectTransactions(date *time.Time, expandVolumes, expandEffecti `), ). Column("transactions_id"). - ColumnExpr("aggregate_objects(post_commit_effective_volumes::jsonb) as post_commit_effective_volumes"). + ColumnExpr("public.aggregate_objects(post_commit_effective_volumes::jsonb) as post_commit_effective_volumes"). Group("transactions_id"), ). ColumnExpr("pcev.*") @@ -241,24 +241,6 @@ func (s *Store) selectTransactions(date *time.Time, expandVolumes, expandEffecti func (s *Store) CommitTransaction(ctx context.Context, tx *ledger.Transaction) error { - // todo(next-minor): remove that on ledger 2.3 when the corresponding index will be completely built (see migration 12) - //if tx.Reference != "" { - // // Magic number, as long as no other process try to take the same exact lock for another reason, it will be ok. - // // This code will be removed in the next minor by the way. - // _, err := s.db.ExecContext(ctx, `select pg_advisory_xact_lock(99999999999)`) - // if err != nil { - // return err - // } - // - // exists, err := s.db.NewSelect(). - // ModelTableExpr(s.GetPrefixedRelationName("transactions")). - // Where("reference = ?", tx.Reference). - // Exists(ctx) - // if exists { - // return ledgercontroller.NewErrTransactionReferenceConflict(tx.Reference) - // } - //} - postCommitVolumes, err := s.UpdateVolumes(ctx, tx.VolumeUpdates()...) if err != nil { return fmt.Errorf("failed to update balances: %w", err) diff --git a/internal/storage/ledger/transactions_test.go b/internal/storage/ledger/transactions_test.go index 5978f51a0..6bb0312e3 100644 --- a/internal/storage/ledger/transactions_test.go +++ b/internal/storage/ledger/transactions_test.go @@ -433,7 +433,7 @@ func TestTransactionsCommit(t *testing.T) { errChan <- nil }) } - wp.StopAndWaitFor(2 * time.Second) + wp.StopAndWait() close(errChan) for err := range errChan { From 582387b2495abb84ea0fcc11028b1e526259cdd1 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 7 Nov 2024 14:31:19 +0100 Subject: [PATCH 05/71] fix: missing content type on /info --- cmd/serve.go | 12 ++++++++++++ internal/api/module.go | 3 --- internal/api/router.go | 10 ---------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 26cd9cce2..620a8fd52 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -1,6 +1,7 @@ package cmd import ( + "github.com/formancehq/go-libs/v2/logging" "net/http" "net/http/pprof" "time" @@ -99,6 +100,7 @@ func NewServeCommand() *cobra.Command { Handler chi.Router HealthController *health.HealthController + Logger logging.Logger MeterProvider *metric.MeterProvider `optional:"true"` Exporter *otlpmetrics.InMemoryExporter `optional:"true"` @@ -109,6 +111,7 @@ func NewServeCommand() *cobra.Command { params.MeterProvider, params.Exporter, params.HealthController, + params.Logger, params.Handler, ) }), @@ -163,9 +166,18 @@ func assembleFinalRouter( meterProvider *metric.MeterProvider, exporter *otlpmetrics.InMemoryExporter, healthController *health.HealthController, + logger logging.Logger, handler http.Handler, ) *chi.Mux { wrappedRouter := chi.NewRouter() + wrappedRouter.Use(func(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + r = r.WithContext(logging.ContextWithLogger(r.Context(), logger)) + + handler.ServeHTTP(w, r) + }) + }) wrappedRouter.Route("/_/", func(r chi.Router) { if exporter != nil { r.Handle("/metrics", otlpmetrics.NewInMemoryExporterHandler( diff --git a/internal/api/module.go b/internal/api/module.go index 40a83fa41..45a144452 100644 --- a/internal/api/module.go +++ b/internal/api/module.go @@ -4,7 +4,6 @@ import ( _ "embed" "github.com/formancehq/go-libs/v2/auth" "github.com/formancehq/go-libs/v2/health" - "github.com/formancehq/go-libs/v2/logging" "github.com/formancehq/ledger/internal/controller/system" "github.com/go-chi/chi/v5" "go.opentelemetry.io/otel/trace" @@ -22,13 +21,11 @@ func Module(cfg Config) fx.Option { fx.Provide(func( backend system.Controller, authenticator auth.Authenticator, - logger logging.Logger, tracer trace.TracerProvider, ) chi.Router { return NewRouter( backend, authenticator, - logger, "develop", cfg.Debug, WithTracer(tracer.Tracer("api")), diff --git a/internal/api/router.go b/internal/api/router.go index 5ee8d2362..a9025f2ca 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -7,7 +7,6 @@ import ( nooptracer "go.opentelemetry.io/otel/trace/noop" "net/http" - "github.com/formancehq/go-libs/v2/logging" "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" @@ -22,7 +21,6 @@ import ( func NewRouter( systemController system.Controller, authenticator auth.Authenticator, - logger logging.Logger, version string, debug bool, opts ...RouterOption, @@ -36,14 +34,6 @@ func NewRouter( mux := chi.NewRouter() mux.Use( middleware.Recoverer, - func(handler http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - r = r.WithContext(logging.ContextWithLogger(r.Context(), logger)) - - handler.ServeHTTP(w, r) - }) - }, cors.New(cors.Options{ AllowOriginFunc: func(r *http.Request, origin string) bool { return true From baf0f207d412c729ad5586de975270c9d921dc6d Mon Sep 17 00:00:00 2001 From: Ragot Geoffrey Date: Fri, 8 Nov 2024 12:20:46 +0100 Subject: [PATCH 06/71] test: add rolling upgrade test (#549) --- .github/workflows/main.yml | 41 +++ CONTRIBUTING.md | 10 +- Earthfile | 33 +- cmd/docs.go | 13 + .../docs/events/main.go => cmd/docs_events.go | 11 +- tools/docs/flags/main.go => cmd/docs_flags.go | 14 +- cmd/root.go | 1 + .../otel-collector-config.yaml | 0 .../{ => docker-compose}/prometheus.yaml | 0 deployments/helm/.helmignore | 23 ++ deployments/helm/Chart.yaml | 24 ++ deployments/helm/Earthfile | 12 + deployments/helm/README.md | 5 + deployments/helm/templates/NOTES.txt | 22 ++ deployments/helm/templates/_helpers.tpl | 62 ++++ deployments/helm/templates/deployment.yaml | 84 +++++ deployments/helm/templates/hpa.yaml | 32 ++ deployments/helm/templates/ingress.yaml | 61 ++++ deployments/helm/templates/service.yaml | 15 + .../helm/templates/serviceaccount.yaml | 13 + .../helm/templates/tests/test-connection.yaml | 15 + deployments/helm/values.yaml | 105 ++++++ deployments/pulumi/Earthfile | 11 + deployments/pulumi/Pulumi.yaml | 7 + deployments/pulumi/README.md | 5 + deployments/pulumi/go.mod | 92 +++++ deployments/pulumi/go.sum | 315 ++++++++++++++++ deployments/pulumi/main.go | 71 ++++ docker-compose.yml | 4 +- .../_default/diagrams/orphans/orphans.dot | 7 +- .../_default/diagrams/orphans/orphans.png | Bin 63843 -> 70865 bytes .../summary/relationships.real.compact.dot | 34 +- .../summary/relationships.real.large.dot | 34 +- .../tables/goose_db_version.1degree.dot | 7 +- .../tables/goose_db_version.1degree.png | Bin 11668 -> 15800 bytes .../diagrams/tables/transactions.1degree.dot | 2 +- .../diagrams/tables/transactions.1degree.png | Bin 73219 -> 73342 bytes .../diagrams/tables/transactions.2degrees.dot | 2 +- .../diagrams/tables/transactions.2degrees.png | Bin 75941 -> 76011 bytes .../_system/diagrams/orphans/orphans.dot | 7 +- .../_system/diagrams/orphans/orphans.png | Bin 22078 -> 26264 bytes .../tables/goose_db_version.1degree.dot | 7 +- .../tables/goose_db_version.1degree.png | Bin 11668 -> 15800 bytes go.mod | 4 +- go.sum | 8 +- scripts/export-database-schema.sh | 16 - test/rolling-upgrades/Earthfile | 118 ++++++ test/rolling-upgrades/README.md | 55 +++ test/rolling-upgrades/go.mod | 107 ++++++ test/rolling-upgrades/go.sum | 337 ++++++++++++++++++ test/rolling-upgrades/main_test.go | 255 +++++++++++++ tools/generator/Earthfile | 65 ++++ tools/generator/cmd/root.go | 52 +-- tools/generator/go.mod | 49 +++ tools/generator/go.sum | 115 ++++++ 55 files changed, 2261 insertions(+), 121 deletions(-) create mode 100644 cmd/docs.go rename tools/docs/events/main.go => cmd/docs_events.go (88%) rename tools/docs/flags/main.go => cmd/docs_flags.go (80%) rename deployments/{ => docker-compose}/otel-collector-config.yaml (100%) rename deployments/{ => docker-compose}/prometheus.yaml (100%) create mode 100644 deployments/helm/.helmignore create mode 100644 deployments/helm/Chart.yaml create mode 100644 deployments/helm/Earthfile create mode 100644 deployments/helm/README.md create mode 100644 deployments/helm/templates/NOTES.txt create mode 100644 deployments/helm/templates/_helpers.tpl create mode 100644 deployments/helm/templates/deployment.yaml create mode 100644 deployments/helm/templates/hpa.yaml create mode 100644 deployments/helm/templates/ingress.yaml create mode 100644 deployments/helm/templates/service.yaml create mode 100644 deployments/helm/templates/serviceaccount.yaml create mode 100644 deployments/helm/templates/tests/test-connection.yaml create mode 100644 deployments/helm/values.yaml create mode 100644 deployments/pulumi/Earthfile create mode 100644 deployments/pulumi/Pulumi.yaml create mode 100644 deployments/pulumi/README.md create mode 100644 deployments/pulumi/go.mod create mode 100644 deployments/pulumi/go.sum create mode 100644 deployments/pulumi/main.go delete mode 100755 scripts/export-database-schema.sh create mode 100644 test/rolling-upgrades/Earthfile create mode 100644 test/rolling-upgrades/README.md create mode 100644 test/rolling-upgrades/go.mod create mode 100644 test/rolling-upgrades/go.sum create mode 100644 test/rolling-upgrades/main_test.go create mode 100644 tools/generator/Earthfile create mode 100644 tools/generator/go.mod create mode 100644 tools/generator/go.sum diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 367248788..23968870a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -66,6 +66,7 @@ jobs: token: ${{ secrets.NUMARY_GITHUB_TOKEN }} - run: > earthly + --no-output --allow-privileged --secret SPEAKEASY_API_KEY=$SPEAKEASY_API_KEY ${{ contains(github.event.pull_request.labels.*.name, 'no-cache') && '--no-cache' || '' }} @@ -77,6 +78,46 @@ jobs: env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + TestsDeployments: + runs-on: "formance-runner" + concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-deployments-tests + cancel-in-progress: false + steps: + - uses: 'actions/checkout@v4' + with: + fetch-depth: 0 + - name: Setup Env + uses: ./.github/actions/env + with: + token: ${{ secrets.NUMARY_GITHUB_TOKEN }} + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: "NumaryBot" + password: ${{ secrets.NUMARY_GITHUB_TOKEN }} + - run: > + earthly + --allow-privileged + --no-output + --push + --secret GITHUB_TOKEN=$GITHUB_TOKEN + --secret KUBE_APISERVER=$KUBE_APISERVER + --secret KUBE_TOKEN=$KUBE_TOKEN + --secret PULUMI_ACCESS_TOKEN=$PULUMI_ACCESS_TOKEN + ${{ contains(github.event.pull_request.labels.*.name, 'no-cache') && '--no-cache' || '' }} + ./test/rolling-upgrades+run + --CLUSTER_NAME ledger-${{ github.event.number }} + --NO_CLEANUP=${{ contains(github.event.pull_request.labels.*.name, 'no-cleanup') && 'true' || 'false' }} + --NO_CLEANUP_ON_FAILURE=true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + KUBE_APISERVER: ${{ secrets.FORMANCE_DEV_KUBE_API_SERVER_ADDRESS }} + KUBE_TOKEN: ${{ secrets.FORMANCE_DEV_KUBE_TOKEN }} + PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }} + + GoReleaser: runs-on: "formance-runner" if: contains(github.event.pull_request.labels.*.name, 'build-images') || github.ref == 'refs/heads/main' || github.event_name == 'merge_group' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b73b54152..6ebc0ec2f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -382,10 +382,12 @@ That's because, if we would do that, we would have frequent serialization errors Tests are split in different scopes : * Unit tests: as any go app, you will find unit test along the source code in _test.go files over the app. -* [e2e](./test/e2e) : End to end test. Those tests are mainly api tests, and app lifecycle tests. It checks than the ledger endpoint works as expected. -* [migrations](./test/migrations) : Migrations tests. Tests inside this package allow to import an existing database to apply current code migrations on it. -* [performance](./test/performance) : Performance tests. Tests inside this package test performance of the ledger. -* [stress](./test/stress) : Stress tests. Tests inside this package ensure than ledger state stay consistent under high concurrency. +* Integration tests: tests that involve the ledger and external services, like the database. + * [e2e](./test/e2e) : End to end test. Those tests are mainly api tests, and app lifecycle tests. It checks than the ledger endpoint works as expected. + * [migrations](./test/migrations) : Migrations tests. Tests inside this package allow to import an existing database to apply current code migrations on it. + * [performance](./test/performance) : Performance tests. Tests inside this package test performance of the ledger. + * [stress](./test/stress) : Stress tests. Tests inside this package ensure than ledger state stay consistent under high concurrency. + * [rolling-upgrades](./test/rolling-upgrades) : Rolling upgrade test under K8S ## API changes diff --git a/Earthfile b/Earthfile index 0c094ac6e..5ecc79d6a 100644 --- a/Earthfile +++ b/Earthfile @@ -1,4 +1,4 @@ -VERSION 0.8 +VERSION --wildcard-builds 0.8 PROJECT FormanceHQ/ledger IMPORT github.com/formancehq/earthly:tags/v0.17.1 AS core @@ -22,7 +22,7 @@ sources: WORKDIR /src COPY go.mod go.sum ./ RUN go mod download - COPY --dir internal pkg cmd tools . + COPY --dir internal pkg cmd . COPY main.go . SAVE ARTIFACT /src @@ -68,7 +68,6 @@ tests: COPY --dir --pass-args (+generate/*) . ARG includeIntegrationTests="true" - ARG includeEnd2EndTests="true" ARG coverage="" ARG debug=false @@ -128,7 +127,6 @@ lint: SAVE ARTIFACT internal AS LOCAL internal SAVE ARTIFACT pkg AS LOCAL pkg SAVE ARTIFACT test AS LOCAL test - SAVE ARTIFACT tools AS LOCAL tools SAVE ARTIFACT main.go AS LOCAL main.go pre-commit: @@ -142,6 +140,10 @@ pre-commit: BUILD +generate-client BUILD +export-docs-events + # todo: currently not working with earthly + #BUILD ./test/rolling-upgrades+pre-commit + #BUILD ./tools/*+pre-commit + openapi: FROM node:20-alpine RUN apk update && apk add yq @@ -167,6 +169,9 @@ tidy: COPY --dir test . RUN go mod tidy + SAVE ARTIFACT go.mod AS LOCAL go.mod + SAVE ARTIFACT go.sum AS LOCAL go.sum + release: FROM core+builder-image ARG mode=local @@ -189,9 +194,23 @@ generate-client: export-database-schema: FROM +sources RUN go install github.com/roerohan/wait-for-it@latest - COPY --dir scripts scripts WITH DOCKER --load=postgres:15-alpine=+postgres --pull schemaspy/schemaspy:6.2.4 - RUN ./scripts/export-database-schema.sh + RUN bash -c ' + echo "Creating PG server..."; + postgresContainerID=$(docker run -d --rm -e POSTGRES_USER=root -e POSTGRES_PASSWORD=root -e POSTGRES_DB=formance --net=host postgres:15-alpine); + wait-for-it -w 127.0.0.1:5432; + + echo "Creating bucket..."; + go run main.go buckets upgrade _default --postgres-uri "postgres://root:root@127.0.0.1:5432/formance?sslmode=disable"; + + echo "Exporting schemas..."; + docker run --rm -u root \ + -v ./docs/database:/output \ + --net=host \ + schemaspy/schemaspy:6.2.4 -u root -db formance -t pgsql11 -host 127.0.0.1 -port 5432 -p root -schemas _system,_default; + + docker kill "$postgresContainerID"; + ' END SAVE ARTIFACT docs/database/_system/diagrams AS LOCAL docs/database/_system/diagrams SAVE ARTIFACT docs/database/_default/diagrams AS LOCAL docs/database/_default/diagrams @@ -201,6 +220,6 @@ export-docs-events: CACHE --id go-mod-cache /go/pkg/mod CACHE --id go-cache /root/.cache/go-build - RUN go run tools/docs/events/main.go --write-dir docs/events + RUN go run . docs events --write-dir docs/events SAVE ARTIFACT docs/events AS LOCAL docs/events \ No newline at end of file diff --git a/cmd/docs.go b/cmd/docs.go new file mode 100644 index 000000000..e1adf8a33 --- /dev/null +++ b/cmd/docs.go @@ -0,0 +1,13 @@ +package cmd + +import "github.com/spf13/cobra" + +func NewDocsCommand() *cobra.Command { + ret := &cobra.Command{ + Use: "docs", + } + ret.AddCommand(NewDocFlagsCommand()) + ret.AddCommand(NewDocEventsCommand()) + + return ret +} diff --git a/tools/docs/events/main.go b/cmd/docs_events.go similarity index 88% rename from tools/docs/events/main.go rename to cmd/docs_events.go index ffd200162..7996ab195 100644 --- a/tools/docs/events/main.go +++ b/cmd/docs_events.go @@ -1,4 +1,4 @@ -package main +package cmd import ( "encoding/json" @@ -11,11 +11,12 @@ import ( "reflect" ) -func newDocEventsCommand() *cobra.Command { +func NewDocEventsCommand() *cobra.Command { const ( writeDirFlag = "write-dir" ) cmd := &cobra.Command{ + Use: "events", RunE: func(cmd *cobra.Command, _ []string) error { writeDir, err := cmd.Flags().GetString(writeDirFlag) @@ -52,9 +53,3 @@ func newDocEventsCommand() *cobra.Command { return cmd } - -func main() { - if err := newDocEventsCommand().Execute(); err != nil { - os.Exit(1) - } -} diff --git a/tools/docs/flags/main.go b/cmd/docs_flags.go similarity index 80% rename from tools/docs/flags/main.go rename to cmd/docs_flags.go index 48834fd06..9406800ba 100644 --- a/tools/docs/flags/main.go +++ b/cmd/docs_flags.go @@ -1,9 +1,7 @@ -package main +package cmd import ( "fmt" - ledgercmd "github.com/formancehq/ledger/cmd" - "os" "sort" "strings" "text/tabwriter" @@ -12,7 +10,7 @@ import ( "github.com/spf13/pflag" ) -func newDocFlagsCommand() *cobra.Command { +func NewDocFlagsCommand() *cobra.Command { return &cobra.Command{ Use: "flags", RunE: func(cmd *cobra.Command, _ []string) error { @@ -21,7 +19,7 @@ func newDocFlagsCommand() *cobra.Command { allKeys := make([]string, 0) - serveCommand := ledgercmd.NewServeCommand() + serveCommand := NewServeCommand() serveCommand.Flags().VisitAll(func(f *pflag.Flag) { allKeys = append(allKeys, f.Name) }) @@ -51,9 +49,3 @@ func newDocFlagsCommand() *cobra.Command { }, } } - -func main() { - if err := newDocFlagsCommand().Execute(); err != nil { - os.Exit(1) - } -} diff --git a/cmd/root.go b/cmd/root.go index 1a49d271a..b363efb89 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -38,6 +38,7 @@ func NewRootCommand() *cobra.Command { // todo: use provided db ... return upgradeAll(cmd) })) + root.AddCommand(NewDocsCommand()) return root } diff --git a/deployments/otel-collector-config.yaml b/deployments/docker-compose/otel-collector-config.yaml similarity index 100% rename from deployments/otel-collector-config.yaml rename to deployments/docker-compose/otel-collector-config.yaml diff --git a/deployments/prometheus.yaml b/deployments/docker-compose/prometheus.yaml similarity index 100% rename from deployments/prometheus.yaml rename to deployments/docker-compose/prometheus.yaml diff --git a/deployments/helm/.helmignore b/deployments/helm/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/deployments/helm/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/deployments/helm/Chart.yaml b/deployments/helm/Chart.yaml new file mode 100644 index 000000000..7fba0ef93 --- /dev/null +++ b/deployments/helm/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: ledger +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "latest" diff --git a/deployments/helm/Earthfile b/deployments/helm/Earthfile new file mode 100644 index 000000000..f5ce08d71 --- /dev/null +++ b/deployments/helm/Earthfile @@ -0,0 +1,12 @@ +VERSION 0.8 +PROJECT FormanceHQ/ledger + +IMPORT github.com/formancehq/earthly:tags/v0.17.1 AS core + +FROM core+base-image + +sources: + WORKDIR /src + COPY *.yaml . + COPY --dir templates . + SAVE ARTIFACT /src diff --git a/deployments/helm/README.md b/deployments/helm/README.md new file mode 100644 index 000000000..1fe5d4963 --- /dev/null +++ b/deployments/helm/README.md @@ -0,0 +1,5 @@ +# Helm + +> [!WARNING] +> This chart is used for testing only. It is not intended for production use. +> It can be broken or removed at any time. \ No newline at end of file diff --git a/deployments/helm/templates/NOTES.txt b/deployments/helm/templates/NOTES.txt new file mode 100644 index 000000000..319f01bda --- /dev/null +++ b/deployments/helm/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "chart.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "chart.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "chart.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "chart.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/deployments/helm/templates/_helpers.tpl b/deployments/helm/templates/_helpers.tpl new file mode 100644 index 000000000..7ba5edc27 --- /dev/null +++ b/deployments/helm/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "chart.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "chart.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "chart.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "chart.labels" -}} +helm.sh/chart: {{ include "chart.chart" . }} +{{ include "chart.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "chart.selectorLabels" -}} +app.kubernetes.io/name: {{ include "chart.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "chart.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "chart.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/deployments/helm/templates/deployment.yaml b/deployments/helm/templates/deployment.yaml new file mode 100644 index 000000000..136cca619 --- /dev/null +++ b/deployments/helm/templates/deployment.yaml @@ -0,0 +1,84 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "chart.fullname" . }} + labels: + {{- include "chart.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "chart.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "chart.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "chart.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: POSTGRES_URI + value: {{ .Values.postgres.uri }} + - name: BIND + value: ":{{ .Values.service.port }}" + - name: DEBUG + value: "{{ .Values.debug }}" + {{- if not (eq .Values.gracePeriod "") }} + # https://freecontent.manning.com/handling-client-requests-properly-with-kubernetes/ + - name: GRACE_PERIOD + value: "{{ .Values.gracePeriod }}" + {{- end }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + livenessProbe: + httpGet: + path: /_healthcheck + port: http + readinessProbe: + httpGet: + path: /_healthcheck + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/deployments/helm/templates/hpa.yaml b/deployments/helm/templates/hpa.yaml new file mode 100644 index 000000000..a91f61bd5 --- /dev/null +++ b/deployments/helm/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "chart.fullname" . }} + labels: + {{- include "chart.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "chart.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/deployments/helm/templates/ingress.yaml b/deployments/helm/templates/ingress.yaml new file mode 100644 index 000000000..63c1311c9 --- /dev/null +++ b/deployments/helm/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "chart.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "chart.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/deployments/helm/templates/service.yaml b/deployments/helm/templates/service.yaml new file mode 100644 index 000000000..dfc5b3a33 --- /dev/null +++ b/deployments/helm/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "chart.fullname" . }} + labels: + {{- include "chart.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "chart.selectorLabels" . | nindent 4 }} diff --git a/deployments/helm/templates/serviceaccount.yaml b/deployments/helm/templates/serviceaccount.yaml new file mode 100644 index 000000000..1df935010 --- /dev/null +++ b/deployments/helm/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "chart.serviceAccountName" . }} + labels: + {{- include "chart.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/deployments/helm/templates/tests/test-connection.yaml b/deployments/helm/templates/tests/test-connection.yaml new file mode 100644 index 000000000..8dfed872d --- /dev/null +++ b/deployments/helm/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "chart.fullname" . }}-test-connection" + labels: + {{- include "chart.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "chart.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/deployments/helm/values.yaml b/deployments/helm/values.yaml new file mode 100644 index 000000000..f59bab341 --- /dev/null +++ b/deployments/helm/values.yaml @@ -0,0 +1,105 @@ +# Default values for chart. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: ghcr.io/formancehq/ledger + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 8080 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +postgres: + uri: "" + +debug: false + +gracePeriod: "5s" \ No newline at end of file diff --git a/deployments/pulumi/Earthfile b/deployments/pulumi/Earthfile new file mode 100644 index 000000000..9e19d0cb9 --- /dev/null +++ b/deployments/pulumi/Earthfile @@ -0,0 +1,11 @@ +VERSION 0.8 +PROJECT FormanceHQ/ledger + +IMPORT github.com/formancehq/earthly:tags/v0.17.1 AS core + +FROM core+base-image + +sources: + WORKDIR /src + COPY *.go go.* Pulumi.yaml . + SAVE ARTIFACT /src diff --git a/deployments/pulumi/Pulumi.yaml b/deployments/pulumi/Pulumi.yaml new file mode 100644 index 000000000..ee945ae54 --- /dev/null +++ b/deployments/pulumi/Pulumi.yaml @@ -0,0 +1,7 @@ +name: ledger +description: Ledger deployment +runtime: go +config: + pulumi:tags: + value: + pulumi:template: kubernetes-go diff --git a/deployments/pulumi/README.md b/deployments/pulumi/README.md new file mode 100644 index 000000000..9ea3aa0d4 --- /dev/null +++ b/deployments/pulumi/README.md @@ -0,0 +1,5 @@ +# Pulumi + +> [!WARNING] +> This Pulumi app is used for testing only. It is not intended for production use. +> It can be broken or removed at any time. \ No newline at end of file diff --git a/deployments/pulumi/go.mod b/deployments/pulumi/go.mod new file mode 100644 index 000000000..f7c0a198b --- /dev/null +++ b/deployments/pulumi/go.mod @@ -0,0 +1,92 @@ +module github.com/formancehq/ledger/deployments/pulumi + +go 1.22 + +require ( + github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.18.3 + github.com/pulumi/pulumi/sdk/v3 v3.137.0 +) + +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/BurntSushi/toml v1.2.1 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/ProtonMail/go-crypto v1.0.0 // indirect + github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect + github.com/agext/levenshtein v1.2.3 // indirect + github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/blang/semver v3.5.1+incompatible // indirect + github.com/charmbracelet/bubbles v0.16.1 // indirect + github.com/charmbracelet/bubbletea v0.25.0 // indirect + github.com/charmbracelet/lipgloss v0.7.1 // indirect + github.com/cheggaaa/pb v1.0.29 // indirect + github.com/cloudflare/circl v1.3.7 // indirect + github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect + github.com/cyphar/filepath-securejoin v0.2.4 // indirect + github.com/djherbis/times v1.5.0 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.5.0 // indirect + github.com/go-git/go-git/v5 v5.12.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/glog v1.2.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/hcl/v2 v2.17.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mitchellh/go-ps v1.0.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/opentracing/basictracer-go v1.1.0 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/pgavlin/fx v0.1.6 // indirect + github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pkg/term v1.1.0 // indirect + github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 // indirect + github.com/pulumi/esc v0.10.0 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect + github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/skeema/knownhosts v1.2.2 // indirect + github.com/spf13/cast v1.4.1 // indirect + github.com/spf13/cobra v1.8.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/texttheater/golang-levenshtein v1.0.1 // indirect + github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect + github.com/uber/jaeger-lib v2.4.1+incompatible // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/zclconf/go-cty v1.13.2 // indirect + go.uber.org/atomic v1.9.0 // indirect + golang.org/x/crypto v0.25.0 // indirect + golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 // indirect + golang.org/x/mod v0.18.0 // indirect + golang.org/x/net v0.27.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/term v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/tools v0.22.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240311173647-c811ad7063a7 // indirect + google.golang.org/grpc v1.63.2 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + lukechampine.com/frand v1.4.2 // indirect +) diff --git a/deployments/pulumi/go.sum b/deployments/pulumi/go.sum new file mode 100644 index 000000000..7b4d7c8ab --- /dev/null +++ b/deployments/pulumi/go.sum @@ -0,0 +1,315 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= +github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= +github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= +github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= +github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= +github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= +github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= +github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= +github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= +github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= +github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= +github.com/cheggaaa/pb v1.0.29 h1:FckUN5ngEk2LpvuG0fw1GEFx6LtyY2pWI/Z2QgCnEYo= +github.com/cheggaaa/pb v1.0.29/go.mod h1:W40334L7FMC5JKWldsTWbdGjLo0RxUKK73K+TuPxX30= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/djherbis/times v1.5.0 h1:79myA211VwPhFTqUk8xehWrsEO+zcIZj0zT8mXPVARU= +github.com/djherbis/times v1.5.0/go.mod h1:5q7FDLvbNg1L/KaBmPcWlVR9NmoKo3+ucqUA3ijQhA0= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= +github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= +github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68= +github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU= +github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/hcl/v2 v2.17.0 h1:z1XvSUyXd1HP10U4lrLg5e0JMVz6CPaJvAgxM0KNZVY= +github.com/hashicorp/hcl/v2 v2.17.0/go.mod h1:gJyW2PTShkJqQBKpAmPO3yxMxIuoXkOF2TpqXzrQyx4= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= +github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/opentracing/basictracer-go v1.1.0 h1:Oa1fTSBvAl8pa3U+IJYqrKm0NALwH9OsgwOqDv4xJW0= +github.com/opentracing/basictracer-go v1.1.0/go.mod h1:V2HZueSJEp879yv285Aap1BS69fQMD+MNP1mRs6mBQc= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/pgavlin/fx v0.1.6 h1:r9jEg69DhNoCd3Xh0+5mIbdbS3PqWrVWujkY76MFRTU= +github.com/pgavlin/fx v0.1.6/go.mod h1:KWZJ6fqBBSh8GxHYqwYCf3rYE7Gp2p0N8tJp8xv9u9M= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/term v1.1.0 h1:xIAAdCMh3QIAy+5FrE8Ad8XoDhEU4ufwbaSozViP9kk= +github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 h1:vkHw5I/plNdTr435cARxCW6q9gc0S/Yxz7Mkd38pOb0= +github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231/go.mod h1:murToZ2N9hNJzewjHBgfFdXhZKjY3z5cYC1VXk+lbFE= +github.com/pulumi/esc v0.10.0 h1:jzBKzkLVW0mePeanDRfqSQoCJ5yrkux0jIwAkUxpRKE= +github.com/pulumi/esc v0.10.0/go.mod h1:2Bfa+FWj/xl8CKqRTWbWgDX0SOD4opdQgvYSURTGK2c= +github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.18.3 h1:quqoGsLbF7lpGpGU4mi5WfVLIAo4gfvoQeYYmemx1Dg= +github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.18.3/go.mod h1:9dBA6+rtpKmyZB3k1XryUOHDOuNdoTODFKEEZZCtrz8= +github.com/pulumi/pulumi/sdk/v3 v3.137.0 h1:bxhYpOY7Z4xt+VmezEpHuhjpOekkaMqOjzxFg/1OhCw= +github.com/pulumi/pulumi/sdk/v3 v3.137.0/go.mod h1:PvKsX88co8XuwuPdzolMvew5lZV+4JmZfkeSjj7A6dI= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI= +github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= +github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 h1:TToq11gyfNlrMFZiYujSekIsPd9AmsA2Bj/iv+s4JHE= +github.com/santhosh-tekuri/jsonschema/v5 v5.0.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= +github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= +github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/texttheater/golang-levenshtein v1.0.1 h1:+cRNoVrfiwufQPhoMzB6N0Yf/Mqajr6t1lOv8GyGE2U= +github.com/texttheater/golang-levenshtein v1.0.1/go.mod h1:PYAKrbF5sAiq9wd+H82hs7gNaen0CplQ9uvm6+enD/8= +github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= +github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= +github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zclconf/go-cty v1.13.2 h1:4GvrUxe/QUDYuJKAav4EYqdM47/kZa672LwmXFmEKT0= +github.com/zclconf/go-cty v1.13.2/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 h1:LoYXNGAShUG3m/ehNk4iFctuhGX/+R1ZpfJ4/ia80JM= +golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240311173647-c811ad7063a7 h1:8EeVk1VKMD+GD/neyEHGmz7pFblqPjHoi+PGQIlLx2s= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240311173647-c811ad7063a7/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= +google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +lukechampine.com/frand v1.4.2 h1:RzFIpOvkMXuPMBb9maa4ND4wjBn71E1Jpf8BzJHMaVw= +lukechampine.com/frand v1.4.2/go.mod h1:4S/TM2ZgrKejMcKMbeLjISpJMO+/eZ1zu3vYX9dtj3s= +pgregory.net/rapid v0.6.1 h1:4eyrDxyht86tT4Ztm+kvlyNBLIk071gR+ZQdhphc9dQ= +pgregory.net/rapid v0.6.1/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= diff --git a/deployments/pulumi/main.go b/deployments/pulumi/main.go new file mode 100644 index 000000000..d615eb2bf --- /dev/null +++ b/deployments/pulumi/main.go @@ -0,0 +1,71 @@ +package main + +import ( + "errors" + "fmt" + helm "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v3" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi/config" +) + +func main() { + pulumi.Run(deployLedger) +} + +func deployLedger(ctx *pulumi.Context) error { + + conf := config.New(ctx, "") + postgresURI := conf.Require("postgres.uri") + + namespace, err := conf.Try("namespace") + if err != nil { + namespace = "default" + } + + version, err := conf.Try("version") + if err != nil { + version = "latest" + } + + timeout, err := conf.TryInt("timeout") + if err != nil { + if errors.Is(err, config.ErrMissingVar) { + timeout = 60 + } else { + return fmt.Errorf("error reading timeout: %w", err) + } + } + + debug, _ := conf.TryBool("debug") + imagePullPolicy, _ := conf.Try("image.pullPolicy") + + replicaCount, _ := conf.TryInt("replicaCount") + + rel, err := helm.NewRelease(ctx, "ledger", &helm.ReleaseArgs{ + Chart: pulumi.String("../helm"), + Namespace: pulumi.String(namespace), + CreateNamespace: pulumi.BoolPtr(true), + Timeout: pulumi.IntPtr(timeout), + Values: pulumi.Map(map[string]pulumi.Input{ + "image": pulumi.Map{ + "repository": pulumi.String("ghcr.io/formancehq/ledger"), + "tag": pulumi.String(version), + "pullPolicy": pulumi.String(imagePullPolicy), + }, + "postgres": pulumi.Map{ + "uri": pulumi.String(postgresURI), + }, + "debug": pulumi.Bool(debug), + "replicaCount": pulumi.Int(replicaCount), + }), + }) + if err != nil { + return err + } + + ctx.Export("service-name", rel.Status.Name()) + ctx.Export("service-namespace", rel.Status.Namespace()) + ctx.Export("service-port", pulumi.Int(8080)) + + return err +} diff --git a/docker-compose.yml b/docker-compose.yml index d315ca717..b6260a214 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,7 +27,7 @@ services: image: prom/prometheus:latest restart: always volumes: - - ./deployments/prometheus.yaml:/etc/prometheus/prometheus.yml + - ./deployments/docker-compose/prometheus.yaml:/etc/prometheus/prometheus.yml ports: - "9090:9090" @@ -35,7 +35,7 @@ services: image: "otel/opentelemetry-collector-contrib:0.81.0" command: [ "--config=/etc/otel-collector-config.yaml" ] volumes: - - ./deployments/otel-collector-config.yaml:/etc/otel-collector-config.yaml + - ./deployments/docker-compose/otel-collector-config.yaml:/etc/otel-collector-config.yaml jaeger: image: jaegertracing/opentelemetry-all-in-one diff --git a/docs/database/_default/diagrams/orphans/orphans.dot b/docs/database/_default/diagrams/orphans/orphans.dot index 77e45a25e..a976f6195 100644 --- a/docs/database/_default/diagrams/orphans/orphans.dot +++ b/docs/database/_default/diagrams/orphans/orphans.dot @@ -19,10 +19,13 @@ digraph "orphans" { label=< - - + + + + +
goose_db_version[table]
id
serial[10]
version_id
int8[19]
version_id
int8[19]
is_applied
bool[1]
tstamp
timestamp[29,6]
id
serial[10]
max_counter
numeric[0]
actual_counter
numeric[0]
terminated_at
timestamp[29,6]
< 0 0 >
> URL="tables/goose_db_version.html" diff --git a/docs/database/_default/diagrams/orphans/orphans.png b/docs/database/_default/diagrams/orphans/orphans.png index e0313458982a605e4fa44c043312fbe6ff68e66f..ac18e03edb640c5e3e1fc8423ce1a58a367e4f8e 100644 GIT binary patch delta 36532 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 delta 29454 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 diff --git a/docs/database/_default/diagrams/summary/relationships.real.compact.dot b/docs/database/_default/diagrams/summary/relationships.real.compact.dot index acf21c52d..68003186a 100644 --- a/docs/database/_default/diagrams/summary/relationships.real.compact.dot +++ b/docs/database/_default/diagrams/summary/relationships.real.compact.dot @@ -35,23 +35,6 @@ digraph "compactRelationshipsDiagram" { target="_top" tooltip="accounts" ]; - "transactions_metadata" [ - label=< - - - - - - - - - - -
transactions_metadata[table]
seq
ledger
transactions_seq
revision
date
metadata
...
< 1
> - URL="tables/transactions_metadata.html" - target="_top" - tooltip="transactions_metadata" - ]; "accounts_metadata" [ label=< @@ -69,6 +52,23 @@ digraph "compactRelationshipsDiagram" { target="_top" tooltip="accounts_metadata" ]; + "transactions_metadata" [ + label=< +
+ + + + + + + + + +
transactions_metadata[table]
seq
ledger
transactions_seq
revision
date
metadata
...
< 1
> + URL="tables/transactions_metadata.html" + target="_top" + tooltip="transactions_metadata" + ]; "transactions" [ label=< diff --git a/docs/database/_default/diagrams/summary/relationships.real.large.dot b/docs/database/_default/diagrams/summary/relationships.real.large.dot index 8a11f9ae6..b59384fe6 100644 --- a/docs/database/_default/diagrams/summary/relationships.real.large.dot +++ b/docs/database/_default/diagrams/summary/relationships.real.large.dot @@ -42,23 +42,6 @@ digraph "largeRelationshipsDiagram" { target="_top" tooltip="accounts" ]; - "transactions_metadata" [ - label=< -
- - - - - - - - - -
transactions_metadata[table]
seq
ledger
transactions_seq
revision
date
metadata
transactions_id
< 1
> - URL="tables/transactions_metadata.html" - target="_top" - tooltip="transactions_metadata" - ]; "accounts_metadata" [ label=< @@ -76,6 +59,23 @@ digraph "largeRelationshipsDiagram" { target="_top" tooltip="accounts_metadata" ]; + "transactions_metadata" [ + label=< +
+ + + + + + + + + +
transactions_metadata[table]
seq
ledger
transactions_seq
revision
date
metadata
transactions_id
< 1
> + URL="tables/transactions_metadata.html" + target="_top" + tooltip="transactions_metadata" + ]; "transactions" [ label=< diff --git a/docs/database/_default/diagrams/tables/goose_db_version.1degree.dot b/docs/database/_default/diagrams/tables/goose_db_version.1degree.dot index 400a0af51..f885b618e 100644 --- a/docs/database/_default/diagrams/tables/goose_db_version.1degree.dot +++ b/docs/database/_default/diagrams/tables/goose_db_version.1degree.dot @@ -4,10 +4,13 @@ digraph "oneDegreeRelationshipsDiagram" { label=<
- - + + + + +
goose_db_version[table]
id
serial[10]
version_id
int8[19]
version_id
int8[19]
is_applied
bool[1]
tstamp
timestamp[29,6]
id
serial[10]
max_counter
numeric[0]
actual_counter
numeric[0]
terminated_at
timestamp[29,6]
< 0 0 >
> URL="goose_db_version.html" diff --git a/docs/database/_default/diagrams/tables/goose_db_version.1degree.png b/docs/database/_default/diagrams/tables/goose_db_version.1degree.png index a0b0c9ea05eec73bd9353ea307055718af17f732..262a6a407beafc5404971ebf7c655ce0a3462999 100644 GIT binary patch literal 15800 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 G-1}eKG1!*? literal 11668 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 diff --git a/docs/database/_default/diagrams/tables/transactions.1degree.dot b/docs/database/_default/diagrams/tables/transactions.1degree.dot index 3b53f2715..5fbecf18b 100644 --- a/docs/database/_default/diagrams/tables/transactions.1degree.dot +++ b/docs/database/_default/diagrams/tables/transactions.1degree.dot @@ -27,7 +27,7 @@ digraph "oneDegreeRelationshipsDiagram" {
transactions[table]
seq
bigserial[19]
ledger
varchar[2147483647] -
id
int8[19] +
id
numeric[0]
timestamp
timestamp[29,6]
reference
varchar[2147483647]
reverted_at
timestamp[29,6] diff --git a/docs/database/_default/diagrams/tables/transactions.1degree.png b/docs/database/_default/diagrams/tables/transactions.1degree.png index b580165c12df5ab78381f25b113c93fb75f379c4..5dbb51b85d0f45f8955c3af863b498cc05def26d 100644 GIT binary patch delta 54182 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 F|1VgX-3=2>E^7JJ^ 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 F`8SvU;0gc$ diff --git a/docs/database/_default/diagrams/tables/transactions.2degrees.dot b/docs/database/_default/diagrams/tables/transactions.2degrees.dot index ed85af516..1303d9cd3 100644 --- a/docs/database/_default/diagrams/tables/transactions.2degrees.dot +++ b/docs/database/_default/diagrams/tables/transactions.2degrees.dot @@ -39,7 +39,7 @@ digraph "twoDegreesRelationshipsDiagram" {
transactions[table]
seq
bigserial[19]
ledger
varchar[2147483647] -
id
int8[19] +
id
numeric[0]
timestamp
timestamp[29,6]
reference
varchar[2147483647]
reverted_at
timestamp[29,6] diff --git a/docs/database/_default/diagrams/tables/transactions.2degrees.png b/docs/database/_default/diagrams/tables/transactions.2degrees.png index 1ca4b07ceaa1b415dfb8157d2f72ce1ce10183e0..010febb8c663252a647aa66a8b8efb85d2a6a0cc 100644 GIT binary patch literal 76011 zcma&O2{_g5+BdwUSWzrelzFJ6WC+O|LNcVukeNyuOG)NgQY0aBs3dbFGLs}SM25^s zgitbM{C>;*?C0Cxv!CyH-*q4R?%u4{`VZH2p1cQ!Ha3oi8H2{t{yeY-xBbaAqt1H*RO0R#SI z{-hIAd{cq94j+zUqBYsLB69Xr_Lz zL;jQN4wowVS6=)+Y@5hGNG4umBL8`t=l}SItLvY|+xmBAq_?-%pM4XdWMphCCNA!P zSHSwG%1Pb&{Uk?;;1y+VZoaX7$2Wcgp+fcG!Gl$F6a?cp&cBi?zWDsY4+#X)vHL0H zKNXmchLEorL1*I;^3}8xG%nFQdv#<05!?EBU{(*tC()Wc%*+)Lc9(Hte)Xg&7SMr!j zg!Ex)y;xji;>APPytHfi)pwrv! z=#=DfKE??3?dEEf;t#V8)Yb3wYr3^)%dea{eR>-sqYvrL&8fbCt*i?zH1$bJfmg0v zDX{6P>Nr~Bz1LleCycQ7?JzLi4!#br%X)BuY49bd-k(0H9e1C zg3PU#*MFSi<>ftg>=-X^is1PIF>!IT2t5M>fqnZ{R_6P)w6s2a_^`ORD8IU3p{vVW z@aNOh6Q`ePB%S2nwQJX*iw%;la|cGgNJ&fk`ub{WYSvl4%*x72Pq)5ta&8O4_I)xJE?f|Z zcwgX7#J3e!0gAe%xQ~DUaX0d-u-IZ{x0e?R|Z6h0}{| z?d@|@o@>RAA1fR`9;B5@O+}>@Ble>!@6wAGFNTMQ!@|Ot4;m^EZ98+g#l-US@>*+Z zj%jGv^q0A_9=4lUze3Kgt|sLkr70;&Dk^&p95^5^pSbwbk(-Z~*KwjV zH%iC^x5#vcmXRaME)o;@on>gwy`;1=^-UH<<4;>0u7&6|VL zb+d6BqN1X7&z`;d{O8Z^*OF{>+}9VT2fu&+j_cKtZQ>YmNJ=W^)j4c@Y`CLGz3<-L zqBy$nYsi-_n>vGW|NithZ{Ebj?7|uN-A_s~!`f0(@=Z(Ir)agZIE!1R_qsBeSty4F zTT}6QNl8hQ=IE5G^X%C6xxCx9RjsMoxqZR-x8rL0Kv|IY%a<={Hg7g_H+*SX=eat= z*(|lIQ6C>4TpFWdr?eL@%x5l(#rXLtYiMX(zKm5cd3WcbiyvcZ4D#KVoNS~- zMXxnH67gJ_)xCA=Zy2xYlc!G;Q6_%w=*Ww@dF@)&JB>%t(cFGHCj__c)k2+`8Xa}G zeqHR)A!+x;{3lQN0>Av8nTghT_UIAI9F?+&h={H2PoaRAsHniq;cwskNS~UUa|#Mn zh_ddB*Bu6DYbz@$iV0?>re36%uV3#CQ*)N_z(Es}0s;a`N@S>fy-B;|J!FEEPn|wp zeee&{jvaIQW}f{@IywxBYKDevuI}DgpSiiem^*fz+*Wcd>EyXggQwUle2nEwlf|ee zcka;GwEX$eDl03yef##bGh^PA)Sp^gX=!L&GFE4rGyoU z=g6)5N*kxf$yX&QEv+ubV{Nt6*H_8fdc4Y?_UFU|DukkvlIug=lP4?obW)SlPMpAI zSZatqq#D9@3)?a=(F9v5G*sL3)p0d7(`|a?#3qFp05Z{DQmDM|EPEh-Pl zsW;A0x4dw{wJm5jKUS8s(}bx>m}1hOte?UC0(5~ETfdu%SAJaEG_N_WuA`%K{raEk zKzdY%+%^A#0tp)iSEr|`sVFTtq#Qo_(|8#hul{`9|5BIVCnYI~6CH5l))Tanp($>Q ze=b{Fr$2e}E!Uz}Mn)!QoLTly^w~4Ix~~fgvJ8tv4;^ysF0dsg{rPj9?$wX+ab7=` zEh^WpiAwC1m0bzG8p3*5%g`{c($tjlNPT@hR!@t((H_dJq1E%#H4SxbZA^jHA}m{I zX@faebFU4nZ{2#YFScUPwYjxbiFm`w$y_%DRd@RP$5^2(@yC0dpJR{AO?2@w?h_RJ zIb0tVSFS|m+cIQrV`qoe;XM7=`y+bI0>juz1$lCp*)?}*5{;&zp+R8({@~zX*Qm8Ok59wuo{RU=DVS6A%!vZqGP!ee`~6>ZOw>N%30tSFVVPi7kJ1O<39zTCX`C{_NTJoBi%L z|BUMB>ULaiea1laOHSswabq?=Kff>5;qUL^)iP=$qh9^Ii?|O$#wDH2&C|F;dh}Zt zKaua+76y*cOE*r-y3S?1e0i-YK~^u|4^~)Sl-u9myR8?|y)!d2C*t{u+-PT_(QYG+ z>^6F#?|nP~SxEU$Pei>bF2>dNT>Mjqjni~qwl;*Fb;pipSy`*!>cX)}#yhgH92`#< zB_#CuZQj;ba?5dc>@1aUuFLG$?c2o0S2Z;?wmpS9+S=Lq`AppT<~1~SUvZacw{HF1 z+Ukmxou5zcvTMsTO+s>~pEOMub{(YS;pJ6QQktEeJ%pF2D0`#M71XiaGzEKYu3s%T0`p`q9 z$<4{0I%6!enfGD5uU@@ENksp6(qg42#6Zgt+K(f5aG3tlnkw(Uh>m-Gb@^|f#W_E^ zj-gXLsQC$!o~trW*?~EfJE`<2_v!KN-c2ngcTqV;&izeZ-XSMWamD9lWgNvS&z?VD zz$#le1sq=bz#N8ZWpITa4+S>TI*IzwehVi47eSBLgZnuoYdM#{x z_rC()fBW=xD0^Sz(Z_hB8sZIFX%a!1mV9Vj8sf8Nn>Zidk&j`D94jbT+dbhxAaSq1 zy;7xa3l{$E%xa_Ys`6j z?zYb@e|N-#H$8!n<&U zyLTVsV^qDj>lHRNI;~$bFdoqL$B!S;YJtclu6}*%IMT@NN5>(9ZxFln+fp-1y0${>F*!}-=N_vM9_ znwkr!Tal5G*!DO&Vp3dO+(C)Wn>T~TtxN^S?B?Th=Ab9^tgj}&&4Ik;+g|c9%ZYRE z-#`BIXM9vtM@viC!-oJcPai*aSepCEQPbAehQ667>-sKc=J)UkIsc7>!=?=f&?wkB zk|qo0YB!<+qba zrk5@SlD-t!#@@ZFp{MsKDvCKw4Y)`i3`3D$SU4v)_bRrfprByuGtKTI$4jQBTumt( zyiQ0?Y~aIQ?340bEh#RRK0MOtbL!sY+;q&nW_vN8p z{2z>Z*Yqon${xn81eTrouU^^O*pT05s<+D_JZ zG)14wR(J!tt>W{dqUh900zrk2F^F%19*JQe`R1ENH;Q!O=Jqb1VI3++q7Z012a_Yfq@fTx0!(41*M5F^b)i0z+Yco`jqkym zH&Tl4KYeg~C< z@;W>`9K-<}H&lML^Ydqa!BSAEhx<^qURM6PiI51#}G9((Ek)A$2 zHf8_}V-m;5_yeuE(;z7+sng($zzwWe5NlsLdVpZcNcdjuE7sOWI=&ALIeu^0$wrr&oo!-d6rY%w{rq{SN!$T|BKaF*OdO#d zt*!g_??0iY<_s#=TYM8GlbV5pu)GJIe)n#FO_Mj*gZ^Rdx-{MjTqGjsheJ9gLtyow zANvLZ@mC4wh2A$e4qa+gCThlr?L(F2=JtN|ys&VgGuJ}Mr0lwrlL}EYL8j>OW85O@ zk-unMX za_#ps{n7sgwNCIV*f_1~>|B_YbudWzWkEsX`}Y+Wot&H?#=Lm`oD`(&>bj(zD3|*I zQbLSM%8$`eu%00YNi>vi-`*w(sH)bYm6p2B1D&7O*Wc_sUP0t7TylXX(bLC@2VI##5vX9MMK_;$++ZhgDK=3FAi5LbWKc*%`0Z z)YPO(3{!iOb~+Ueavql#V%YgIwchP0xp%n3Z=LEdzvafy!)IAdSp0YO1`G(Dw2y6D(gW+tYEzkhA}%i@)685tQn-+H|{xEb8C zW#||yL`z>{=#mq~05wz;NN-!WZsiGE=)Kv07RUyYNboc5#K|8&(w;w8dwL3D(tWT7 z>9vKwBZ(ltwV|9sXt;xeW|uGjU79MVb>k)2PxKZa78U)}(_^TmW!Lrc0$9Jy%}F-J zQ7rePM~`AF?g_6$J;|UO09=Bsf%1-J0Zj!LPR3*Tk>lA%Z{I2(I+OidkqZ3s zXLgoBaSIjiks~`}!xIu#Yb0y$vF<&1u+daFCs-OnRaaNnXyUeA6V09OA0E$UDDX(WZBgrs`j)Vle3)$Q1k+F z)xGvQ%Us;C;@Dp7^fM+pS|8{VgknmRii(PuG&ko2u#YA*XU!jn-(>3LR^b9GDGk+y z?_JkU>0V)ZgJPMKIA5I|`wRjBq7B`->|$>bW*)KC8g%=dreOQ(C(C`Ez~V}fz@EmOoSf$7ld`g(W8~95IO8=Wf;dIB z9a_GSyQQUTS&q;&+I1I}Yfb1e?A( zq_XSYv((gDV?t;xPUeWDq~6h^W@z2Ewzfx(9Dy9kCS~FEzG~*Y9D`k>fYVEIJZvw8*bAB-U$gDzLlosr{k5*R&|OBn^!Km5Lhl_ z!#+Rr0@oxWVta)8%5aoPfqj1#TG#k^vFDn{-1%-gPPw>y;q~zt{A)|^5A0Bc<%d!|3*mmy#r-42JMdkA4ot&JZMqj&7bK{OUes7FVfA*}v zb8QveqFq32*Ut_U&)p=VB%2n*g1WlRtd6}VX9?4!tlG@|CdL$jO2n?tP8qiaQZe6V z0x1i95LeA@ZDk%+Z-RI8LMq4zi2s2D8MThSoo1oOuRA&}U9nfdtp%W7QfSKM8Gd(< zHCDn_>e{#4-rf}9bs1S%efgKe%gQ_we;im)hp5l(r*|`Xdi$Rgf-vJoDymcb4{^(h z&x&xjPxC(n(yA1r_S;mfe{HNS4J96o2fH7Fl7j^^iKQ>DKyCiJTJnpwP>0KT z&z?QlGw@vCSj5N2qe0)fcMo-VyOZI7^*W*OD%-SiBQ|JfXD8Hc_2b8*AE89PEqwEa zM*86ND=#voR_KSmk&>L86ZM#a%UPHMKl;CMvaIgU&w?2}_Dta2Odvddosn_iPx;g~ zDpSh}G5TDX*8x#DtF4T5(%!3f-gl;vtpTfUyAGhC*gNFPQ3WR~(l_hD^ltcf6Ec}Q4 zw)&l4jg+FfkFD&?Arp-9G9_ z^+#!EJ@0C6|1}}oTQD|r(4$~e*%csw=;-B-zSLj*aCzwf*c>KaYom=ODsbg=wD$F_ zR@2Mh-6gKBrPWwh_XF6Em=v*J*L?hwl^BQJ2GSjz9v1Uzq-b;l*eHY!32|}IW+rB4 zT2YA>ZZ9R4Evjw}I#*beyT5Rre#j$!wyB=6JTtAoyG8$f`S*&B3GB1ZnU~5hf|{F~ z(fgpb`JHrk$C`A6g9dlNHF_(nL=S}FuQkCaGhhS2kua4Y30v9P_7>Q(3LPd8&p}ne z4k0O`gg~uY*}h*;(ALUISXh`v+9~7C9juSzCr;4X^VYq8KRq}1-8NpSgI9opt;$q# z*S8s+!j>MFZQN~bzf&K+^(zQrCMLDdNi%MAUwuVXkrBx)%NxU&P8a_;D#ra^m^bz5 z(|rO03DP&Uz+TQiE-Ly9&<_b528b#V&YUb%+P1XQkVd)5csN0~(w7_crPyWm3RX36 z-XjqUT(NJ%!%O=$Qd=z`TU%HFfhbyic({-4N>*lO&1yt=xV4Q9Y=h6AA+2ywc@9=> z;%I-jUpASsf_qB(eICbh1pdh{|Gb6)AOy|s*e5)27aCDw9 zD>JjJBaQJwDnM->(0u_;0Eych00yai&8}Se5TlgRQkb@t_$fs-6g`(&$k=VL%3pqM z8M^Cd)OvJ@DI-xKA$yQP&=+(C)D0jdlMJ>KWGRzr0@RPGo0ihkS?P1|t|3d88rQ*C<$dg-vtibG4=D{LgOHr>7v8VEt;vNw!=% zlQ9%rXSw>jUP$jRAXcEiKP=)(|F*pgNjp^*9;Lhwk9_I*c96(-S&O;jA61HnhX)ud zQRddy!9nkzG`YFC5H+zHp0@sg>jM?md1+2iksmMcC2{dr85s@r_11L{b}x7D;NpSi zh_@RX8ZN`A#2G@npFfxf{tujhw%yTDQP-gYC1E`a&{14m+!QZ8{~=;OWpDAnq2H0F z#Hp5zL1g*X1vCSalMeu+puyXfJvog9L~g($zmvy~ZL-!ndv^PhD%bVvQt>J&_t>Qi za5o{M?%kVOQnK7_Uv89RdeqMD;JE-?1mIXSLl{QOz;o?gT}Pn3p#*N)v_(3s1C!_(7gi=|roTiQ%yA>%SI zFbob3qQ7Eo#LC>dmZ+19l0DI1E-xVwsg=rk>oQ7(pmGmEltJ%101X)>G z?bxPkxr}8E32u_qm75J9xQ4p= zr;d(GbZ%4E92|tj#ea^CncK5lCsQAI`ndIn><7K`=cl?~XWzWV81$Co9rkx`PY)JB zcXu~EyVR5C&#fS13DOXWIj>)jOm7d8oG^s-xo1Kl;B9}uiK;4!hQIs8n>Xh{&%moX zQD1TG(YVNpYYl^Oc6Q%Zf zL~b8t2{ELVZZ59`+k_lY&SO!V-8-#MKt%ym{jSf6CXl8i!a~QW6&*59w27$()S+-pOM0N$gW4y!)60%-`ZkFxnAoTfe2-u?MzIj_IA~vwV|;2e-GDt zczA$N9k%Ns8tniUKynJs1VvU9d{OEb_P^lMNbSa)hPSSFX6sPsT$OQcxP(>R}bYj7IF7yEc{oFlzSe zvl6)8fq|;#=3hhDrJ->_yui}9OZre(w-e5*Yu)eP*Z!0X$<`V>5>+Jjbo(>r`zQG!7X#wruxRl$ANm^Sk^s2C)~aJDYS#F!{WmL zMc_Rdp=e5wPB^aH@{D5FVI}WyV)9MJdR+Pn++Y7WT^E!Fug;M}(G4i6d0AH-{=T!Y zu@Y2NQ&O^c|LdB+d;j+m`Ea!-a@M2nVuQaHRS0}wx*w))PTg$lsXsw!mUQhM{$4zInNJVw`c$UrHQUX?QsYY{N}sJ z^#NA4ek)q{6H_HX_Ro(MoB14OSkUSmj>sd9b#$Szfa20^^$$U{Q+gR zu@NryNDn<$R3kp^H<<}?aJxRaMJ#~0B&t95R^d+#3nuU3l`^flmV zm}u~Kg!b*LTMUm^;5wPs_TT}djnTn!p7!1=Zi_3`L%*;xR|MJrto969jb(Gt_$NPp zd_O+^x@&bz6cX7ks)zo~3 z>C41KhU9c~be)rF%c7jYU)R*&EF0}4|thZZ4Bo}9gj*UX^3rPBetT$!$>haay7=QKOJ+;goA zuD*f-A?mesZs1$@;|oa)MwBb*8_oyUi{xqSnfdAZJN;&1W0tnb0sWIFO>Jy6_!x04 z;9zuYk`)Hmrw5M0o7v#aB_src^SX_)C4e6wB>E2mL~v!ut}4q1u?K&ST!j9VcOvQ_ zP!w;N8crpHB4C1Y@}SY1w_~yRq2u-I2ZV$;NBcgz`W;w`gDwQd0aU5+NO*8{WeJyk zs;|_7?n;A=qT&W`h5KP)(P&w)@?Gl~&E(lHj6^0SCdnj2koTuT>bm^xO0L3ob_aHG0HsP;ZW@!D3rGv zH*X%6@O;cYOC>L+wD-M8o?DX+&50W(rPbmEhOEvul;JTkG{uc-pQKd> z-(g{ZS55U6tERT1r9x?iIrn3DxF(c%B(IRFY1mMHYHQ2)SaE3|T0k$0I-vh)WUJ&Bsv+DQtXv{Dv@CMUu85wz zd(qbbYD+@cr&*Spg*U~fOs^P(K6pWMnTsw@8Uh+lchu4D#9DQ2Zn~A-&dOoBS3|F%{Ot|1#voR zIyR_2@0*&C0k~{oF@(#uR6;==Sp5;Osxe*~h4qG`qcr^KC{0sdgMcA-ZJu=8(-+Lx zw%q4v<;6C73**`4NQ@X78ExCP&7WP1len=8Bmn-1KDz+hquAI)sG@uJ zP;tXPS*_uem}pO6aOy5{IIgZ%u@6gLKY~~NNDYFQdT;)C!9eMjmQ1Q4e=4` z#P{#tW7QQQcXl#~LRe;6R#iub7!|b=%4fEvoHa%sYWm-qzr8f1tN1uDOW)Bqgk(Zs z^$OAse2jJ7-H@tpS;`2goI6)ge(7h<$+R=bt*j<2Qz%m}HxpU<9hum)6d#x_4qrp~ zlB8(Wk!1v(>_p^&A_y69IQ-Z{xNZjp9fe0^@5V{C+bOrRv)kf6ef?VH@=6@82s!qt z@Qph~6fka2PvU#pRRFRXD1A(bE z{z*kx__F0%!-2U1ZD>FV@NGR&c)2=j%Ur-ZX0Qu97X||nbJOh1-O`C(#xPNB`W7or zL|B-labD?7Hnl5{9ASAHG!%lenJR_fU1c7GBo7*wI3wwZ#6uO`CE)>?%_@(u1ZXlc zkACL|86_s+fY-kc)}6|+V-b;&Fz|%cG7d}AwIB$Ul9IBryle-rBb;W_yXIz6Mc0kX4s5g zUboMMY^ZRsw6O4nG$7gYOf#0GsGT4aE$=B8tpUJ1H#dheH{p-2tj$G&r|sGSLo5Y2 zxAZJ<-S&=P$hMxt42tj~0k=W$BM~&q&qruhN6BEQk;vPKk>3!4R4ZlJ! z-UrucYPAC;x%}_<*wqgZZu zN^IhoI}EC|)%kLW&0%qIJ&3;D0h{0Myk&2hPamBy@5!|mu zjw6dDQn$c-U%U_i0h;)$rp$A-?YU0IQxp<)ZEeqAUyqTmye9}9PzPBcCK(ji=$|>$ z2BqX&wsCw|Sf#;!&{#RS)$H~jxfAi+++MRVJp)DE32x*JHg^ZYpKwytI9_m(fi!?y*#fHxKX;K3ZwOGbtb2+lwzC8fEY9S0+0)gri27E%fj z1rRGjRvWz;8VGW65Ti{uxvY&qaRNcoNjrl8grHz192L{u@VjtcnaJSo2?yh!ne3Ie zk((EvuShtIyx<|0EvSWl_aSS7;$xK;gY0Ddh9;fpu?SN#xAyanNNDs8aYtG^J0B_C zLiSou@3tl!RqWb14;ojfCf3&bqcv2ByZ7vYxRMEpA z%R)RjV32?N?6X==J3tI&Wo2h)XEU=weBvJMguB~=Lqa65$Cu`8A!0#Odj0yaMB-A+ zkp=c$i%(64(Ad} z8HEd3LsR*7!YH$&SI(=Pkaq8^rDH%mWMKXbpVZpgsvnF(i>(gPsLt|t(6+sLGpN`6 zOf=zj5Fw8eOK;6k#U>(ElbzjEBtT6~efo4Oq^<;eB8$|--utt;Z{8RvDt^U+>r{o> zg7P}6_cS?q12_DpX!n&7*ur4!fAnf9E6o7=GD5M?Q4dOe2MzOHy_&^!a>+hxVDRGE zGbeOmj+x_ppCEbYb7CnXnk#K1#m{er6ol*t)nms1NQ7DL3Ppy7hF)S%f@_rV7iwka zG6*;G_U+)<8I-Ufmw|nN%WyE>pSg35-k9Z}n1#v7p)LYRkr7j$Mj>jXR%9q4ekD@#tlPQT4Q4Zj|!CYIiDimt(6MJBzTP%ekKGsZ1AkCkW$}X5TV%n zk&zs-E`Lyh7!?0oH~4;XdqF$O?Ix#v>|6XFJoxmw-y?V>9^#I%&OJI^EfmNPFOVTLn2Hh<1!-L@7}4{#MH+up-n-9#CAAR-u)Hz zvGD59JMf>y{ms3-K)0pvkR})z1An9RLU#lZ0Ly~I%Oieq(end4_jchzP|q7g$2l~7a#v3Cct9~3aUd`&F|Bqa7|h5+)ed~DUt;O zv7y1i&Px;lYdOr40t@uK@qj9h{c2r}I+9*?B)%N=#~^@qj8rY$Tg)p(MR&^U1_ zLx*K%od`AkQ{J%zB~!1pd~MYg*(u~$ke0&Shf~7OYyq3?IFO@u%q%q8+D$Ofks*d? zZP!CS-FxZT*@-~c$XuzYsGu5zvT}8;Iquv-HFn8QJV<#=Hhd{&3EAdLcv)N5dw7>? z<6YS%Ng^Cj-NiA&0pne=Znlx{%1&sIY z+c(4m8)IhBgxdcA0fRRnLyf?J!&uusNcD7Qz0GR-`VthldP;5`5)oloq9qHX2u<O`Nk%k3>11c0%+d5B5W^c6*2B#}CIp*4Cls-z_jPrD(K*WX5)_k0$Hs8}aQ30kL4=T$kZ?j;10tdeY**;` z&z@a0GBN^s0$|F~3PaG)#o75Tehohr`X;W#%2230)vnN!diwhN_U}(kP0cfrCK!Ho zP@izWJ&R7XAJ?y-AX5G5%O>V++m;3cIh`gtlQJ`HFIL+86?8sy#sLE%T!EhK>(1Gl{J|3AUqsq=@*#M z{7mYeSXv{+Q)P$5#IE1GiAe_qBES)*Y!E&XG%4$8Yx{!)2#C-Rdm~TJHAG}NIXR`` zwUSOkJ(B#!PrjN?P7-qpVQp<^+8Y37=_ilI;hG{%df_RN4cLr4049>%K+Ccp78V!P)zmVKiVL1R zQAY5mzJ5HA(}VPOk9XLW$G$eXuNJPGG@Udo_vl5OA4bu2sZ!AN!-sEvZ-~Y&t_q>u zaw>GmMLGJ_s~Zq4uxgxk4yS(tN9gPvothHLS#lvKkkAd`053m}(^dXf#zAnVogCkJ z_n)YXM!XclgPQnm*tHnI!bQNRx^3UEVFO~VP~-gBZ$Hk|&jT@yT-nLuBZ8nC)R(w8 zHj3A@1k#hR@RUdYkoYFq|0eNdb==9&^qcF#GL+=|_`|+cuscoe*6QM#_0__=GYBTwQn5>`tI}Ek~A(Er9UP9|M z)&gfM>_d*{vb=+32(4-tD=Rsl>oxz+?>l7!U@fg*8Q1DPs>d}o&tme@3EAD2WaI-6 z9$cOKdESg#)TW|{X$x<1L<--YJ!t3TyHGX|EdAnN4tVD_(MKBJF}V2z*HKWAJYzK> z_mFIRU%>RE*H@09b?FU>s~|F;o=yOt1`fvoG2+YIC=N>_1z=@uZH;)2BEOUrVlmaA z>Vg6SFQE$Na}Du#Y7-<#e4I3;BUyW31v z#Kyt#V5!5P4}phbAXqt_v!98$?@Ztp*4}g=XRvw{zVLL zsHjk-2mE`sF=?$=CtBk>04}04P%fcm0$scKX{NTGP*=a$7*`i_$nm?zxna{$*m~Tm zgI?7|`Eb};l8+((qs-G%RrM9Z9g!PV9brUZf;cehv@;o&iGfterWzcf!7%1yh(o9i zfU5{0V%~4FG!K^>kcotdNFBsLP-Fn5sNNP7cHo<+p2D!mNF=HXQEd?ugd{M6 z;sr{hmy4_ka$2d+o_+f92Qmv{ML8T7I#Y`lHoLyXyBGrx&t3y~4G_2b&cgW~%fIbk3##mMv6^pSaNYjQpCRN$a}{W3U3+6WcioN*~#u zDJ3Or*BO!XOab~7F%n+Diboo>=98*(MoGdKQA3qj`84YnCXOCGew_n;b|2ryFjmD5V zf^o)#b?d(=-IX(FW z1n9(MDk%IFz1g11uf(4K+d*yz`bwmbi5>jm>48cZ&Zg`KFIAokCCd#zQTH=rj}Gash6J971_@>Jj_pGxlV zx@nff1QibQSo0QFOVOgNEUiv}3+F8uhN;NeDwh#osr5yAx?F?9S#W z<9V8jWk5w4K~R4hi$*`O(;}wfzWNv z5Nv7C0Y+em+go{i+s>Wn;)0I~sG?I$v$^yO9k{i9DF(`MT`*Y2f!~t<+GbHz1hx1W zkGEX|n%TW^7 zgWpawk}pV@g1Y((Ofs%tu=NvG$b_$xf0K6{3s02Y{5((fcO& zy>9O^D-Qf`5c0GAKXrD!Togf0j|DT7dZ-2Hw%C>9c?H0<7`A6!HzSePMd*r*R%Q_o zskDtC05Vm)=QTK@uA&kE!EStr%%B3fHfJ2;`k=Br$?$)nnCF{S2f!17w*3nL07ET+ z@VKV3W0f6rS z_e)2CYffT6it)}Z0fmLmSFeirRw8qBK`^<+DnnQgZ5WEq5<^_bpUsOpi-+E(_%2Z< zp-^>n=pH-fug3SKyBlt1&{^BWbO)0|3oatm#Pjv<-d!Y(d$b@P(Vn4Sy*N5BkmEXU z*Dl5pDvzf|Kv}^R?eBy;qGG2#LeP-$EU*A=Na zAYTklg-1j{gbluZ+fm?9}9^>PP>+d45R zHrvwQ-`~}>Gxpp+aWcV|>v1w|WGLwZk>i#d+0kX@IRNZO@v1My(%6_oaTMFm&_;HF zlk%fZz|v%kNyw>xX|=Z|hW;}j);Scob7|=Fe~Pf3pBUm&&^It)LGw)TV31ab=))A> zx>;{H8IXu74Unhnxb&d1j3!E)GK}Xv+cCeaS+}sDpv~3>P##@Pn8z)YlqeOSzxf~P z>KP*=Sxk38U54R0CDgj>J@USo)L&Kh-vU7F@wSN};EkvWIuQ^e$ z+$vVZBpY5(chuB~*3SY0RMgc!ATEg`L&JsN2y)%kf#+dtV`fGg>EcI7vDpBWLJ16% z?;5TNW49T3Qg4W4%bRNGOuuv_150PXn#?LVLZ>DtIn=iTT7aM;@;vF721)Vt>zg=M zRaNTtt@VB8D9va@`0BZWQ|;UKQPGT7)XE@IpP6}YC3}akmC5Hx9TO9KV1Ek>!~v?4 zUO-I*4Va6@?A+J4L70cI{0N59(b2(am|so&#;^2taCG3i!pYO85pQr^T|Q!+iA0y9 zv44F`Wrs%oHKu>Xa*Rz( zZ~HH84-bbC#{)~6wI39sV1Q_1CK1*-BuvaAlm(a&DHdFuLecnOR_~teVPZ7F%-i%d7FR&*_+7hJ^#g&In_0 z1Ig?v0|rf`tCCWMGNgZ?Xd`}yXQd!6nY(BznNbz0wB;@z2s<_i3k%EW_;{Yld$#Ac z-34Hwi0WbtOy9r&(*r{eg$Q=ydSb+tmp2cwIg|_pI5-3W#{N;=!s@=E{c*@RlcX;M?CNGO@Qmh*^Fey zOuZMch}%U@Fyx=wuB-fT_LTWBLq5N*ZR@n_NrlX0t_|viL~p<4)kjlWdECm3-iFTd zo|+)iGopw5qVTnRu;I+dhbKhY%FPwh`K8|P=(c{8Fy68O@DRepix5TDKb~2twC{^# z;JTwdbO5lBh)kIUh%PrYXsm|7kO3?O!~L>o*aVSL9?UZgBgk#viZ}=XJ7hXiy56p> zI8i;8KN3Y9T!WT?_ugsOH#ROq19ZEctN~k)iDtK)oN>zs@lD)yJx4+oal4}1}l>2m~l)sbEY1AO3*O%$YT4(7h*fF1}X z`41k%%sHN*H8V3aJ~5$8{PlMkIrNcl-%LX%=jU-4h$ETRmz9-;u5}=|1nEuou$a~( zmAKHxWg`)*v8TWIW>afxc=aF@807cPiRMYT3wbX=Nn|Z^&&Nt|HE? z$Z%2t;SFR3>T%$BzLy`#py=8K_#fcdh|j@G@R|ZshhORhogZ~hB36(w2#Ghpr+ZGd zCeev+A0<3E02#>~>v z@_ichmTj0UN1Ytv+_Zu0c>2>abI7?B;!zn$@#A4QfWN5F847Es<(qF!pl-jmiwB5_ zP>+zA^3B%2^R4FsFm-k|{Ze-khVCJ^pw}b9pJs&T;jGPem;lnL_u5wO-n-ZHcimoa zfr!(6xSs_DJI;d);3;Ba2)@ukc-BV?UL#&7z5vh~Swuw6* zNEEOg?0x;=9{=NqNh@gzu$V>4n_5>uI`MpOx=v!Ld-Cp)l&jR8~FU2dRp2$jKv^I zgn?D4K6@Q^y!>Y-$0jE@?zijwq^5?3jPtZ9x;El6C8ebZ>|5?PJb0L&|2>B4>g&^y z|EV3qlEByp=oNr8e4h-7V?#RI6q87kvxLXCZ)vLLs`}beM$7Dnr>4mH+N6`WvvV=V z-qchp?{e$bPxSNyJ75`qz~n+^4V6h!dAYoAWkrXhBpW`{<`#A>n(?V5x-;}F0-cEp z%NCA~c?k)|fY5VS5#fgN8u{gEZY~2iX3e=GZ;V|?Q1$fml$V!Z<_)aI0}5As!%XrJ z&_P#Tn!>d}cI(Du&yloQJXHas0yXl8&}AAG?>HY49DF|CD(nn7e-0*x-TpdK`^1TA zPbEw@{hEc}-s!^0nFz{vr{*hgAFm%M9a63URAjh!`SLA{*Y5tx5L|O$N3d4viS5+} zTj=N}JuL&OF_a05?0vqt8n#OHFWJprSCHey>z>B>D;?rvLqond<)E~{k5^k2x=k8e zdvokBE(Q!mkK5T8MzpiGRw6Gb`G}d+|&D;bIe z72Ubn*(48pRaTs!6_Suy`gwaqM1CQsfh-@AtyGCk^MC#r=9(YFbN(>!i^7J7olO6J zu)f&euW$Hgm|VwGfv_g)M8rg)jP#Xznv5Oi+~7@)&&hst#mJjnVmS?6=S%($bE%IJX6=^o;8l z*hFK51KtkbrJV$?)5X8|AE37(_68&)!dJs25QIdEn21PN+$qH(mx|())*s3|zYr0_ z4Mf74m;?wXAt8Zc)Zfssg_}=&TM%FLh)zglM-L}XcXsw^iwdMeQ|%;t1C> zE32`)n|1$v$mTnCT(q*{FJI2T-0G*uE$7F^Ee4ZgVn+lM9-Ofw_{R0?$SmGiS-j!q z4sQ<6=S=?foc`V;&ZjW`PectLpd>h4S=pbNlCljM6;@VjbMxOYf}H*z=H3LV=e_;+ z{!x-76_TV$B}4;JnUatsGDf6Arceo~s8q(1l#)Eqde7JZH3t zh}q8Y!I@6L1FRm1m4XNFGdvZ0Fw-s|FMCA_}XLxhu&Nr+V3^G@i6PCb`Fzq5=)}1C3fdCyNLUzAUx&&}wL7p!3q6r@ zr>!qvy{ha>@YH-8-CxtuQiHD1|K(O6Z6m~Q zy&8X}rKg(=vdkl&29JpTc4d<<_`y*Nd14IUDXKeT^GW6Ijr*H@h}#L4%{{%pU*vhQ z0DoCfAArZLAKPb@Gdl9y#kodf5{iJJxn^S+)cG}S(}08Gtw%zxklGl1EAQe$x8gOU z34F7UD^!uk?9gS4#gd2DSJHza%vq9aB%M|6bhi};MwprXl3^I2CuBRPB24h~Jew%i zZSkM&_``sBj?W*ZJ`*2;Xl~qjvFu$dv&M=uMPcTER&^nl6CXZ*zKqiFmPDda;87w` zN&KZt-TfBGi_C@yjO`ztcDX}GT5-DC4VQCHG{^SHmc6RIas9iqTi@bl$80|5t*zgL zqI>rYoxdiIYWZUf6*PuS9}@3BxH#^{ul0~&cmR{EY` z9b?!g^g+)(2~l*O$V@H-h7=^lyF1N$c_q+wkw7Q=51N$26!3Zb-@1OQQo58kBL!z} z2cgy18ZkmuX!Sb?^F`QB6RAOwro%HEmK9U)A!8%;YH7%hsxgDt{i-~I(g=qBcR~c+9ymE zL{p^&A@{)j+OKqYOz|bYZXUjjRLUhwW=)zDzw?-AQeWZQ4czZir2{rpGEcOHU!wTxh2$JR{&z^k{9TR^>GTx#Lj-H?$dpa5-!f-tE1dO!d+x>~; z>RbmcIA*FLdeGygj?|5Ug0$18Co`Lf7Au7($ZpB)(s#PRjdJKgj1W6oMw(IB^(m`n zi;n5tu=PK3^Y(4sK7IHTqc)C7kR!M=AZ6`W7r~A?8UqlM#^mGF#?z?R^Wv;YQOQakJScX8b_7jIb!DXotrVU?0HUBt^rjf7q2f?lYrS(i^8{KI zW29gVQ^%DPBr7!fTZ)xF+%GCR(0d;QMD~>A^_{gNNH(r57+eM>cPTyHsV~GnqBXLy zNJ<-&9L@xVMy=*DcZL@Hd-Wl|il&E1D40vW;r^W3=V58-`uORx<~+3wf_zUBK@Z)3 zgP}hPn0K3ZO$Q~ivKw^$y$3QX9JRAwQx%Y2b6&) z5))@-c%@RR=?6H4QfwqcZm--I5%B`?D58nw%gLbutNC?a=W9cs2c|Gb6DQ8S?46dH zihNauG-(_z^u5Q_NNJKjfBp=%+FMsQ12~NVBGuXOZY_@VKbA!{ea zZxnJ8oTIeMIm4NTP+ri5w-?_a1dao4F$C)(*o+`2<=&Axq?8fW$j&_r_^AFnoT?17Y~vK302XsdI# zr`isgD&h6b@XhR1hE{g^%p!=L^X ztr1lFeSfl$&$RQBV*umxym{0A?D@~oU&l;X)HW&Ov6yJmC7)qC@9R0QsexSV8%em; zc3G(o9q@FzqM_k0T_`m7*Q8B?4<`!Ww0Onn?1TJXWn*SNnF?gUl~?KRM32Zh1Lqw5~$OfeM$> z(tv)+SQzi|?IHPDaS;(Hci-Xh)z;Dig8>Z6M@M)P6g%~jxgyt#J0DP4B*PgRzw1H& zvElIR(DEKs@ut|4 zca7f<9xz~(WJRZ0mPTzOzkl3h+cesokXQ{3ub~bTCw6=^Uhm1ly|?TYAspE?-`UdI6p%?r!v%7hUK2rt+MlZ5keTx5DkS zL${b~VX9%WEKqRfDlJ}o{G~4Nyz&!FLSe)Pw=Hp}?C}XHdAPV5Xv_8^ zYNDqPFwb+imP#e%vQ3B^-iRohJggm0o=sATFPd*DlW=?G9KxZnl9Rl#)#6~gPTDDU zx^w52HT9G6T;Jt1f0C99SZI!>Jk0=%^f$dJfS;2mZ;&N@Aa4eOV>oDdVT&-o3}&I%S|8@#fX5 zb3mC*v0xJrnglA~=<4P7a*7{h4wxL}&SQ0FNpTKqH2SmqN9R=NC||5h`Lq=eklb#o zRgC`#ji1o{xeY$FgX8Pdm1c*Bwu?+$_e;hMwthGhr4i`S=ElRS9vBEq0j4Ae9{Sj* zvjiYiv3vLNNey)`!u;y1y6(yq8@O%n$!&SgsdguS8wPt%U5GXZ*7@baK@1fm8ozZi zw|twb{ULt;3tN}F`voeF(V@_h&?D=)b5-~2NQ{!Xl%B8`@9Fs{LV5T#>0&#hqj$hl zMOIVZ84wAquh{ZV$E&vS4_}F2Y1*dv;2?7++|7Sb0d%kDgzgnR9n^rJ#n{u*(037< z-vYr8SRy6h<}Nu7IRcDcNi8$UAc2Vxew=8aJy@Rb>06W}dpa;! z6;7YEJAnz5?4HHb3q+#B=r{b&-MgyXCPbe3k@h)pyYme^hnUjUWK+ob`e*ds&LF@n z1?Z9oT(MiXes(G4#1R-SUAkgL7qzrRYp!ltKKZ=R9|j7F@p6b%4V^44wJU{&cX!BSJY zmGNZ$O9xE`TX^Ewf5Foc6=F@pnlB{86^9LQW}Jb{m?q@mzV2-@VykKQ+XLGn7g0R2 zXH$xa+UU3X`!Uo6*()!HvBN7+>_NEA`pc0;QljJ6yXA>Q9! zL&TzVg$$Ygcg!7yX-1k$j6~W0yPfmJy|?VP&NtVU-@r2n;6`|pb);D0Bm!z z7Pz>eesbu0UoJ+t=pi9fTQ2(X8QV4H!qS7qAkbhzK-@5&bA$o8u7eQgRS1PmhB^Cn;mLQ}dY!X(Q-eIjSJ z-xa-RT{dG2hLRfN0fqi0KIu8oJT9LE53cZ_3-7hOpU_Chogn52Z)TpgEJJZ zw4Lts*Nc`a2=C!6=Qd70MT-V%eP4GXX23!fQ%}ie_@4#Hdt9HCmBr=HuZ;B?sv<}$ zXho+Se?SzA?|mp3F#KOLwy)a-4!k+9l2er>7%yV~YxCU0x4W+^iS0V`Rr;yqR^FD{=CNTLRZP&YL%7&YZ?43**KO z>!_!&P0kz*3)r7{>+FJpe(YuuzxMH%6$moI3u3a(`W|1kU86Q@!-KiN>DH9T74!3B ze-!_cku&!lUIqbYt<4)Z)cf0_`0A;tsjWe^yIA3@kDhYII?z={x)U5#ewc+us$WdN z9V2s{|4GMsBO_!%6y{J4-r6r;O4tgN`2{o6-TeHuAcD+`c$*1+6WAN}qD-e?)tU7V z+JV^xXx^-5(M4J)3h{-s5{!TkB(XoFwG zf;orvdv@7}Fe3NnrbZgt$!WivSy%Lk*K%`Tf*DQDzHxl!k&%O18dvxDm7!c`ZrxCo zWSRvsoD)ZzRL@X1)O*pCSLd6VoU9i*X5N`69*KSW_3I!dwR1LVxB;9B2tQsdMLXR? zSvhjs^hT$$-zV{dzGGvImOcWB;Qy=lZK5rOTIio%iz@`~lUs-;q+|Q`Uw{0#p1Jl) zUF9|s1EY8Cs;T#t*%a7lAJVdh#{4yEK?XpYl-gJ|6IN-s1@lRn_tDQ;BA#Nhe%_GM(*k|ICARR zDl+Hzi|O-}$v}&1ZaC^ulsH0nYygPuqF$rhra1a^xWD@e;_n~4L3IK_Tfu-D`( z>{btc97tZCa}C$Ov7rG4D}Y1lPWIu{!^PqwXGIThKeMo!P(90(y-qaEb2%)N@L=~$ zUtVzv5LlN|vtrk-V0S|s<~6UJLmbemw^Mnn*R5;pIJ{2H#AK$+HnDxefL(1Er97bT zllB`(Sz#yzAmu!?)&>*& zq1^|+ee)(?wf@0e0{V#tBKdJ|k2ZfH+*NMIQkza0w-l{9zIx7MsZBXGU%xUZCrr}e z-$Z6w58gS1`7ZfVZAF{{!|DjGeAjuzZgmNsIQ;A`7EH+!J<~FmPi= zAKiDxN-i#nw}3TLt>br{b?e_yaRlOdV&_10?3e!{MOo7R-ALP?!@7t>tB#C=P8X?x z^XB-^q?5e%En#-L$LNRZqR*$w4OKiCPCl0#(0d9nxstTQBQbvkVJSm=cAf8Vq1o#z zw5m!xdmTWA|L6(zg+lPhe0i( ziwv}W*`~xCCE#MCVDZ{;Il1jAg90PNl~)9|TS4e{{f{uOynFrHzZ$i|@5XZ))T5p5 zM&a*jX_qJPwr!LZvPG5&St4drgtl`vA3J1;wKj=T(;)`5oJ^!b#)0 zqh$Qjm65FYf|qZ2!MRKuGflB$;qw1qX-VMkM!y(0aUx2E5**%zJ;t}e=R`rrd9v%Q zj7V|0)t#_|O|U;|^#WF$xK%tv;%{B=a3jgs0uopR>h0UNmu|+uAk@rV*1B#GW6PE- z0Rm@u94XmNNePc`tFoW4zU;E=A3a1+!{}XUIE@ZrT!glI5a}v0k&&LjcZ|RU_?7$M zCnwqne|Il#Wr52!i-VLbr{h~5L}mttY!c9Bi{md{a{0ivI+5Wqo7>=7d~iZFIDbAY z4<)FC{0wVtcHqI4I>+WZ*B};ywBisNOp-V<(am|zu2XDmwxeZa_J=VG%obE1|I{h} zEOTnh!^gRzp>dV+$;p6Xk71_x2r$h6#Sngb1^A1H&+0JXq?t>np1$+_{{XhbS_fot zDl#m?X%J!`1G>WxBUr`BNqxG1{(P4O3xuFQ+8EkeN%d4a5}rrCmxps_AkDJ+{_WdI z^67JPgRBl)IY_t2Q{t?{EorpK*xM#M3vPxF!0{iBkEs9 za8%yxy@Fs>d;4wu$RJ@QPOV>r=-&%erj;+?gc)3W(^l3Tz#X$H&Lt=e4IHgpUSPrJCgbNMAwKopwJCH!>*5#L}3Dofz1F_dwJ~XGiTU^IZ5Z_ zUv2m_yZ|yCJ4T;e%Q;tP$mlfKy+!MiYS(}^KB$n!Jf#+$2&o~?sb2BgSt(bW#4A;S zirM&wz9=OO{)57J=0ShEWP5fQ+Ub7(@dLNz#YsB3=W+868a8aslCJ-|BA9VsJjh6# zb!m0KbIM)2o+oej_!sgN=wKMjR`iRDirUz`5qEawJG{H8wOk&a{m$F+;B3Z9nhY7j z)g*@xK!YrPVcxRJ?WwTs_i)f>W16C}^0Vd}(z_4^-enZ3d)roYEUKh7!8&$Ohq+B8 z`IY&6x{NG}QUe}q_5B2qO#EoU|7CBFE%N8DU%&7^EnN8H)9C0oOcS}$ymP6&6oo&> z{OfShvzYu@U!k5u zKcK7%EW1GD;t=t-(@e-(j9Ff$jk&lKVgHwO@GgLklP9}Iki zuV*a2ul-(O=iV>8LcCFf2mfGzgW)37n$fiQ2w)vrt3vB-$_;a!BuJ!hQRaM2Vn7}+|Cm%#KC}=2GXtZzLy{l(r^zi0QtKTA; zus=mKQ`-x&py#0M*&jbS{zkt)(imo7RDt^0`PCFd2ZB(+sUh@PE$TssTr|Sx zQ`4NCHP*Jv9}H;!;%hxaE<)A*F)84GtP0ZMx{H=r}@J zCh0WLMStEycGZ@SCefO*lDW>QsA2G>1BGlC-U?4Qw@Cw49$wx5u0@4gad7or6(U6k zpzkm?zR*I|-Nq!s$`W<9gMOcLX*D>~#q{(>lrsPo3sUWNHhwTau;cm*4AD|PJBSr& zf=Y!9!x|MBxA6xwMwp8bpl!XDFpK4Q{;Q{4S|5ol)p^SXrrmw}o^$(CYB|9;tmgXGKx5@0 z;~7D08WTg$AdERQKP*d*v35vxZnG`#OKt6F$ND9*^W?K9T`Z> z)BvBcN5?bwQeSV_Lmu&da};1L*dR+#GpYqHJ`E>7ERW!glxOBgm?JlD-VCbD)zq7P zA?3)CF*c*59_k(k^F=a93dO-7sJ_vDldXwxK}xn?;|A+a(f4pg0QOb8b?+{Wj&PI= zV}lJ0U}=SfA;ciOrD*?QNQt`MBIL) z7q+kZV5X)<^2RJe0}lSm>=KdBo`t8BiNfhaEV=6lBiZU(OhwLfb|=t*ftuNSyUMV{ zonB+oe+$ahuUy+G7COeB=YmO}LVzjyhYTJhcf54RW^3TxI%CVaSdJCPBSTc1$0i1`TYmV?7}PyP-=I}_V>RDb4pLA

4qLq7rTH|v>w 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�ph?GqZnY*apu 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 qJnN6*Ld=E#4{M(PmIu0(EksA-stK8 literal 75941 zcmd43cOcjO-#_|k`XoD{LRO)yMD_?#R!Akv79oVn9@$w*5-LfhA$ufy6hf%1>|_fG z+0NtRy1)0ifA{x3=lst3>-c`JtFGexd5_oY`FyPBJ48!Ug_3+PIe|c+Jatk@hd|ie zkAGH(oAD>*)+;{vWxJ`GiV|U+^uH&?DbWN18{w4Fab1^(6W#98x})oaY0I!{ao6_z zW!L$5z=&7L_N{JjIBlN)v%34M_ib+;$Q^weqnD|h^|X^ACROL^bDj4*ubht?IG*Ai zc)Z+QKNb0>bMbSV#oqdG*(tfA&ktQc|MB=NK3s44*y*#AgZjZ;n{X0@V5TD`WTf8^ zn#gLjNx!n)W>X>kl@HG?=53@ucuI7nC;b}C@&EV@*EWvD`L)1lxU0K6fMpw@Xn1Ta z{@y((^r~Z_Wp>1ha*)NZnWB*fBG+vuO+IXoPoXb#%UTbzQu0fpFDUJ*!>9gS{#gxkB={Q|I6V&*M@83%*)Zhv@mzBL{o}*-?b~nP zzKwq;9J^|5ZC!Ebh?v-U9i64+snSOeAKr_Jxqkh+_uArcTAIK_cPOKv*YdA?eBa)^ zdyihO7k3(>82&CPCFOVPR#-UhMpj;4-kUczq@THV_vAB9l?Y?CZDeF-YTB>9QSLkT zt)+!hvZyCR)zQi6iby;jTUy#=iRZGc$AXaMXJ!ivi$gK*J(q7Bj1D=GW!d=Hdv)9h zXSeI1I*~}Ux^iW7DtTyHLRVM!T|ogaFK;-D)O?$f_gP(C^KD;yb+%kJeNy$|!}Dj) z@bApV@Ve(!RofQ_HQ8vMJ$p9oveL&eEJ0dz@=tn--BmcEXcYAI^__-4Uw3w{8WIx~ zZMMoeYWZ12Of0(A++WMx-Cb4nc2?F*>V@q1gak8F)Ar?+&hOtZ*FT7gj!sTWI-c^C znpxcEIaNSFfZBaN*R_=b{4O)|nx>}a)vNr&-_O0(&%HU-ga3!`Ir^foP)bnnw6=C; zsrZG~v`dA!yuVYuk@%a;%p(E<5z5KA`T5iWMY6fMxvR^)>jFGHS~@yUzm9I&wCT(X zb<*KqzqWRDbyZTTTKqFit1#aBb|)3pda1j*|C zO)a#zC@$`ThK7cdQ}&}rEDBbWT?Gvd4ec6YbaZsyt8?d_oSbmAXV2~q`rcFG8Cuy- zqYy;B-wt24wjO_mD2K&y{~GlVv9l)?6cjvpBCo8xo!xn5;rH>Nwk_vE0s>B4x^ziH zgEBzjIB|4*{QS9d$3#S!_U+^J$L%-J(+j@7ugGJO?#-Q(Tpa$N>gtv+2CT^jwWS-G z8HKhVl6Ie`7%vWvj2vDm?JEiK&C1AVFT5!@KKS%R=-Qve^$bG>M#ib$($XhSp5VC* ze}05J)b6o2{2)6!?e5)Y_4VVUqmP<>44W!hj+krByV{vC8XQdIedjFX5=9 zufOyml(F5w%gZbEgqzci8#k_B-(9b{brq$D?GTpF;N+z2<$9)xW!b}rZT<{3?Bdqc zJ$rU;c2BxS zs7U%?H1ENKfij!4)z#J2)W$=ThK7br)g&Y&*!@LBMELmmX%!kD%j~=8vU<(Ip-fYL zWOS6>e_~?dBo`fPq?oX<0#Q^%CyL;+qO-HVSTR6NKsLdTk|2$;ltzo{U&yHYr~1&GtxEew}$7?M_w|C zTGQJstuFtiqM~~H_U)hH#{Aq|{zHezu@$rpk00NZU|i}Wk2^d6t25B~#D(+~D(N!$3_N}dg6$du^`e+Ensn0KVcX#^{r%*|LrL#}uFutL?aG`_sy6KMF z4<5ARFddA#XU>dY>SUBV%S=`M{()c1Rt>C}21fIk(xD1qFMS1#@z9P3$u* z&r3>5a?!nd`I4wMBP1#+IygAk>%A`H9>C!uDl(glt9W8^Wz+CDPoj)_>-X=6czInX z+K*6EQ%|g%p!U4HyQQTCS5|kQzpkdH{r&aB3OxM$;fhz^zkg3;-|iQ6@1B3dDK)ia z(}$>wG>nXvLQL%e5Ah)vE?gK%6o~c>tnPPOFdZ_twGFH6XQBvw4Qty}jIXvur?bRmHaz#o?%IwPx98o7AqeA>k$M~=KoP3031n7p)oxuvtG$IjA{ z`@n$@UQ_|u_4W17)kuxY+R94WbBUha+|KTE(lLI1eoM>Yos0shdYPuEBZ+d}A4@XD zt6x2Rn&mk7Nim4Z)y)k(>C5Qsix&qyJy!)~hYK2~-(Ad=4c{v|R=wX_8u$FooAI10 zUx`~4w6&w&zjs4dnmYRqh0HtAFE4NYX0K=L{rhHeDqX)UxH#5wxYZe((Z=SmJ2$z4)wh@0=E94gA4#=a$Z+iZ=b9M= z4JRfi1q21xN5698$g=ix%yM2HCr<{yf4}rK<@CP2d(#YyaW0=U2C)vJ4{CgBY+N5t zFJ+?mczgHZii!&HHilihvb|Q_SXdr?`J&_B^yRr)LC8@qE-rC#Jr$KGpYemt2ie5Qaa{0uFgN)_@K#jf- z{DH||H=@)X6QAqs+d`i``{|PwxhLwy=|r@pux^!bn%C*+)t{qy4;>mB9_Us7=3|w0nh(xu;3E#MO?Lvm}ack?bn8W9>VBfyIYHt24D@$0R=EH{%SVO(N z``-v1Bz?Vj_6%jy4Hpd%Q}y29##GPhL`U{opxK2Ymm{o^mXG%QJS^U}1BTzUHL0wCxw!xqBB4mv zz@T$rN5IM!39y4{T50F)KF9q?Kzm{H`0?_`Toh%&&BM_?3tQ9vw@^5{nNWgM;ceP6 zh8t{zV1^B>^uT&dkzAH%x3>r%UyI#UZ=MMU0uB2eQWa|w$u~@2D$>yIIc-#^&76Kt z-=xu4gj|Y;ptOyIp4mjMYpBpD{4jkj{?Pxy17^SG_HS+)?Z-E<9ooR}NhU&LGzzzIpyjK;dPAc3tPeyV21a>gvHtPq6^Pl@A|2TsagJ7-(%{6Md3PO|1@V-}bVl zWlCgZ+ofxl~tI zCnGJ5y(ww&>E5SLp9*hInM$aj6nc1gsH&33s202Ye(L8}iH(QbyJgFk=;-J(Iy%^G zU%q~2=lU4PD*>B#U!`wc}6|v{&<wa*@f*u%}3(YXG^KA@87x8=PFHbpxz)T2bm*+HEQl&T=?dnnwpxQ ze-jW)MMcHb^y!NiFK*qcz@gmf@c_`<8x!R$8(incb|)%q_Sxsqz(*huSy+=M^XB<( zEilQ7Hnp_GBqSIZ7^pqh`R!7am}n#~?}J02SmRXYf7b3gdi3a)Eq>r*Q|+04s?WhQ zE?>UPDs?05^lw5L<;Ie%aS-2eF#BxaNd2GqojZ4erhq>*b$81EDtUW*x(};b z-zKu}`5|pW_GQ;f@V1I=q+LneW2nA^{ad#NJR$Zo_j{AL zrcU3B2g`Jem%1r%F>JFxadI5z!;S@}6Gf!IK~D%~9{F$E*s5&UZL5=-2*KFA>B|?~ ze>!}dkI9uXiBBtwe->%U2*ye%Xi_&PA7)_3QmAb_k?9ja#R@gm)hg}2gfcZjqmpmm zzR{~2_|d%1Tv$Aw>M)>lfmLQL=y@;M*WyRBIGxWCST2GPdQEqO_jx39co#ce_ zYuB#rASd5DVqn~U&DoiPNMvf_03)O)50L982oZ`JwOOTI9MA>D?0cq*F87SFaEB;8 z)p?~rvBRf4PS#U0Lb*D7WANwoGZJG3^oxw23@2X)==m&d=qkVat><5D{;|fE}{d9 z?B(a?CSzAR7)`72>(?*bIZz9cBS&1P`;LPm*x1-OJByqvK_mVcwg(afw&95rC&;Ln zo3Kmg=LPuqE@qjZx_R?&*q)=H|B14m`UVDd1$k%RoO>kexi_eCbZqS0`SZhrgJ}Kp zjNEnKb8SvPk_@OG0NFeQPA+GBkz&S z<7=wyG{mi&H$_%c+g)S(;UNAHQV)Us@!w<+^2JH(lk$w|8%J3^qL zTq^Y{hIRnft9TVxRN*F!B5qjZ@*AZiLCpSkNXWI|T?U6_Wm(ysQOR+p_&Wt<<%42k zv8zpTPo@bp4@7NRnPuE&zr8wxDm_pgsY2Y%$S8Q}z3%DLr!QP^{dAAFJ?}Qy&hOdT zXIeaf4rn)N5TLR@1K$Dffi<+Zw|5oZJS}gtd4Utkl7T^j62}F7{TC@IW=B!)Jv}@i zfS{&5dejYqqc_-F*U zoJ>hcNoU^G0j&go|E2eD4FC4JBdA)+t}Tnw)4$3_`yNcf8m`}wR1 z&@XoR#|W0g3)i=D?wE3-=i@iNl=VFY$Ii{m1I9qdt*@+1{yWI_GBNcjsAq2Ob<|!U zcY}Pp6U67wpI>{aasE6bdzG=!-Me=)B(SE9!kjAncVKH%Y~LbPaK_N^aZF6W+Nkgz zD;H*2GQ!p;FJD@9zrP;MefpTNa8_pKaU!HH6Z`OeVi9N6RaAnMICk&eohfL5OEWZF zTm9Qxw>p5Ww_nbS-h9R9{OdPwMxEb-wl2sC3k$2NsU1Fa2%D_Eu@0h+*)DJJ-J_p`JA1nguy2iA&p3#a2;mg~ej__P#fj%U<^&B^4GH0_J)z&7LwcDs8lNgBZrn&W@V| zA>^u+)jPL2bO&CsnDLPj^>`6pIl1-p(lwmI;d80PojV6E8mOz+g8-sNXE1vM#CBEf z!BSynPEAh!v$pCE0Tv(9`|g_G_1-F;K4Ys>cY zcOfB|Y77q#=Yx&c7%PYGJ#jF4s;i*#?~_gISFEiM@$k&PliI|tl%)0Z=TFqq^7u+w zhi~I;>HK_rKy+GKT0qFWwu)ovhX^jAe^+_Xh^ix4%o6?PcxE$h{K6w{%QC!dZa#ay zr%^1Lj-PaZN2~K z5j68pNk_K_^GUN#4G%NY&_E8H?!MWJAC+;PsRDN`D>I%OYmvG!@kHkp_9uwwM82e~ z%d|XJe7SK(@2k&AhfGrUFlF|+$`Hat3=Iu+54E5NR8-tjeZDr|X^+bCwW(?P_wOCl z)Rv{Wzfdl5koBdu(iiG+s1F_s)BQcUSHI`*vazx8@qx{cblR8xit6y|w*V?Qe*8Gr z0ImR%UuI@zJMZI5g*Q4nJE8X_C?xp@1yxs7WiTW)Ta~V_N%_i`^*ewML&9zsZCgFN zjX>S+*7bg7aGQg;|DBkamSmO4j5Bu$gnm-p0$VyQx5~xNUhZ{Z(>jRQ%^NoY{rzW| z#DZClsXt9f=sfSfW$Yh3P39K1o3}0PQfG!qWfZ5%?Ck6V^~=|<&jnKMbH09kS>3Yy zd__Y8C41HTT>Ko``6yCw@HkAYhzOrG`JR2c&K04zaR$a8s+8X6%ji< z_&4BplbE;ndSKx8!-wB|{HTm>qNrH;@+Hst^XE;fx0NuExGrHN;n_3vORjku5yDpL z|2;Yl*K6-jPZ=nHBZ=3zfaPcJ$Hj%L{dKJ%gi*Bnm2EY?BSAht@(aeuvE&dtru(JF+5gplyJ;qBnyJNNH1Y+&>Z%Ufk9+kL#jTOZ4LQ+2jaZ79IaC`M5NtQ`fVnwlS~l zJU`YLr(db-w0uHFR#rv7gE(`<>ip>Rw8LiKGQ-pE?#uW24S*#%xw(gb{+ylcmhx}P zvZ%|jfY?NReiQKoo-XVk?*|iQe^qZI=WW}Z~6J5^Ho+<&?w+0Lb|D~twqO} zu)wpwiRA>{vB+yRUiCRJfA;g|2LuGt(A}_AiY{(q(}N85cd}cNSX)!`^7ZTa^Ql12 zFndm)wnsHRbm-850|x-Kio&^MWV$;$6Y{T9U5b3UHDjk=ztx?AWQXK}xq}KXUkwJ{ zt9q6f5lc*JotL89dTA|}s4RUXvm|>A8sVS9(c>cQ=nV%B1ZbqZc!8rnmT|wYkd#3V z_X*AblsRr73u01DO^r$<>m$kQ)@UyqVEE4=7a=8_BgrAz;Nd~wXpkz3iafjnMxEV& z4FTPK<>iTDe{_OiB@SvoIqx-TYMX%=R_z_ejN2<|_b+QZ zh+py7Qgrh8&7J%?jjolM>i?4rVdf_D7QvXK?ff9K9nRt<1We!UF&GiM<9#K*-!LK_Z^LTlSElllA6JsA%@P zcMr;q(f6~5nyf_Zthd~nUHEMVITr01n_>-x8a=J4xw*h?&U)%DKq=((udS^;=m`M| zJ18htOb7M8Ujob+X->||JLXSg?{r2`FufAyjGWg)^@C?#>3(a=>+YSO7#YFGjQ{%e z_SrKoK|y@z{MpZDvF-nY^j^N8d_^kD%*OCnP%0f zhC*@mUU-mP6U~raTGdbFx}wS5_Kze5Nf^)ugk!9~-}fr{zhhea-jWl< zZQHjW=FYdP%p0c;{^ zX=zExc+C{j4*J{cO-)J3a`9wwIb(xRB{Q2XFQi1f`)^-%EG)51&PjD8(A5SG(SHu8 z`}D~{ybG$kdp$}%v_!MwoXpIQVt4q!T+y}gqKq4QbFM_k$A2fZZVf&*p>t+l0#pM$ zI5ifIFEMFl#be?vx+>JOiHowwU>`%N#$FR9E4eNqE{^qc^ZND7{0PzJE%$YrMrTmW1iB;fd*i6+L{dr)m8#e&A)6>#kd>zF_O&oxUi6@5d%7{>{_usK=A(LGG*~+-lx~T^C9$kp8 zmX?o90VO9tzYKW2TCBk0($bBaH;oE!G<^9&N9izLaS(92#VFm~#Kc5TuN^x#AwK?r z`auQa`1Ae#koe%SLxPBvXXCs0&H+gWgcAJTsEQ2!{JAnyCrFB9aF{fv?=i%E8Jw8t z8rWeCy6-epzk`a36(-}2<2w{!G3|twF*=IvHUg;pqa>s`Sp~Kdhd;I+Wj1orOiqw{N8O2P4Nf@Y_C* zW(vWT2JLCn_1-JIg5w`FTm=OMsi`CV{r#a?kZs%6UE)~?bCLbtqen8=ekje~h5R_; zz27ZDW{4)tRy}Xm0g@@$yj=blDUFnWoD*~`KsDbjWEqQH_LrC@`g@9D7o#T%>gVvt z$Ut*}>~UJrRlOiEP}$K@czm!v?kI>A*`)*`+^XJOo7jwGCh4nJfAw%1fsn2(ji;j> z!HSz-R6qC!L6uY|=8LkwnF@0l*q?1s828D|@062log*KJahSlP7&B0~6J} z0{H@;A!Y-$%ujZQ&XH#Xi727^L*+uEU@9b>U=8Sc}4z7Y{r^W$&Da9^k(5rxwr7Nr1r@7%do z8kHlUZBDS;&CCW+fG;;bE=Ctd?N8F>v7UC34p7(w2Qa_as}MU4`d|_0BTz7)IHWI_ zo+}`I?na`#l88lpPL%5%NC?qPss8W*&AShtJ{l>E$P|=~2M-|ToWFK06PXMp8XKiA1E-k zy(KdI{0K6w;o(7aL4~~7Gg-w-_xC`5sIoKV?D=hZ^s#OA0Fn7qwduY6l7+IKndZlj zlM)G#%pfx(oq&y@XAcXesjSRLo~@>?4(ynvkRuD52TTps7d#UMn8b%JeiFGm>)J38 zchnNbFCRBIVnY`%UAn-?lx5^_^Y@Pv=Xz5gK75#*Y=)DS&rj<)Uz61+;XHB&o5kP1 z>e;F2$=*`r14f`193i{jV2g-^uRKCIae#C>I&|)}wB8MTe&r|C)xSY^ymznb{dF66 z_d>SH0Uhu|u*j?|M=U?ELR!!LW{SYod`*8^117suGSN@|?Jz`6b8+4D*(R;nR#FCad_^Hfn--CPi=-Zu@@AgQL zi@{uj-|IcqBP039@Z7ok2*5ZEsiUBIDb>UQ6oS!Xsj-!~Ax5=#-@etA73CWdw?;=s z#Bs+UbQKi19zX7DY`Twh`$*(Ia9|fBqfw5PcA{9H5K|ah7O(=FKeVi4c3m}t$5#5v z{m|Iph5eDdQD!!MqLSV=qT;WUjHxL(teDY`6}#ZtxegcPUoSk#D(iA*rM`-7z*o1{0 zpVVc2G&`xGF{on8aq{%(DX3zstgNVL$aYmwrBH9(Tn?u2?c2SUqzd0xudaX^>gv)f z@Q8@ah8o$nzm?&f$aCtyd+(m@_#kQ@zg1%+??*Q%ipF8*$FfX*iVoh z0RUl=VG{%f24XdEPH4tOsp{wmBSEBoaDBRx8vF<7n&yU*A`~531-aGPGuN++4*Q-4 zK9KcZ>uPRhf7wJ}VqJKg{E`RJsx_6n##K{+Lr7BGoV@6jzgg+w2AlrL_Flk;2Z}@K z+R|YpBPg~ z0f*xg6vP5)27Kezi07Jc*b4uGf{vcxHn2Pg4_ZhIa4{d7=-M?CneThy?~puJHbGilDj!?#BrHghj4GNFAiB_(U_(ea4TWWxZ6XlKNIo{LC}Lb#3xIeG?m7DieKtpH zY%XV}cv?fpClU8IFJ5dfKQ`a8jPw#julE*p_vO|W6x%Z?+L6kmwXHO+LT79K`gNDO zf#UmgvVFAs30^LXGZt9w@Z}VgIikQ>q0_!YDg*|oNzhRyhlg7^>!G%6ZEa<9qKEEE z@*3$*`)%qg#na{Bktd&0L=~E~p;8Fq; z@&v*yQo_KPDFdp@kt2)0@_YReKah7&I;`?)l?;O*gQWJnhoD7zxHrP=$QR{!>K& zEi2Y(t&(K&KMfH?62kNS)85i1O#|JJdAXD1ts&NN?TjE6+B2(r&>L%mXzA4@gKTzU(3BX7DxwlStI0$_`4( zme$sTQ756Zrw-Wu*MvuQ_VVoLt~V=y238{9!PrUmgE(`X`VB|NA6hZ!zNV%>P>q4f z!3nn(-7uC{*U*59Q|H#v(~|~BiCYW<3J)ClCjfucbCeF_ZEovMaq2^Ga*($M;(qaB z2#71=E>c3cE_5rl=vq7t1k@^roQ#~PCtRIw-W2mIN9az4Snyv2i8s|8h<+oYZG^t* z_VB8SghO8$Fm-kKzUri+_;`KzZ^w|&I;XGy-JO0O&mO-QjtiqvJ$Z6rG$nCKU2yx} z$g&3s2?*qRiARJADtX#v7E_0;{xY3tvpvAgo$%lRO^6a`_}=iqOZ%z}ef|*{+6|8Z z5Mb+`ONqs8u(d@zgGAJ@OqWZx%X=${sf0vCL_|iOK6NVT>nOH4Dc{NJMa#tW0J!Du zU7d61hJO7jb{f`fjL@sA zySv$+g7i2)x%HJZ2nTd^J@mXBd1f~r1o|9QU+gN+#~<+UZik0|5V+B=<%Or)-`@|a zaF~bZ0b<*L>Cg?^4O|yzo+B6tkJ#4M7OFSm4@kkIpFMoY9HUA}O6ac9J#>IV1dOL{HWT|>S1JL1fFm(y5q~2TQs0u)kEAHs)1wLC&<(7?e8FT4IkbdNVQCEUYwei zhER^n%EFQPFSWH-Dg!BH7~mDl6Prh!FB`32+`T&nkAXE*TwEL;X|^E%6oAY;RiIM? z_7iT?c(Lx4E4%>;66nB@i(GW0K+vhNva+)1RGi0Hn3%*ZKR;w0fSo0~5zAZIkLHMi0Obe_e}ZKK zesQ~hPR=aI5sY_#nz6~rk{iF`A@l~l&Ug+gb0Fi)s4)+sfhFr}?h`i+kw~<*evLk% z@DdBPv(p_eCRR?nchTFolm`B*i3a!%tjdA-GhtCtv^M&Nh9#MqvW)K`Cb>mWFM`lLEPkPVsQ%$Z%|4?jc0xnTc~KrwA*QGSx+ zsRa3r{YvP&jQb{3gqC{$tfF|Oj$#WUi>h?yj8oUUYhVcN*YZkVXPiLi-d12}%p)x$ z0|l`Yp*6tW9Y`QQ5K1a3>G}R$A3+EinPRNMYRWqk>le5C`7RYy#4B;kJ+Ad^@Px#f z4LXyVX(CtNn*zlb!5m;#AQqtVe$Q$buy1hayakZh}A*8^=!y|ru>>k|vpx0#?W|o#JY&3Ql0=jd@XSJg4 z^XD|gISOy4liZn?)hD?oNI@%l`u3ph{b9@T?r+l55nczy!@gQF?Z>pty{wKRSByhk zg4K$Oma!Q%4uuz4B_~&m4qyle!NhY7NEKITYR@(G^n5lK{_U~?84&%aHa;;nHs#G5 zsU{bHQ`|5(6tDn z&(c21J$1f%sNA6PBybpukFNRrdF@Z5T&bQRo){9b)z#eau(Y**!$pCb4*%30)(zqj zXb?Ca*kbI12QGu3cBh^(2i`}AL2%%au!X9d+Y-(ZdsY^A5bX-#7zt!*R=YO!(FG}* zPF+epjSYc2h=-GMR>L0At4tsO}FI{!<7@HZrZ`WQ8=loY4<(n z3fhe}W)IR22~DgZE{%EEd3oLmBwT>@Ud4mBR7@T|s9+ zX$Cf+lK6!C8H+FmB@e1=MLY@1mQz;kVP%~h8~db6E6zqkgqGHeAr;JaAi)j@Jup1{ z7fRA@dU}XB++18gfez47AZ)`fICkt9k$rFYjp@E}Q0H9Od9|f{f8^N{2 zAOFI}tazVq^=`o73&5B*8rF+tE(g^eGdtqs5^J`irucCIu!!2 z9UI$*o4Peq)%e-okrR3CIv(%SEqVSgwFP*z+h0|C$~>+A@AocBAUi#bkX!zsd;SJT1 z`vX`Q7Y_ZWFaQQ~+@QemIRw3BCZ=0QAQGd=Oib8r%HK(#c@`C&4Cx&m*K~jtOba@&YnAm9ZW8DUl!$!tv#?j z_|MFsTDc63*^@0_P1PWu@8FRolsSA%$(%ldZzm=J$j_vQB>7bD@QqT1YAGbu%(kqJ zl8&YQVEq{%VfrB;E9`b#vr0y)pn;pau=ME3frE$PC=J{+@SAQB>n%G*q09>N$v1af z8$9~JBdCyA#yh28d>hG=F7+c~1~{ZYW=DMlNnSsN8R?$>b00n;>Lx+RAg5cNT3Hd-u@%y(-lyV&Jfj5MOQQ#pc zed{|Gi6(`P1z`Y>32@%1*ll-${eO^N=4Cbp)N^p5;uz2h6b_VlWlc@^)1!cf#3bkj zCUsw|a@f-unCLe9K6&-(f{qRc=^+(y>q~&(ofkfO6oard+86Cuaa&Q&Q)AJuos^Y7 z;s7?*)~)GAQ$K#(O-zg|-p$P11(}V3A&@kLfn)|U9rvT6Owz9+g9YSW;?Q3K+WAU% z3`E61Umv+GWH{jxefm1L#ZLs9C5BT>RE|8IZ4V*P+&>=C|B)Mzcyfw=QyAGcCFY<8 zzi#NW9Y!Rd@85|bqAo=Cf5>O59^lo}r$0bDI||E@I*(+QKxZfE)&5%M=0BlwfEDCI z@kMN|dcZZ;+L#Ag^=f}589v|E!HMrz=B`a_d{0{Xm)ZW}g zENc=*d(6Fi6Jukh4H#y+EX0I8o+=@-_ZMWV6@5h^9%v4Ck{!zW_4IXhbBxP3quX#q zSzW#yc(t?mCWpV4mDNjF$stNvIXUX5PcweH?UR1TYA!hgVw*Z{{;5+(5L-Ka+9=aB z;N80s<(*sYA!Phx!2-a5aZba&e%n@uD_}9+_BL zRwwDtN7co|#Ly}L_@f_}E-vqoueUE<%`+~)1<|rc zN*n9=f1G&;$k*(%Vb@>fnV@2oW&$I)KBg-iho(6(I?6i{>|>RgmuHBKEC!uWzCAoW zVaR{8%mi}9&eH|Wpd;2NvWM$eAC3x@vL!|Q*1^wja;HUupM=!*L1dss^kxFPIcoZv% zsDS+d_hx5g7&n+q*^?32MZ8vAT62(t069zi0J@JLCCVs#5-@Uq2r}zmBA=4^gTBKcbs}Eakz1_zu-yZ=7XVNB$3@84?)kO{U*mhFc?DSP9bZ;T4L}J4>9SV`HTS| zMElNzX8TblVb>8L16+I+cHM{pG~LhYul)|BkcVK zq1fE|)zK+ENw-<#9$`$OEM;e9wO22j^oC6T$I!_CYK^tBDH5h*MuvC_Z~V&2&rg(b zKZcYC;&PWvs0fx&ZZX31wWS51>)-~`Nq_x19@$z%7LbI14dJyk$~|eLrl|=9v9+m* z2(}*_jKp2P(NAL-Bj4CmMo)WJ>--CI841V13mXHXG`+BMsrM0)JqPh2B>w=m0ItC& z5I$qdASZ5*Sk+WZV<^MuGL9U{DzkSai{Q`*fGR9}*SqrpTw3H5N-@lj1iG)`uWzX6 z9UXg^m=t40SK+M_PQMELP!J9kK2WDC(|L} zIEu=5n}lZMAH@ExUnW&`6=w@5SGeer$)Z3Wg21 z=uUleqz7x-P|k@%r(~E&!nqJ#84WKXOqz7@%A2vX5E9(LTpO3e=?g0ZAh6|h za`V>wbDK#IhBMM?kmp};fs~TlewdpkR2B5Z(3KzY~`E~`tTF{+OwoG!zBh;)OgwZ1y#UFxwY4ABAe{$gT`k<~DO z5g|vY;;iu-PGg6(An%2I&N5mihhsIrwHJQEGoBDczvk~_@a5U}J0S>^9aR3<>g?#5 z)`e%#hH*onFu(?p!t6GRK7!f|R$Cxpq%;zZWn9A>&8A+rBP_1w`V^mKNB_t)cZ{2D<1(j#}wr#kT zQ75@z;6fipFz|av$Ayh{JsPy%tIW&7R6$V@gbykrNS>RUy=;PAj`csIq=qX=~XzJ(~9T-T)A-L#%O-#soE>*qG zYdrBTeUC@+gDw92P}4+%ElREa74C6%aA?o9VYJyBS-oHCMk=OEU`nE@1L=`;Q$azD z!XU*=MNVF!$|~tx0~QI0hJwz0`Y{FyKQ}g}d>!4oxgT=_nD~MUE`1 zF};Qx3jG#!9z4@GZ?dmXo+AnhKt?{^M4+DB?6P*;lQN@DxP3TmqM`E37e_#6PcM2L z9i4P)WCSDrio}@v_p^HRbFEu|h1+*Ry(UH3U<-i!;>O|uA>M;|0{1rLyFY)nV_Od@ zuZ-A@RN<{FlXB~ek-%`q-+#~@BQHT@Wvr+Cr<)3dzaY#*;)VlbAW)l*p&qw4r6=B{ z?g^&U*b9&n~b}r!*h=`6)lMK>h9`->IGGa znnm&%grI@YJYTLSz$o`1oe$=O05ygEIhCR3W&t_K1r~MxiFx(IuNizU8FE&z6Ggu5r0OBe!6NB~K5&Kk&2cH6FnJNLaimtf&;izeu zXf`v&wJGDa-Uy!q)f4-U&W~K63U#<}bRjmLY8;BYP+m78XG7 zACg|9r(t2v$GJ#*ff+#>HE)1a3&_H9F{hIFlSi@2WUBA7%9H;TBKhZr8uoqu&+}4v zu7k-6#gwnOIb`Rzutxs<^JgcKNb2ffJxEJ7K9q<~Ol&;#p98p|my|#E{D7B6tANUG zb@gfksPODbI;i!KqX6lkx`pGN0m%5bO;fTHj2efU|C=Hg3&M`C*jP?~lIstPr@h^! zzajudWU3=u<uv=hoWl#BF zM8-e)CTXvgcO@mVqM|V^WR)hgUK6IqT(oaDb_mRCfcFV*VKVD~A8in4PTMe=?kI79 zee>z()SzadkEw*>XV6#>7%~^eaB>Uj)dBNyzSvcs(|wy--(x-nrHrgtz^A({+Z9ST z=n2n(0~UPyQcJlcBw$uvP*miEPy{+sT}{Rgm}ZFg$9xE*dD?7+gLUCG6fd=tA&5uc zxg&%gb==#uGQi(o2_`10QqNMC6iu5{1pVEWda#lkdF@m%$IJqB;9grUxurmzCPL%&?{OpbKtPj`pr-d8(Oue0^7?uXs9 z7RC4UT(|OnI@>9-!b-38h9}sEPC$W;y;N?f@T)B`N{NlJr_m@n?&xq)Jw-n$8E9dF z5$u_pc@#9`zf zipui@EpI@sC!VBNCOn}8*py$Em-|Fh=s0-$wIX3`r$A&sDV6Y@jPzoT?*ss?9FpQi zxJuagQ~bBjF&`*dzewLDzYl_t&pb)l`j2DrEM7dVqC%!ngZEFIM65y(Fp-#cM99ke zK{EOi=et1PRsf4QY1f&WoH@-#R0r&I|(2wo4-((+1@<_Xutb!X0whwle@@Cv8n#Bx!J9Cauay4P`GNzckR^dL+CncCVG z-JCK!TfJFicXVx7NQm}zdSfC9yMI?X<8%)~6YllbfwJ9C_Q0 ztZnQ_eDzOFoqn$KO83ws&GgjNdwdTcJiwcOLg7lZp%>z1PXkj^ku;wg8V<}3{QN1R z{(Kuk#VE*l=@xURHN-5Q4bm~y05@R0R}H~DxKE7q^nLCaJ9zf|ImRQZdwUZbE{7^TMNHb{ zRmeBzmwsVEcz_{F*Ik|{bI~2gV$fGEUedCU-UnqlZ+VbMJU!>bhX{xEX#Qo znR-|uEAUdF3X<*j5YoylKOY=}H%9HGrWOMtKXz;$H4`Ru-YGtY>gsA>oAJ?6ASDF9 zYF?31?7~}p;P~|I-?rHo7ALqntE|U6k3~o1?V!pa`2tfVRet@9T$3wAr3(;~LPB=L zsK!W7`K4dD{jUsQ|eS!GW4|GK!!(ckbXU`}ZS90ftp)E_{mX z3;Yz&Cnz$pcn=TmO;OmlSFx_FnR6f=C>vO4Gey)Tmn9IXwZ{v2#>Ng6=vcqbMB<{a zuMe+A!g{QI7=V#XWaBCKO>m^FAi#q8>w$p*D8PuRO568HOFk0lAdju1q@cicBY!_O zI@-T|(6At}c-vrUjNqL<#zNC`cq>*-4bcEI!?x(px-Yl1YzZWv z=cZY2@9IiTNdZ6#^h5@~AGXf38&_;?l}K+B3$}RV7B}3Cf&8{M#`T(h2OfIWb>p9H zXfv#cRC=f5&4rj@K|wk7mOJVsDI&NA0o4)eb-Saj?F202=b!MNDR4O~%HmuPq%79< zfxLyJQxl10t$2T*9EM2w7+|n7v;RcF03ZVdfIkf11ibPPZKNLG$;q)1ktDP;GxLj$ zm7WuBPG{iIX!b)<4AyHhA9R4IIclEnSl2$M^y9%-4p~SAj69-PP(O?1KMO}Ec1ztnJ zVq|pT!f0RL6D#R4Mb04=2E|sRwKWDJrcFKPdDdt6T{EhK-tir1!5COuVJXL)4PcG+F*G? zN()-KAZh-%+L!#WZuYbJ`5W8)jv(ZOK@>UHnXLv_jE&j;xSILX+~3kqLTD5e)3OQp zs9p$BXj}ID_P|RR*+d>^*Df<_Yvg1e1J<#yAeeb$dH$;8BUu~`j|8O?FY0gUGu5E`m}AijxBI$p1myo5y3hwr}582$c$z zR6>T56dDw%Bq5DxkOpJYprlDs2&sffNh*ZqLi4OLrBNe|L>i<*31xV`r`EcEYu)$y zJ@50ppU>}o{j;=^>$=YKJdR`Aw|(2TU2!Xt4qs$}g3uMgznfd{relvTLGFX?X@BqkC8p<1?M~aV`?4?sNymmN;Oqu;2wzX$Y}G=k|KbJ5 z>FqMF^e^3%e$0{U-1+;{KBq&nkhTIL?Q5#`pdlcuq5HD-;(FeN%S9}4_57Xo5g=&W zG&`NP!SY?Yq?DVO+uPYG0@6c6ZC9!akqK97U)%l$mtSgqL{t3g7JpBgLa63Qm(+#m zIkDOtGHNTjHbn@*@;CVK4$d^RB2Xjl?y40lE>l{OMUZ@Uz^t zEIWBc)0RU1@rIwkobL%k#K3zN-4uFVw_vYj~$yzAKhfP z`CBbBH6aKKvqSxx5EWWRl(h)4ZkAR}?b(&e%&T+4VF-5Whp9cYw-GJyzbQ7dwzQED z+!^S|@G!VTA0UCOXx{;C00d@e7mP&B8YKMT1C<>iQOW!nbB zE28`%Uyvp$|D1=ga9b#{l~=U0!4}X#0H=80>T%7pB*g+8k)_QZAoyI59}mkdWkXIr zco0|TXV6^M`aO}4A3R7*O`WVEKdt8`F&Jxh#r0oa8cvfE^Z(gB#l=l`_W6XYxmNeb zrOaM#vgNUpF&Yx-bHiSeeNffMmZnVQr?9(j+w)DIM zinKJnJfFhFXDu~^#0mr%LhY*1i%ci1jAzY%fg;?k`p}4}e0)?TIM3B>-n`F68AYI4 zf)!?UBPkSpnpz}b+UaY;h)ELCi@$2hN)rW>HmXl!yV_I(3?ooNY{!IE@bSgZY>+L- zows$X@P=Uc_fA5sc%h*V>D#}<`k-xh-udtUsbb!HJKKMt$Gm~DuoI{ht z7lr3eote%Q3uNkG%s$phO#Rzzq+r7g4Jm+Va1K)3%Cp|o|CVt^+cRj}XxkMWn~AO& zi-5S87`={pO_!tEq+4o`NB}*CMFu9V2nTf4L|EdE;xVzY7o0LBJtkVXKD?!r!o%JB zhE&oXpE8=3ORv_`nH@N4l*%WuiI_YHz=lUA>;X#*O+dKXF{Y*{5rV&Z;mttlg|hyX zXY)czU?N|DUJnV)L`M){KUz9FC+8h1w0G}l){@WTSMy{>bs#VmxC)QJAuyMF#7CAh zZHyDQ^8VyRq|;YYW#Vxg#Kh#j@uZE976fsZE$%*wIu@Qww(Gu6Fsc$#QV6afrDsf? z%J;Ko&a7E#{jPr6p_MIi39^!Ss{&0vV(nQ{P|h_~0O`)9rFmk)89LNLdfb5_oJck^ zCrzH*F(Owz(z2!dV9xyn+SreJWDYI$ql`UAW4i3wu3_f3&PC@)Cyyi1V~SQ*f!(ZG zgtnBbT)BLiwok#oApG$^AiSiu&Z+0*Tm=OO=PezpqhNdZF<`*A5%n0hW~us9j(&!~ zoS#FbB*l9eXqi|Oj zNNRG%1|1p`cIHnl!2Em1?9W)ab$UL!-&bP$9)Fn|@%DRY;hY8@*tVMNa_T{78xSi} ziQu+>M!e;nUHW4tIUN>0srrvOx`sL?fRjmYeMc7IVVuw#a)NkX%Q)rGC(=OPD3y65 zOM1Om!{D**N5xDNlWQD7tS+=snrBiQz75~?RNqF|NiH;7JS=;Oher*9!0PHKrFAt; zzrM0L(31O!z#d3M)DVtyFI@;A{(Z#>*_`LkFJgB~@IEcHo82|f+_u($jQ|&;M@(w& zZM1iihCF)CeX+3w8L4G>`f&q<+`oR1M0kwoVGRenH~#V}$2UoRQi|}JVnx=pE=a-V zZmX0lznr4|BBy9YbDStg6g+wqSo3iFS*`aC+Gbw&P7u0_Ph!@|N3C<;e0TYF-a5 zeRcs%HFs_HfYsc|>UOpBmo9a{=$_AW@~y0*F1xD(YLwI$Hy=D$ zM#)1^*_+Phmk>|x@f``SVjxH&slKfPh2rltnIP*PG7N=RZG^TRnHJ-8BNLqawcH8<5F z8GsS%*jf9>!0>+u_bGSYZLAiB^&aD;IB(x4mKT&1ebcEL@)LLI1!Ozd*VTPrx-8+_ z(QW)otoCcs1FJ>eEGQVNshQ#zi_Vb(CTPl%B?GQLBn}N3X`eobl1v>Onv)_%8m;&s z`*gFKtTf@YurKLRGLoe?Bh?)k)(AEfTDI=wQ{tm3sHiMM_GTCZ>rN$S!F$GLXzIT1Y~0kf{?CNlT?6M4`H$6;!FREq@l!{_gfd6QGL#r|(J%O@OvS z6TmHVZl)lEcrMlQr$7Mo=^at`YbZmIn)J~VWwtt@UpZZPwd2uuthG9m{7uRDXQ2Qg z9<*q}^1ep1PCR7yfZF)>dhOS+3Cl(Pazg2F_u@LHaQsRAv#;D3a^Qk_OH&}fOp=y^ z0RQhwZoeaza9mK#xEt_R^M#ia4W30(ddtT3``y#X$e(V%D+yR(KXc}S>32n<6ePR; zXCK~BYQ0q+dmka3N5Y+NVmT2Eb+T!>$4NRsNDXzr!z$&`N3g7&et=0Df_AjIiru>P zM{duyFmTbL|KQ=NQnJelk5{j?4MU#SL4I>GH%T;RY4kfSG7Ui|&!6u(8)dYWnymkT z0m^v_Aus;-e0+MhqM;toS3ssvj=-jqGA8h}SQ8?TV2rpEKPvuq8R>SI9;&>J+4_*@ z(n{uB3@?lrfUW+ygo1JLJ(o2g+SlsCIyY>ksUuhs zOB>q}J18A($v^=JXN-+~KO^yJ+3Xw}jZV&#u>}PfLVkhi*(8t@mBuN`NGeMX7&K@( z7Bc(T@SBgHJ|*z(_nn`7#jA5Y*5dbfVr@BKXU}fngu&6|{E3f*&6O$;LLP^1#?SJ8 zzFmp4paf)(1R0U?KfvLtQZEz`XPvXRzW!etz1rQghCj^74iC zLv9bg`OaVdwT~jbkxMH+J_M(|Ei1$F1iy*$Dd@A2h`bDUDQs4Biv*6;s5x-NHi<37lHrDAj@xW*bin29!@b>#VS`o)VD zjS-JW>F^0TY5iuSn(Jic0e|Er^Y?#;_v=j~klCs;-MBDz|QtqDcdn#P|sJV1nrdK>RM zdi3a9k}#&TVbz%o2-=2u-PZPzNtOFWsxca(@MZqfZBmuSmXDg`J4ssRseZU%FXf&e zq_ax_L%1cWTBUI`z#{s;{44!zb4HL=o2e~EkC>B{^_C*m=K9UUGY^b5kc$5CC1hE` z@-VJ5x6W8O6|m5+-}obA4d2h`HBh&-Df=&Uwbv1C8c8xT(X$UHB?(CYxB^NSp>2Z^ ziVmIL=v4IkB!1k_B9^l6uAucU4w`#t9%e=ZKqpl>B zkXW)F0VI3$udyosJU7{A`<<3mkf*H0Nu zHM*=b9Fxevg@7e+GM$=qI~~fMGpVVmlz+#M_n%%3pj7d#Y!zua*uY>Py(0RMZwquD z6~~D$ed2fL*kh;Y?t?;gL$gb8EpvN{RoJWH10wt?{7%qoIxuN383Hs_o3Bf$X!PlG zdogf#c%5)W@P;+lj`oBFb50*NV#HpU!aP@`&n(bm$jDK7ec>KgnG@VSWZtdLI1dS+ z!%gISf#s3;9I8>XX{m8;u~Y561%J@kSKIvyw8|}#J2V~6he>Ldb6wO; z?#)9Rs6e9Df=3ueN7Spw|3r&OcE$13)RU zmeko39l=3HIZfSMXWvS6D9Gp4r^v|@n`XC>5DQ!{465(7yky zPN_6NZtbriU|4@iVbVwc1Foam8hl%UG<}<-_%(;L+L~(u{&f2?Lx7pi{J1E8Dr5+{ z^vc;C|A52?C|GFa3cu@Yi}>U)*h|*51p>EVRBhoOFWHvlZ_Q9Qb{B~M1PvVv0n54{ zeGnyxvQfMq=Kt=c=oO%-H0eyErLmyknj^psIC}6Ab-HFdOzrTKu`^s4`NeI;yKAaN zV0wPU_U(^M%H&Mh3ux1DInX+DA`157v->jN+}S%k#x#-2<9^)G)YGRsrzQfPjEsDs zUSJ5PYERRU@7w)860-Dp*wDXN*jW3HR1vQE^YM%RB!C-Ak%mK*O;hZczocmWFAt!B zsp*C(S~Bh0eXFe0B2eSlvD5UDkXKezm`PXFH*gk1>p4bzsja2tg?e+kjYmGBxnM)< zc2qJW1XTt)E^1P0XPo;dk;(b{`y&FQQOT$;bk8d@8h_vz-w_(xF7P@jFPf&5Q2&8_ zdyXyro0OzA!=u>HnW(VibO({*603U=`Sr`&3J`QXS zoc_r>z0J$L{hK?<%hPk{+fd380JtNg2R|8mFx(dY7MN@*o$#1?LT*39UdZ! z%H9*Q5WgwefxK}B?xZkq)-7kGVOwc3O+BUh~Jgen3#f%6@OFyrmNcLv{U=`&7C>3+mB`Rv)C9TpSS+BQW%04noUgsH^f6z_Z0In3ugT1 z66p3%pF;hRhjnh7;-^===nuv_W5&N2@8qY1;pW}$`#&y#nR=G#rS#(8990n|*e@)I z7)WjG`LsWOSW#0G3+T0X=)pK1ZRb z=sjtVH@GK=?Tujzr07^k_brf6wExYb-?(T)u{KO;5;l2L!9!_m>oSimhEoFRbs<{mjpqWumFdik)P<_t1HV4QXStHtlK?om3;2M*D99f0dw;*=5mC)`;WJLm#tT~ zd)~r73y&zJG3vI=O(SXqYs(Lbujna+>6A=P0dsdjb0rpk-vKp#LpAj^;ewx7&g>Y7 z?_!w;=aM6%5E}e>o};5liVb|_MM!X11WL8v6g#|vu{^aK$)BvJ{zA2~%Jz~{>q{Ad z|5c_`L(}6Qwz~Ie3Z%MPB=>sLI!sP~sXW^CLU%GK-*c297n$o1lWR}F2>T2DFJIf{ z-q)m)9(`mm2DbsAY}e-&c4p8q9ZJS;Y?O`Kz1a-+O^Y-~Q-iYlOT_*6zv}AX!3^RW zpWcAAJ-R)OC=YT6XUsr8Z+y%I;Rd*u`_WJhG2N+cuz;fHe&aB$>h(T~L&HvoEGwa+ zfjzFOfjOgXf>Ox7aQTQM*yHqOKmPKp6u~HxO+kG&oE=KfWgP~61hGE7Xoo}ceo`Dn^(_~nPm) zY5t*jyjo1GLHSS}OdR`8IM~hj6c;u@9fjLBZa6da2Cf{AmpNeF52cw78PDJf7(58K z7ia*{w59Dpc`6=uD#uH84`K&qafXGp|4~IBk6M=;l!jTixmAedIAVZxh!7Y>N~pA% zg$4N#5a4l>4o37CN*?+Ua#<;A><-@iZzVcYg^`H)OV}e$db}{uv8Mlc=F}-nLK0_3 z0-KGDeunB^r7FP<@`N73RhQfjK~p?Dj<7!-oxxYilorCL{BLNB~4!JrK~rh`}*&;0ZOBEVDC&L@4rviTXO{*W5(Db;Hbq zt0U&SwkWW}RCm@aPQ2VxS|UjH4`#pUob$j~`p)z*dOlw$h3t7LY%@=A92#jm9scI^ z$$6pJE)Y0PlfSQpoGLatdhpwVLeGQ;?(`gSMs274$=8w0Zq~#EusIqS?7Ur8;xb+D zcYAYIeE#(3_&J%^OE}5M2{q!PspCKYw4Pcw^dk?I4B^$^zYE5-vu9a~LgEIy&(tm* z^Hp^eLQNAXTL z`2BV|XlQI!>ZtbzK>d1_Vq|SDxfRXPrNggw&Fn(AfU<3HcUniU5bQm85P8>a3ZD2sC(&RzgBQw7O-;xy7Bo}pPKIq;{?t;ZXjF6& z<})N@|NfJ(K|K}9qJ&7nHE>)?D7Dm?tBhpsP+nXh!G@#r-v6T(DB!xp5|R4|&E=gd zCg~X;Xt%M)Y!^pW-m;z|7J>t=;m&Su(`{{&TVVIxB@|@8&0%cTOiVlJ$%v~+%jj-S|FZ>_*D&FU z7CuFSK4C2Ovu7LM2ot7(Q@}yWB7|o42tBfv2&RXybOi;>)aleK>;1PFotd1aV|s6& zH@*7*)2ixrFG->0=wo&7jpVhP!_nO6Ju!B>=_Wy>g;T5>wx>?bKFkRj6}sq zxRs$&IDrLVSItiCUa|y%J1{wnc9b;*c%%8db`GsjA%o-BToUKq^6BlcS3G{DlbodNayaKrTQLxP#$D7-kYF8j|=j<>bmw z(=9A=NlzOP(+}54W@gny20Kyve8G4xJn(cj;1x24Y~&0;6p5I{>O}PJ3mK5C!-mz- zK|oG6uulyRSH_%YN?y9zH-oGRinqGDI?fk94x9!d;6QtV{}S zbv1`468p&$CS)zN9jv^P*V`y7h%Qg>u^U}DcX0QKo7a_*9o9!y8ddz!$39HWz$RL( za_SV%@k#9Eq1gz3jpX?So;;*d=lSu!_pvIKw*#tkG z!%viM6SE~gUM2AQwV_09QdnKKhiLch^wg!Q__}zy-5)ajQD^@y(~q(SS)7=4uEVVG zurT5^(x=ulH-qa5ZFg=b!xb|9Y6!iks)1E6^r0I-b+{$Fgu_%l2uT`+79}-?0op5T zs^5;&EI}5+nM6eRD6znR)<`CkUw&x`RT6wl`bs;*MUR8J*CZiYA_NwAMwBEnH>1jX?0&bovPArptSJvMcLaR>E=_4(g4AKOxua<}2FbPrb-b_gh6}U#0^GC}gaP!+aetLS3J71jo zcKc;7$_rSNk#)s^e`|3pJ^qb7Vx>G_Xc8XT@^UJ|kS>I-%aDOLcyP4CdO#Hl+kyKi z@_?E@mRkQ^kUyvT;Brq-Xqq7*je)HMF~P*1@grah82iOuKr@HJ?HNmpUM5FBtuw)e zfTjdKzoPd&qBDhrBW9D6h(ht%bJY6Tlg|93N}n;l)bO^zn$XEH8P2?*rPNCyqQhJy~ z2aQqPQROQtaw0gYuo&t6U3y^fQ1S8R&J_7+jK5N$k zV-XZ0E+p)47!4|V0a1Bxh4_``(QGCvOcG77^xr+D#3m97sFbn=piVLrk2za?<87MK z&YiXZ`%C)4NJYo}G~npa)03Sn_f{2u$B8+i;o(gmk|H3%$+ML0(&da(=0@j zt3*Ojv$l9?ikKBEj64h{!3qH(Qlb?Ib2;n^Z5NM`kJ$a8qJoxt?!HDzk$b-lc2Cte zRj*Q&l{E$TQe-_?*Glm+S{HW2yhU5{TzOQ(-(FhzS4|J2Acs1CJ|g}XnSYgjzqZ$} z_CwR07SbH@6ikwoltdM`@v@u9|L2rTaPl%{ItmdD&3FGq?_r*owCSsX-X#sYKDha* zq9S^bbazog&xC`W-TlcgS$yE+{gJ;GYrM#wpQIAZc~f~|*AqcVvsDrRI`|rZhK2#N zxkk?GrdY-`cDkkm_WNn=FUh0XJu{eLBUzOW0K&5 zvr?EeVFG&Xf$EDFEC6MJ3N57ZYsvpy=H=D>5b@^Zuu$V7>?il-$&(sZNzT*d!}cJ0 zB_@lqlzszMY{?L&?Gax=#MDd}X{(eiW@=+gXL1&uB}n9Tqh*Kw!e!DtJ;;JJN}f^f zC_FD`Ahsqh6t}X?i)>~>6%mEx&!DNYv)dICVgZ{xZyuY$-qch=eEXPgy8m96obNt; zLMHUSee5l+(A>2Ws2r7>Qwd7B11*qH-I8TVwatdaB(-_e$ein}(&BIRMeAw-!k+}d z#AI0#glW?d3mCM`M0FP_JTbTL66b+#uF4x0aZ3&*?dvB`&Y+)89KLgmB+rR?8_o9? z-MU%b*Y|mIqjg>cC*qnIy^UuL5>CGvUJZ4%>lizEiC;S;P?d|K{#4Je@$tb~M3avrNAhde+hx}fC=kCJi&$Z<`g5c#h+vJ7qzUI{#W{0Owq&OE9(L-E1 z-MSgBs<1;;V6+$e!1BD~f3@2-uOj!4yFQQ!qU4Y0DMCEMR$&Wf&X2?nE2>tD=HbP3 zDh@F1DI^3%3W$q4F4FLG&bT0<31yZ^`v){#oTxLQ@^ZG4PvML4_qHR7!NV31S-0g>3rcTqfVrz>OxAdx&XI< zK6`I$Y~PjVTF30!(~#FEe!^a(X_MCArX)UhF>&~5* zX-cnQDXLV>sxI9_d^nRdK5d$hKH$9o*@FwmbbwcI;FS{G-l>SMaq>*7@ z5VsexZB!Ivc|76pV3~;DnY4b)Ko{j|^~0|ujW$~C>2N1;ObEj&1lbf_!{dlH;OLfy zz4>d~B3k;N+ZMi8n*(lreQ{2gBWC2ts|OCaF?x!10lyh_-1EXq(sJm95p(xX!ld-) zoTDBx{t%CXMn4Yp1-A$2o2)9ko{4~meGra!BJpP`Puhed$gz1kDsbI8aoXQgbyu%<4;l*?japVR}I{2w+22mgMO}J|q^5f&cN*PFNHSAt#3-6%Ofgpo&6ZBYo=Fjdwc%gB zTOK$D*UZ&D(eM*3wrfsm-NhgxPf`+`cgapAw#3s}L4wrgZ zzWZcTFL-frPYz%vI{TsHzGc})d&+zhJAR)#UDH|8=BLV-l*v-%t0NAu_b2#(%h>06 zwumxU9Yq4;W2$88{H(oqDg zxZg0Rle~%3fqdO1MOO)>f*vn;{(Se29c-o3$laz%hJX{+V$Y!YlSYrePA@-o&ZPtR zosV6*)Wb-Cc(lTwU0S`A>xA|K2#VA{gBt$?k@_!AGq3{DFH~5C!w7r;Ff%+xLbf>T zicnS({Arm8Sv{TQKRS*UDwv$e#=BH zLN-HFzgJdHSyKK2vWJb{c+H(LM2B+ymm)PC6(Md$}qrDJ;ZFxX`GuDveaUh5=^ikp2DLemW& znahV}la$4#s{ZoCEgB&xV(ae*{8>|k0m!duwQ59#-d+gpNs^!>5VcpA-z@A5gM~1k z1Si1|U@a!p5gSSK`P;Xqc}3q^a;lFX|Jno1q;9aSi8qOsr@u1k>wk& z3zW}v`3?y#VMWd8h7lwK|5t{OMqZ8EO#m(NDQT(nib#a!&vvPb=XCuA=m zAbZu?|3_f+SonOmx-V+&-*PF{U#f|o-y&Ct&TgQN44=d#lS~74zR1PPP+F)zQxe(h z5Ghd^IJ4Jp`k%W+?3VRPRmU`Gi4Ar-%5TisP}`)wk}U4I?#HwFZFhvMy=da{TXz-a z8alXpm>{|ef_+sD!an+U4?t8f!j;Wt`|n6oClV*j-^!VCLI$nJkRz!bAq&?fB-0}> zWQfknm5mfx>BM-<|MK!e=eF6zcmWS#2E5zDdy(2oXAz$xbqs{{18-&_hW2NNSUoCg z%px>&>hV-q=6JMCTxPcv8QwuX8vifq3#qleGcu^&Uf#OZcGi!osx=K6jGk3pxqA9Z zV^ep^bM(dZbg7u0U0$;s{}@F%eAMcqb)fW%0zzZhMRs;DfkM9Z^1U=le26nD zSKjIwq@5c!i20_H`hoidMj5f(sb`BLEz!Sg>I{k8eRTD;^NUa?&54G8W6p-N2 znsN(x^nWWAqI$|~KQbrm>aV}p7k2D8^$a?ve+XfDtck4ecS-6kLDE@%~NK3bJ(~za6+j+3!lx`$nVp#_t=~_eeE-Ut^T}gwZ71O%nyX-qm%pqW!}) zO+R6oLOCS0G(NXfKlTnA^!Nb_(gaDlMc9g(X-pg28XgYaHr?8qc}9H0J@CE_uEi?J z{}|l;ZgzI-aPt*GqxJ^nmQq7yPBTJl+PPCFb@DFDyjmZ@WY7pV2YwVg)qfaFs{{QC z&dTgUnkWqN@P_M|O})1)RMa8l(c{OkJt7f54eumZK6k~mh!q>&`N(e4mFQb-&BEiS zsSgp&R`;E2!yG2gd+t8j$uTcmZoW<&0)?rN<(B+bJH9HfZ$rKdX5izI)~mm zM|4$cG0HGlQ9*P+U_t()D>LSydVtj+7zO0{Y=6IWr>n@qqq2WCSG_pQ{OfQ-lwGS zlvocR$!dcA=6V9CAZ0PC#UFAA5X;KRfw*Miad%M~6)0ya$7H0s#d`eKFs=b%A;G~^ z#V`P5DXl;o%BAd-Vy4CZ_WJc}p5=p6v{=4~J0K}cTd&ZOvoB!OGGX+f=KLn1nXjNj zs$$gm?%!8@6{aT$2K~xXWh6_Lhr6Yyk4x62k8f z$y=kAplO1RU4SQXapEF;_`}=E+N+%w64Oqd8nkE2!X-=!v(;q(?o6K z;1Cfn3?UswkP@QYJF7Nwup-H*uNDGnoXwEFtHZ%!nT#gfd-#eE`9Pn9%w9PtM7#)O z!jX^n1-R<>8hPyH1i4K|@eMACD+6kPJ&w zmEG&?;W1GNuVc!=(xUGlm^QIiNI;ojo{ktjz^@tQnwE#-X|YIRHY)AU%2VITl&(VDJ;%I>RXR0$1~p zqMoGH!?vDZjFyFym(bpl&q57FoIU`P2cUp?CX5_r#ofQ(@hN&48A%sFJs*Q@k6*_# zj)KG6Q9)!fW=s+{_^3~Ew~w1+xdGFxt?|jYoq9$g1-k~){>`eNfQbvuuCA>OvEgK- zx??8QHAg+l0^B2zX(AEgG_Iw$v^`=1I(s{;uF&S8X@R_mAHi)*oRBbq!7qExRp0uf zsaFxRAd!iq;Gk#z>r)5S%0x5Ve4`_63MMq%+kv#J$stqh)3nM=GMl5L>t0};WJYS=zOahpDnMyX>w_p{;wh-L+GC%rDw>I!?!zNXetS z92ooZRGz=hd#*hhmTb0MfY5Ay*R!)8aTytYR#t68EMy;f{;37XyuiZ5h|R3Z;yJtc ztP~XbX2Ib>@?E|swCINUfi?%u!O^@y2! z9BD`rA_i_#|9_;7F~@#_sqZV|AOvkB%0IEHxV=_C_it;MCWo4MHJi`x8i@oDB8>B^ ze&7=#z=_>~j%p37kD8b)t1f;Oks?lx-M(7d+BjIiW~?;PHPjG@$u5GsCVgf%K6Cms z_7LtMPd>D$Cy9OBmiex(#%dLBSxBtRP4@>J%@HU{_a(7=-Kuf zv$1p6u0o)eNK_#0aME?$#EI>XF`IV<%nYJ1H1(W;Z0{rzgy)vBMR>fvT+ia;)7oPN z^;^F_=rP*#pTH(o>jHQ8YcrYo;>=Jb7U7{IN8nj%K&|JO(Jg>7VYd>s(_iRt#sH$(@0@K%y966FMn%%i2XnUU{TA6X& z{tPldyLm{}c)sxiDjF=dn>w}go&$2h_?6C`tEFp%8qRS3Jtaj?)R;eD3h0s34~32Y z@WEXyK-FrT1&f7MM@xsGP1vwn^fM;+$H-?DK$S4{dkf65%#LldAI6vro!g~Kc#g$5le3fxpg+6$ol#y^4v5J`%z<>8|j23G}Ujfr49l%_ijqCO+?c zRM8n$cGcLLg`z3R<}l$JYznNSt;!pnD01mENQz{77|7(%o&T|Q(!tC;#Hn?-<8B-t z`rA>atTyU}jxAT6#9tN%QwEwd>8#iKF(P+J%oDE6dAtayg=i>#dv3N-(rgtY}W5(q`55l74y-(z;^C5Rx=OSuk=6^S^1h7IIk zQ<9T&a&(+xXD8jbbPs*UzP)?9(d8D2ek*)SLCJ#1DB@G}{$-sP2tApX0dsrik?rUU zz0aFkn;P0mYNL~<^BG-M5D~lU6clww5N?R|piO~VxeNYonaG{;M(xYl*D*#CV-d1s z9x1Uhdp7(UJk#DjyxX7yw zwB3?po!pJRvZ21lfbJ8aPIf#9JW?Aw^n5;auazOV*)!$ji7&DBv6ZQb)hR7biT-X2 zl60Gu*9Org@mYi2^;0UOZxK`-ArN?CaEnxhMKgrrEL$jI2O5w%c4-7Upu}ghhZ-9; ze7KN^XQp<=x88TMb8*nhNysWI;f)*NDU!!K{HR~wn_m5=oA z%N@vi4Jm9syGGj`<19lncI!FapePRv4E!}K^mH+a8$dxPrvSf$>Q`NpJ(R}E(>w?5 zBpg33Jm5SgzC3(T3R&ki{Di1L*#pbS4X)CtXMZHY_5=A|@E->bU|l;ujhMQQJgDesZN|IrhJFu zuA7YH)vEES8@TS_NSv&H8V`%%SK{~B|TZA!q_;;jKe z3ZeJ|U{7{DKRuGLZK?wm0*%Etor3*%&8>NF@F0gZSJV zRX={bD~j#%ebPYH?iQnYM>2{LYSYFitIFC~nwW^T=XJJp_&Vb1uP?)i9aB(v8tB35 z6h4A~;sMXG{!LFem{|5**Xhx%j((!)lLK;i;VhB(#oq20r2-VcuUM{>a^_4IQE_SM zqagbs1FWU4vlPh9utaQaiJZ932Oyk$VPh8qGHO`#{LokCf^6Rulw^hxDLEfDmnDZElMvov!^j5)MNwY z&(GQxq^i<7z(mHF^XsR#QW<#>51u^vd|O#=)Ad|-Zpnykg)P4{f}?R&UQ_5Mi_KwS6UUF=e($Ml)T7$fuW#Jg z(84qli!3JEXS`-0?h47k>yCOzO2LD5Vf432c$*ST-NE_GUMMai>r~#zk@EJIvWWRf zG)oT1shcw_6~h4&#Cq<82BA8BB?ylAt6%6 z)G_gXeb(-|sv+TnSx27nouXO>b*`pJRKE3gSL8I4u+#LU_`WHJJ1y{W0#ouMCZveP|&^5jiAR^YtmY9gU0#10%AJwjBB z{txNqC7KK<{wP>sOti;Ezdp$TH48B|nI;KkEb z)sVk*{GiU;YK{ZOxoE;f#ocqZC=ZV(hI)b$IiprE&zt zZI=(bw<_xI@8Q#uA1?2?Bw1>3IE$f;Wx+73S+n8@RtG)dfWqj-Yt^L+5(A1ws}y#U z(xQ7HWsc*{ZO7t(y(2e@EB)sZRTJYLL_3U#SoeJ4V?>MI;j%F-iPejP6q1p$M!gr5-C7T%KSc<9wB*)xv zl0!QQv_)*}Gt?=SKh_v4Z_hJc_w)4IsGo98!*!>6)+$Sh63A#KF@A;7Uh^cP7Oy?n z6ka*}E+Gk5t`JnKAWCz8upa`4a}fdX6ivo^gBA#;>9Wh?`sPiW>NxL!7yWYf<+!vY=7_=r5~(yiE4)fX&*2<<89ljmz80oK zLNE3*)f|n?o#K**hv0GD(`N3kP_yqfUL)Upe=kQxVc{?`0P-})iSPg7G*nqi1GPWt zYZz3iqB!!Sl6TEL?>Ut(I$ue_Bi zqwuh=Zxe3!_WgSoMYknOl$DfTTF+Xr;4mWejqac!ir z@oWc&3{>K_zgRO3lfObLdSBSSy^{`U)C|1W1a*FA0)gNahI@iS;bR*enDZpiX%_+r z^qslGRohJ2h55PmNGREou34Mpt~@w7MI>VFlz1E&S#i666`s@UE#BgnSdVFi=Z`N>-9=(xLhQ!l{R zDMpfCXK7`%Wz#0vdDlv-D0okk3py-VFlr_mxI|zkG4w)bWeng={QtQV2GY@usj2C_3Iy6u~K3DRP_4MihNJ z7v7_lh+_#6?n*B&sJNj+h7hRt`Lw$Bj_2vyGMO%FudSuUgi=Cl$48a6J-4yzx8MP% z)$@jg#+WhGt)hN>*5f(kR{xH&ThaJ)C1K!buq_9S^ZB54{zZiNid{(ymM-o3WD+tM zem9w!l+fAzwr<&i`PO#I6#s6QsiSC?4Gb`0(DX)0I4R7>L^yR>D$HCb1$PePm}v&$ zmqQFkI|JKtnnLUZrlR6Kuc@tNP#2S6Y*1CQRHseL-objNBI<1wsu7jkw$=6cZ^P=oPDUERS%X>IVi3-5wj3uX zxo+K_Er=6#_%auim=I+Y0{MLyyS zgik#aj!K$WP}I}%C^P9pQ1pR};08bGpk5-;fYf8jcM?v#(Gr<#)HCz#VjWO!x?Q~P zTN`4j&`Ts6yUPT8kW21Y>kY@r9b(ht6_NIMCQAsH0DU2#8p!8%clVQ*FX13uI5iqV z<62VH3Z6c_@wkUV=t}-SYm@Py2OnvE^m%(TQsmFAy~{)&&W?uLrkfT{G&grc0Rfe) zZoXA-&>)70X8)%8G$s^u&VQ*nF8H%#rUdSV z-iH{YE4Iwj^Mkh(w;T?I8i$_jPZSMymc6%+Z9MA#x>q<&wb%(cT?4Mt!C~hj3OWDq zi0yfgA3m(Es%j%Gjcr&USvi`RV}NUQS#uMkT`uwG0*07oXLI-3ci2+#^1>q+Tu3rl zFScab+#~0S0sPPZ0c($QL_Bw!fWl0JsGcm3V#SBBaa#bez4qL z^eStsJSfzu5Tfr77`dYE8=d&D z-l>k5P&^^dAj~1rD`z`Bqi`851j}lwA|vZR<5g2)}vXAAk-;z{I!B zW(XwH&YjC0lc+BTYB ztq~9BGb&q3QWAdm+Jb1^Ex)71z|kMSTkFX{xWT<%d3;_JRi~x6@rv*8k-W|fwHL2n zKR;YWGmr)riT^9%dx#E@$)pAqiL#o7fqI_zPZC`!mHG<=N5P~o+wIz#5e^{f{%<=V z-~BN+g53x3OSHtYuPYz)a8#5mnDA*llsD}OI0Tj27hiu9I8CkHlG_7w7QFC`)=DA= z$a;>ngw4U6o~2jEYDzio|JFu!)Ajb%)<0DsLEd$EiMn>^PdZY55Dw6ykf%dniC=%% zKlBjY(*FIKwpLzh5{vuE)PeK;vhJ@Pl9jQojjJr4)9~45pfYynp|&Db`Dw$?1W9MD z3$^ON#HE7|++q52#|vq!`yOsJu^e>MqWZ0aJvuSO-wyIlW z2W#_gqWssdHP&=#I=|G|uo!nRM^JxlZJwOP>(`I0(~U3YY=Uy(;H*Pfu=dj+RoRC@ zE6QqAeR?#-JBx1uL^bX7-)3YUuC)29qbAAWh;S~ytee!Iq<6w`8gWP~M+~J5A!TFO zl}q^XTel8|0p2CGWbtC(w>RZPCVU~77+U1}2tdi^49{P(c1Ev%Rl=JYbFxojfacAc z`K?3~`$rg^Az3|-z(1b_E!<>kS1mAE#MZ4@UEf{st*?IHcdV+Ur27deieXE}I3wq4 zUpgU0V&;B%k$+t`aPeDPxv?JdzY z|H7VK^-bzg?ICLGFeA%C#_Xr#o8i#9@KvwBP~d;>`s%xNvDF<0p;TDwOEcGT-#&q) zsVxL%?$~jcL7)X+Yb>9ThQM&F3&QV{1egq=HjjL%+(KI?GAlUY$Up$3Fd_gNJxDQgrfmYDQboRnQtQ z3aFcK@#k`}>?Bo58Kf1^nd_b<1qbti(KP9`{s}n_kKm$5DTTsd02YLzVX#eRYHg<3 z;lVz9*pLe@BH}?UQJiXXe+AFVL{N)IcYWs|t{wd)cx`3j98>fNM~tefdwd!A}uS9?4#5(7%~L0=9Twf=B4UU`%sUbKcXQPpy(=d_QF02 z##Ko>H9})orOq?!=tYXe+=VmE&67_JAQQ0n_BU0?yDDdB4jgDRdh{Dg2tEj1CtAn! zsY_Suc+%3bVKiDrg-jpkzMb2?beF8Ad}3&CQ>3WXTG2JSx_)(Uq~PyG*Ju<$R@2zO zL}v~_cX>Kw9)lc^Y(WWvbv`&^{_rf|4MAj-YadYF{4f-{! z9W;qk@*<_|2M;i6c~Z8uIB0OJa5hk%IsiaejA-c^ex<=+{ELCe)Ln!rkuy~oLx0^7 z+nMMZ>@*@@E;8pczB;&XCuFe(lvyy?;5o1ctPk)rF`8z{NLs#VdEx+rjnD|5z}1H$h-ZVIQP3bZ?hd zksd=K)k*}Z&3lQAi2+B7O@r`Sk3yHJ;Q`MSgj8fuqz`16a|Mvw+FH&SoH_tCg6D9N zNU6iY!-qFtcNB@3%SUTe{qdC#N0dKWK*Nn6fAp(Vp{%r-3S0woAmDnCw=lJNB~?9Z z0rLw~9IDel9DwxGMxTOpga9_u*H0%WkFAAP7ey|jKOP$)dY0XU3)o78Y@PNFM+996 zek)-g!B2RBIE=8ncG)pY?epg=iwbC5=W9ou+xkHqR{GzyQBrzzw|JjIBG=rx>-m6) z_dcV6@;=Vu{rKq zv=8R5CINaqSFFgI%r#oPaN(ACPFx;Km4D+JsJ~k%hM4>u0(Dx62=}`1^UNl%sGY&gPp!2pX^5!^1SirKAx zk!HVcT_gW?(r4j(@Un2kDiW(xnArY zc8pbHI&O3V`cfeE{OYe^;zU94>gWJNBo5sAhLdZRL?#4*-9NiI<9^g~^o@x{Q>C*^ zu5S! zOC5?dZ{lpN6mlX{Y_cm3FvB6!?G(+j0V6kmq1#T`i|A;-jJNS?b_ulcqJ!JA3hBSH z^`Y~s;y)ZtxD?p^Dj!mpGHh-e!YRxs>gq|~zps$V6p8*xwzRJmf`hSe6(f6Yv3+NVP=k{>%hS4 zI2!LG`(rpLD3Q&K^1sTQwZ=wy@yL9UQ_HXxDz2iGntoDh;`i2>sgCT#-Hw#tjHYKv zOK|07hSpR$UPJ82yCU0`2;tTu|5Ud`6u9&$V`DcLKvi^d7<*%<+`2Yl=HGt&fQTZp zAS^VLqY`=hTL*Q-bAeVVC}%D{I#3ty_D49h`%aC~rDHIiv28EC`5|MdINR&!iDj8B}FqEkxgw%Q8*4pdY zd+p~r|L65T=Q&>cwfEY~^!wfScet+4bPc@o&-$M7hIoMjQDv*r_DpIJapYuEjJ>sW z(s3_oMGk2sl|PV*{Fs@upm4CP5E%U7US1v(-^BZ7$XuDt=B|1wK=sP^cN4XT1_{*^ zpvbpvYDzK-t=`;r)FIO74}FYopFZHj$5T_kL6XDqQbP1sehbRT5ka=;-<$S+BTw+y zxQ_{$a&Q|c>{0R+c;%+g z)-oP~cLl&3H_5wK#4*OKRaJFVRE#}wg2ugyE2Q@E88RZOz?nD?`2SRjpm3rT3x}iN z$|vXbP^cVY3>rVijk^n~z;baNjE$UoXpV}3xwUGYjsSjvPKFuCBq?!gn_Lmc7gC3z ziQ9T?uHXymMc%F^tSPJw_dlSI-iXT@^kU#JeLCF%SlOH zl$17?G832snVz3Y7Y*k}Pm|486!#_XnEgN0wx<)$>++M-72pJX#5p;-T<@h`(w2Ao zZ=C}?Ecim+xh&FXSatWrQ60gt@F$pid3w$=`CDALD0DK1zr-=)>MD%RQ3`C1KR);z zqCHJTL$?jPO>dQzndlK|9w%X+u)z`UDr$zt@81<p zy3HR-66Ty*ya|q;Y`tABFKJ#1BIy8X z^EMrp*sw$5_GKG0+7GdRrKa_1TQej+T*$Qi+Dae*pD+cy5#rD$((Cwh29MWLY}E-G zS6cYj##`VZAo-R~?EP{rXK4pXaTq&HAiXlfbfRKn+BXM%xR;YtzQT~pyfIKlDE~v~ zLDs{K=9<1O z207J>oTV&QQ7|x|ywKQS#pdUoxz{NrYNNU%`h#ds|{wj?c+=1AamdsBZPqs66 zCbW_PvM?Z+){iH6(Op8gi{F#Gp<-@C2 zcv_Ef^2f!Yz;UHd#M9dRFu=S)?^omPCq>td-W;pj)%1V{BTi7B7|&ktESo-K#(Mf9I(~Ca)3(1P0~xo; z1t-h`-4;&NVkj^f%dewSv-v8DXDRQ|f>F%+__TCiSjV0tUlH4)rr|}tg zpw@{gx!%(Jqx(XDb%oKIiZR<@?r?{_@L88*U9*7R1pUePnOQetYisxXqqRpQA|)Ut zoy!)24bE_VTvEabTc<`0NVttooE(AW!}RrE=SQ^>NN3UUxB)Ec% zzJ8WZ{{IgJ!H)oxKgf@mmUR$eX!-@({11BW8+TJ;p_*%+S~vpr&8^<4ZFanB-qPao zxvMC-#il1Ki($;g#f6W+?MudgA;Hh=u!fqBg7075esTPq|Hr+)8H@HVKnJg^nKJZe z3Jb<#|0uQ4Rp9x}do`7sYIe06v)ZT7`&i;Xgat$P%ZFlB*>dU|LXbPdK5!2p{8qOS}@a!rWtJYU~NiP08_UN`>xn~lu=W7xu=D|q|EkOol zSS~YZ#R+4Gr_#_hBha06WE>M(dj?y*UA|vN%0i|{KLjffIF2KCxl#1V*x2$HFDB2J zAv0Y2}^k zYcdQ~1F0hSj1Atg1I4>2KWAiT$r6LB_-+R;d3>7FnaT&sit_1Zi#gS;PKO%M4W#;$ zY;AMF1qnUdItRj>{Z*Mpv?kb^m>k5aCLCbegbA8QdgzAQ=rkiB<1M1LVWu+j7Ca3T zQugkh%7rn!KiyHgP~!*0zr^vqE0c&Rpc|o*uhg%4?{Huk8ZG%#n{Xb}Nu88yrnF<; z1riZ_0C09A=H`T9y>)b`SnaE&+0?UffzpHM#verD0%al8;Bba;W8iGiY{UU%1#wj| zZ1o@`m7}B8ug?9&;&vC3jmOV14^Hw;<@!v%j# zR_XK9)!W7%Mm}>oHueT)(TbY^T>VYg@OUz*H6njL94q$qoI7{Ku}3^Gg5PG&G}h6P zda`cHAH8?u)9Su;X*aX74tqcF-pf-Bvee$cZyz#U{8&{lL3U{Y02>%o@0Q%E6OeC% z)Gtb3`_G&n4 z9(Odv=*~=H-HsjWvxx*iW1r>?aT=Ca55F)SOZw}K_&4gZMMgcBl56Vyq;#G`0A3D*=39s8G zM*3`CC83d(oxOmGId#*IIw*GUPAEyJaW}Vj{}pz@qgN1IvAwd50Xc9w_KG04EO%LT zhBWuyoX5-4e_H8u5I4*nMv2Qv-Q@JlD&CW6%{dSFMTLw63o0b0Uzg1)J3Ss)?>d>< zUARH3Q(uLsLB8vd#Ms!_tV~_cX z_9pwGg6%Bf)4wa)V@FLLSQ(-QVt+s{Cv zFqCvTBaRVrb(P<`YnM`Y32`nqWsew<8)N#E#$n`0^;>#^xuvB)84R1vA&Y0y)uEZE zd8>;{NVrv1Z*-&&95pq)h}*}MY~t8hzaHtpRE{%>VCn1*mvrkD3)5|)L$&K_+uhQW z++0vpBx%V|)0&!ZxahdceSCb7Cz2y$Yec4T?c*bjA%f6JE}#X_f8+Iek<)P-v4Oa5 zz{?l#ltJ;2&37DTVDK?!thS2P;a}>CHf63Cy&d98J>H$XFYaFv1R1^|>0d#Rrp==) zXoLSpq!3v>f-!=ts`A2}9}I1DU%IrlKw#FotCryG4~VrtkJt<00(cR(HlaxTxD^>J z!iOMQ+-S%JjvWIdH~P6tGkX07$>i@n_bY6^vCBxK_3yRj3=F`b2b{C5o!V{8$&o(f zF)*r^#(Hbgf2Nrj)7oO+xFg&tK2d_Nz44nVMi1l#fKb0U6;LB^-8RmJ{q;8WIDK`S z&VRV8svm?n#7$GG{;>LxdP_M2>Gt|uWE|7bdmTNlA#zT zM*-jclhvuQ_C0MQ_^tD$WzI=o07?-Lz_M}n)4UQdVaSTZ?K^bH5L~;>Kflga*!Bq# zJcJCv6!}k|stc$8ORJN-DMLQEMj@Lpt_F6Y6u71Mj!h_{MNRFScjz59(bYW~8|zAi z&=j%NaP(Gr3BgrmDq7B;ou_N>2wA@YA^Tqf&45XJIo!xPpl2Z=9^6Zm%dN@MIVPLG zuHyrRhgxSGkM=d$ukd#2vx6_X`o#AS=+)ukg$v&r8|BrFc3Z>|sZOORliy@5?~%A{ z<8D|10%Cm7vM?L=*|TQ!C#TC8g=TLr4r1YOV!GHnd5lB-j6@MI0Kg5%xZ6nu{0*c-Uv|IUsqlV`8`N9o|IX*w>?6Bcd zb~?pZVNhA6=&v}7xcfB-BLtRm?wtA;{oNQvOjjfQTKpl4L0ZXa%goZalOYsSs%B>2 zWAB!4(Z&bsDUvf+hR@VcRegg$IQN+orIL7!u6?)9aHSi7Jyt<@KR6!|en9fd9mI>k z@`Zs2+>16*;-8F6_#1Z8kfTH;+;ZgM1?FglL_HKW7Yy7)Izm>t=+3))cV|#gDUXq> zZ01b0#^sii#*bf0DF;b4gi$?Q3}%{bBJFNOt4VIwTlKFsKnU+s^@s+cw+IVGEL_(WhTO>g+8Hbr;NM z%=iWnL%ccokV*GYi#P@_qA#CYeAKbSvah4Hca?`{(zKb)pRo#UW@Az>KrH0TqO0=4 z1-cpuf&Z!tFVN99Pt$#0r z9dV`WSBEcXT64bB9vc~-J0F8~A*-+R)09^Bdu}~SUed(Ggwq;DXpd#1Tl#-sXVqFm;>h4*z}$f%mGszc(xdpW+ZktZF{(Y@O7{rgM{Q1~ z^talB%K+~Z_c=y|i+o3<=xYuP?>%{9WPfgq?nsacf`$NCv`AH5ynC-#GUDnPY&t0P7F?*$~ohR{6SsprAZt*FR?3 ztqa_~9R%+)w<6RpPy^%*kZabo^)!uSi^8`2nE$|9>UT~RtRUysAmX4BgxTdcrv{penj|U7 z`mCby8r*h=PoGu{=%eM;e)prH1*5#LYJreTp~~T-N^TCb5?xT@KU=7dGR3$e{_{eq zlwbcc`W!9^Mr(Y&Bq!x|j!Ng_0uo1-YAjoN)!UNE`t zW-?Nhd2<E0XBlEg`bN!tUcK2sY_V0_fst+?G?R$lrOERS|ASV{7aZK@GoHw^^$08WvkMQ z37RG*O`MaA?V-l4T;BU1Fk66)j-+6EM5WWTM`C z`EsU&-5A7x=vmVCv(u(uIxmdt79Pf}Fj?E=yq7}?hPDKRpCldPZ+RuIGr z`ri#YLaD60{N3DKgaRW#LC`o+Frgn{0MS<>BnNl?tLXR-aZLRpVoLtaoKyW+J>J{H zt2Y?YHQbssh7j+hKhOl&M;WA?K26e0DL+Zhfa0)@-!BJTpRDfP>3d*Wv9WLiU<83P zparWG-2Zc3U3J5`+5d3jK*4nuZqz>He`P&KP_m)DK*$58<>5k zG@Iwm3KuPt@^YsKN-mx;eY!`&vZOy%DQ4L48V`dLb8g7rf&>CK0ceqW!+23nqVgo# ze$d;^?>U@Tq|Rz$*aYU9X-S>QB&X!8`$(;>w||_hnM~6qN3Nzb#{T{{3LKj|Y&Pyw zcM*c2GE(_(Vw$!a%i>-74!#%CqGb0h&37JxH-Va0xTQyhMhsDiI#I}QO+;^XKg9mT zjWl`MG%&L!HYA^rF0tv$iW~Q+zW>P?^9h8qP7DS`CSBlATy>*Xj>+NIYfgUU5|9uw zv$D1i@7Jf#H;8^H4c-Ykusxb@$qgG!BJ)}cd=hTz(vp(gW@|nh1=oqizx~!@QQ6GNV!q{{0+w(eUuRvOC@13| z8kZG5Ur@N*OOps37z@m$fkzS=ym?aN$3U+#i`mH`f$KBF8x*<{`?$$u&P{(Pjq+BT z)G1NsoGYQBU$=7Q=ANQ(?^mue@GVONmDPZ5)|nZ4@`55RcLW*?q`Z5V9JkWpyIlO* zcMttafl8q4xAmcA#qc0++@SG|7>2Vzln;oC<_^^r0D__C)H)e z2}MFN3Beh=ySu+Gz$B$VdG$|fOa=+D;vQybdn0=o!cH*Ho@VoXlti=4!qLeaI}JvO zOE&@^PN{zRvf*DE72AoCHaal%p8b>Kq0!^x835GFB|I{+_mTX6)2(O~s8_CTY%=cC zJW|Q?n3PwiPl0Jt2cK^S%O&;oQ~N8hyxnlTf)m1M4IU4enp0A;Kb>5bKTRVy?a=*G zKwU}t67&*IheaR?vB{CM{5ic6vQkRLN>@m#NuT3Y{1nJtW50mM3XnefNEA z@hPu8IpqGvD^YTz&%Bt}=lrtcZ};Y12`D%_S!QwPsVAHw*Mu7M``%w`@g%!tOWpSj z=xdoXap3lS6KhT$JM!vl_P6&N7Nm_EdTf63>#9>jYrp zzNfOxjo!|`g=S&Rr9*D0xYdRa-n;iD1R_9xT--byg>3*4>g3dMTi!ON*rm1R>{bot zML#SHVR1h{zmT!DL-h5>+uOe~+q01L8YL|;agh#Pj1?~L->IGbqgP+!el#{1msH-{ zUfzC+-%z=4y_NHTLGinStk0M*frNmHckjv!H|rt_A*lGl@kqq}_Vx->j&hPEB~7Di z0F&&|qhpsYmsY*JlQQ!XORA%VG<4vC1zkFIvXsGgd7r`?v~;LfLv>?gqnIK`0hkhW z(fi8A9~aM^Ta70K1qNYjmL+2#H8>tmA3p&aQs;bh;A4VbaLhfXm=&p}g(oN+Pt%JN z`8f~a4usyr2M?B>)H5{nvfk?^2>W?fjBfdp*Ib1IG+0?U{Lb zZ~5Cr;_lcnx&|mqt(Fjb4z#X3ZDe1|%Ptqr>Qy+)Zc~c~5*uK3Qd3c>feAFP@E_$T z4Z_agpbK8}+f;wgd|zkr{=wjl^S_nS4IW4d%B}*@Uvk4&f;k3!7~Th{NRxrsyYTlR zxF#Y7a2!RRng+u-J9p_~JYoc~7l;qEjEM}w<}2+NmzX$r@nY|ao2@H0&ZoQ}4g(Pl zjTUy;;GH|qo;w$1q8=E(GNwd&-NYP+AVwiDuc)O8V-jPJa#|iY#-yccBug(?@*!q= z!#Nm#lxftt4uI<@8uksYVkJ)A;~Fy~BS~`uxQ9viLBobk=9A8tf#9!tA;8k#^c+yvWf*bs1(SgO zlkNg$AU9*W22A{9Ym=Fr~WW4gn-Lcb1K7C|pT_Z^zhHybhfs@>452N}15ti!vpZjN8F2wFBRMymD4P!bN@ z_!qi7gq}TL!nj=_Xj)hH4&Mws6wHUEdms4U!e;H-FDwvQEm)5&1IF}E^r|r>#OBe& z5Mt$SovLj4mc4jb*n5yQC^fz@g)7pl&Z`Cl&z$HS`=WUTMMKc8woe&A?H+1c@6uamY-NpGpPQ-aT|_U6g5$hmFA?h@E%#*F>p;S4(Y zk=13kI1Pui7_uJKKDWE`auu{8iG*{PFZZE=w-|-cw_m*Y5J7bAO%lA9a#WCx&oKnJ*g%I%n9Zv#V4OyP+Z=nu zGa~$0zDV_ovVO^cYon}JX<2s5zM&DKx1#(8d@q3Jjt*)blPa%|i~nNb)5(3g^I5a8 zmn?ps3_H}U>nGjrJ5;V(CvjrYih*76u1Y(M0+3#ZWn0_HXE%ijMCMzr!1&DjJSxHWf zC2P~8GXr+oyPKD-lqYMCa6$^B;-HV>D2YmzP9Tp)GBd=+KxQ1Y|Sz$ z%`$4@WA;Je3Q|tnzdMUUMI>i7M#>g4z@JR+n0;}{48M=0#c&x`ar)kC`AliaDegG> zWMe)6M~80R`d|H&$>*4T&Fe5@FAc2?Waw03{)rkds$jKy zcEgxU>hLaXTl#2o88V9tvOtB!Aq`Wfe!=er1bMBz%X@T=FpU@B>Ti1!&Dc(LSfJDF zT>1!_ZNQ*psNF$>x*STl>eF)QjcjSwgdWm5>a9Hnsi4qyEvb&w&GAc8eD* zVAF7`Q<(Xzvw8cQwORDU4|+C1_zP^d$cK|XCBGk+j1 z_*(2{0Qva)H5A5R#v`Kb#aR3!N9IQzv?taQ-@&+vAKxYZ6bnV(tT)2hpx|@5^r=e$}H)?6P@4$I#*4&s75izOvcOOorMwUNI zH|V-zY#?U}gKtW^cZ^LFh4IHXroGSw`sE>U@_;~e=x@g6M`$KWUoCRL?zs3wg(T@` zUjiirr%^60&G2naTF=?Jyk(kWwW0In)5n8A5gFUTjW&T5?ysu1fk#+VjxO&PTAINDPhD>y>%6%`Ko*>(gnqRJW=T* zJ^UC;%Xf&|d1OUr1oKk&sC6TW_@pE|PZWcbldAr;nZI>YuqO7xX?0LMQSykv4P+UO zHf!0k57MH{X6+j6Ub=Sn}%R7by4Z8e9dD|^gG&({2?MZfO1~v z@zyt4R@ds|n0Ij--|%=zegj^9vgpjnNu(|rev$;TZv?&(2W5u;jfcsd78a%``}@Gz zoKB(za{3vI6N7^b>dB;G0Ayy$mJcU_f7~TiMr;^(Fr4&^Qdc^G)U~^wtFLSC5^=fA;lRMVmcZb66~WRMl2tahGdy zSI!x1dURhK9GjJ?ur1=?!7^9#9)it5!rV_O9UDL4kjeU0lp^e-&((#ai_QeThn%MD zn?Gbf5`nU^gH9WK6>nD6udRKS?{! z6}Ivem%v)ofiP!$h>VO17Dbb@|6~@NrKKg=t5fXk7E^Let^l7UY^O$a*6XL=GmlW( z(cQp9GT28$*806ZV-cplYrEz8&o48}Mr*C`F*1I$E(^zCX>BK3r^6VasI7iPZ?TXe zsmHzax3eYhxZ@eH#fiSG+5WqW(2zTx*I;p3t%UlamdoE%-8z_#s z2pl^Jw=7N_S69uJS=hYh>Zugd)(wre=Wc0%)T3d6Cq}Y$tS@RruJlcw54ktPHE<$k$|XRjZ{84q@{{s_nc+|mWS=9& zIfZeT+nSDxi)+aci!(R3hA^oeBk}yeXWd8a$SgOWZdexc`^jsocXK{>w7@+4%s#tI zF+UfJc*;U1D*sizCr=ua3%C0WQHTBR@!;lcpa-hgfsuKO`&g5t{$g6z?b{Uf6HB*@ zxWgAh9ziQ+_R@(9?bgkkjZ=y(Zxmk)>)Y2lZlHhssZ$zVx@5N)vY|;4>(yh)n#To$ zGxHsdzB{)UZX_LNe}71;6{Tx)Zc5o^7mek1`ug#s>X5lM%4(rL1inKp0I#@Z zKvcgCq`$OGkP)G7T*{VM+o$z`)1AC&&IXvgIp7Z{4hIZU4f5`+1c zpshA%P6lLG=?9X|bSA6sj`9tn#?C*CAx9^P3@&)3-D&WW>P}*v(>o$21{=l~{$B37 znMG&Lqi9oI(;3;bBCy4%J!D~moDQ3Q>`bl&xv{Lfm(xbXgcQm@84IJ~|@RQST6DZvDx&{@2C=)Y))h2zbw z_Ts2Yk0I3h^gxJ0!W9xf)1t~~0T2=j&bbKrJ~Ewr)JOO3)e<8~yNSY*&%HJxfQlbO zP$@aG*!W;J+}z?h)R+nTc}FJc1(dpUj5L|-jV3zdG23@!xdG7;i3WzaNh>mMBUv33 zn@2ND-JJ9I!w~^^pjs#@B1~%fn+ofoDeZD{b5oqt9zK6=2JOapB-l8xwcqdF?bLlv z(45h)HJ5GcvS?7l{+}Mz9s72pOtb!Z(*2X^Ihym`yLO4r`6pja-`usfdZ;&fs5q0* z4bPdgYu`TixpNt9m0wj)*0H&{FI6_8Mk2J^w_koTEW4_8n>JC{I{WrjRa0XYOt9Hj zDrHfX4pIwOP9`nsIA>jBN+|3;-6A9BRf~1Mfdg4<@rCXHwW5DQ&s9K}T$r#8aXPZ} zekTKNRd{-PBP?cx5}8f+T4#J)?${zlt(T1P!=5pPUp8ryn%SleC@nbHIvaSH(EQ)2 zYyLS!Tl-DB`0d+mIjEv~jui^VYNa5ENZ}FnEj>z!d6QDuED_ zKIV@`r#Pbj=;QWd>h-sywnz%P~Owbr8(0ozA$mj>-?g#k?oW-O~4iWUZHA{)CbTeN9t%HHY;j9xMKV%icw45~J{46gcp z)}gioG18QL6yFnKw-qL#UT;E(MzfX!RYb)@v$~`{Z4pE?T<%+1@U@Sc_ zz%CENmNsuc3mc49CQTPVXVI4Y`J%oUw9vp{YI288LYUZiX?E^zx}mzVk~(n}TaA<# zJELwwn90tO(n78=AU5|Xn2Qgsfev9nVq*|49!p>=l<@gZ1FX=?f`FVC*X zQE1XI;RIFqNPP?R0y+cbgL5OZoL2C0n_7_4;0+HwijYY&YL?|Uc;2} zLa;3l9zC)%I`~l2pTg$+`@zRP7_!*7CMaIX=sbLf!=52X;?kha=-3g-%YlA4xQ6KI z1qKHLha$#8tQTL@se9-G;;2+qa?Bo4!4Xw)fyoWylJ0Xtx6n&bLI9fM9Z0_O^y$<4 z_aih8f%OpnNm~wa#SPA0$-MS|T7TR$kWE`D1lt`b8?K|12*uc>@sX{~^~r^#?4xoe zl>JB-I=bNt8#?wHcd3Tq3UgcAh1~MRj~4ZQ@PEB7;U;S`F}LZwM{z; zd_)Nv=`s;Y7i}L0w{o9V^gOwV$<*mZH%;Qs5AbhzM=ATd`Tmm=-8&HS6h%WwJrv)K}NP{ zP-N?RulMx)L+!m0{D&L`;BLjYPM#(%elpZ#Gf%`bfuQaZ6`8CSMZheH1Hbho=H(A9 z00n0Ln8xjMhAYIb+#a?O{Y@0VnrA?MCb$h!`C;qVDk?Fo%iRv^2aaoyhjpt~dEqPC zJ7^i7)M?YEX4q{Ox0Yzw|+Z{Uv|j%oB^4wM?xwi-|h9Pk6k$Pmj@> z`y6%?lw;QQ4I{L*Z|3KZpQ=3jY>d>U_QI2K435N^4H!UI=5Ww34uJqGgNox1wh0*@ zSTqOm>uGd^44r2d#t3pk(SSkdEIUh}*;?eM9NFw;vJs&f{=`wAStHUURC5SNEoDB6 z0kF>Fx{hb2pYZpWN}k<1JuL5~DU1>*P_e7VnUnLtD#^8hQd-N?C#CswT|!o}G%zdd z?c3T~4Q1uMQBgl=L;0q(@p7SclWs2CRksmP!UNABFXC$T!d9?3(!!MpdIC~sM%mkc zpu)MEmlxq-xOC_F(J>`lkVE#5gNKHo%*?dp({Z|V56ynL3c&~!BLi>1e^3RkX5#rE z+ztF9l7-3Z27GNA+@FdE%@N{G$|D-Fm&l}A|F}#1?0yX^4B#z6%sG6Vn~U**RtS3G z?_5&KWo3nJRqx;DyXpZfFq?yrxEr-9{SuQ$`R@i1ECDQs$U&Bh7k2;t{o}13_xiQ! zY*+vI*sR`R9y**C#ZUKM-Z+1rXG=&@8`(Sg`PYaNOiU1lpug!A(Wge&D25G3B&i6X z0fR6%!+H6Kj$OO%N5V~8Zfm4i-3_2ilqhp=*|yPMLpNh9gOsh)nUSyd?~z0Evue$n zJszFR>qZvK(Ud!%ox7cm0i+SssHeKZ`ACX-efevrp`CiQ~}i^NNnbJcgvwa^dY0(|LNT+jU_)o{)(@1K0-m za5$?9Qwk`Ct&z5HYy}~Ew%ZCGxV{@K1d>SY%2~*{%A!tZ{so1MyO}3tNjyplaT*(4 z8=gMjTyV^5V%2bdXWq;2KYoBeL7;qR;oD3QVq0liZ*~A#APbBrn3D>IkLaBM^m;Ap zyx|HAJChgQm0qglSy7K8N(&E>5w5g;p<8FmN?BiDIYw*o(qRjE!({U^xWPH?VzlJq z8@F!x8CC7>Gs)=HPG?a`!pRKj618{lt3S$^ShZ0h+7CWFyJk!%6h$BmJ_Is=4%OR) zDLgQiD9q%fY14)q7%=_8YR;Uvx{oCMZ|}R{%yhI7OKojs*Bk}&DXM7zu#NsM#-H7< z@n}A{it|ujx{3vei6I)pQNr_sA<5gA*bAXr+5A2A$43K;U z6kD%B@2ST7_wD=S`bJ}|7pQDqQdiAOdpgEKW`t**D|-l0(v#2qoyS2^0-uT2J!Ey9 z9>;xJn!`hE!fF2IQJ8Yf(V_%4wupm-Kf0$+-V z$0Im-apcma0ZLiinRa42-V0j{l92ef$((`1??-;yt$&ax*;g{KSjwRG7W4?0JF>muD(G3DUJ6Fo6qV=9_PpHa0mtAc%P21hz!KM3lM*6-B6v41B)NGEgWMqmfVZ)xdMh zIr&i7kEaYl!AJED($fG@MvlmxJKt$np|R;t9W8RW`KgnD!O+$70Q~ty%J(Iagdh{; zYydNn_G7tBJ z=ty97E18P)-ivMka`ADvA#w9FBdIO5YyF7vD^}E=mN@?$f4Yp8htWp96mW+p zI5K-q>e2A_0Pd(N;rdtexj`_gMTeV~qt?YCrmmzU4t1wF#GegfH7+yHpTeArUwl6B zve;mIi(QfC3*DlAy?bX9SI$o$ZoyiOG*~@d6rw!|`&KJ?j>Vy?J3+18-qe%Fk5?@R zlbEE&jTG!x(f*}k^Q=V9TL=s@rdPm41EjsGsyYW#p!Gf~@%ZsHdJGC{)!>WhQYnxT zGfkR8k%JO04$)yy7SP;|prFMEeZ&t2NCzy)lq`H~2$SZa-MT#3!vVAyzS)MQ%=|x6 zE9PD)`$9fVC)DFAh*)t za}%yFJK}CyOglL;ccQsRUiK^X@;5p=&QMD4&4`LQR7tdD(Ytq_CsSmT;KnJK zCCW%noR9{(TUKUhA>(6vTB4P7UDW6IrIGeY>7Ml#E1t8iNE)SaKHe-YCK`;&+rMZZ zLS^!vKHUg;PK6S)@yq_i9+HB2h<4V|Ih`o22=_uN^Nda_{SM9;=67lP_eVa01tfPa zV#Dz^sgQNdiq@J7Gr&SoGxBgBGbVu8G>&pvEs>nldckDcGpA3h40E3V1ztUBqOCZJ zo|9<4tE-}h#&cRYj%nhsTExT&e*%qntPw?8Y!3Q)1A{Ms`8OvWq;itZ?Ra5r0~kx5 zR5wE0PN-ao;@)VdZZwR9C2l+RQoKikv`{VNI3_-#*d-vA*&aT;4A3bCAn{B;E#}so zn&R|;(u>wz(SoT40g>d(?ai7nRzh>qi&EbA`>v~I zA)YK8v=s2r@I>wv;fA3U0)V9*r=de)BaSQ{(A|=tzm|tJTzJ4_?PX+QxAqVfAzKD$ zm~FcI9N_8%m8YKOMW`Z96~gf2XIreBI%NtiMqiv=tR0M0q#n$mwWqn>`F7VLQg){U zrL_FU+)kgFL_L6!6*p;bInWQUH@5kC5%tO#R-H@095NMIZ!GWBFjZLDH&SQqj)%djhKtpvOno(@cAvO3tat2`$ z5#)N*qDwy#7dMzo09s-1UITE7Wu}*`BbeYU>QY&7Y^X_}$^^ltGpIoBr@A`VGt=p3 z$YtOk_}HZkPu#pY1KN%^WY=zdJcK3+uy3%pTr%hP?tRD$Q`1<=DkZUs{-x_M_X4o7 zXPN<$Vw+XM??*w&xj|Q>p`+_O4mUC?N~$>bR+J;*j@<7|{-nv^2ae?rWg|B2*|u$4 z`N6kM+rP9lR<*at0W?$72u`lV*}JID80p>>mNY-w4T_6^xN$l=H z!u!jK#L%qmhzN;u!7Ys~3WSx;6Jtjxsg7i6CMGg4 zL%YUKs&O*kzaWb?ZNT)?H&8e{c|ycmN7)^_3+~_NS@Kc|-{GC-+`C8W6&jpb1EYK=JG8$j(B1~G7KB!F5bXR-FzojaBj7#lkdReD5l7BL`lXS6Nnft zFtDJoaF@q~x|WeX&1LBAbHcPh%@033f?|>r0v$dCCnpD2aIs{SIP*#QY{`-~ZQ6YE zq26#DsvvlD1$Ve|g#L*gE5iI~Y3wmi90t^v%?=9})?iIxd=LqGv#)RHW0>OmH(cK~ zrQ5U1VFxrd2Mh@AhkvB{<1!+q7irk2OoF#5vmGfx0e>O_#`T&sdbFRD%r%J&TO|o2eV{>SYxJ+2t27#f zG7jVuF(x>pbBMVY3VbCC(6edi*$x~SJ78JQz`uIIEx)anYKyD%KX3XvSWNFfm6;|{ z`A7EJwyC}(3$nbq!WRw~J5f=jvqOVXE;RmMOA|eUs!OKA~m>eA2ba7XeY-V zMaP*<|JF9UwNMbk z8?ZUW$-`|%cHSFG4k~3zFD?X7mI#kof}jpJ<2%fYZij1}4Sgsw61eXSv@JcIYcFr1 zdaC%Rh?5cUWYGbBz>vfv8;qfI-8wPd=@(#CAP71Ff09nU=T$Zx-9=AsSYRaVD>rYt z(Sk(u8qW(2_wW#eaQ|+tV-tXN#L1j=i5L|TL*(b>Va0|aFat#8aNw*=Y$YXx$+54V zh(lfunQlC=cdymUo^Wygio_mnf- z39CBw(dT}{r9u$xtQ>>$ZB}ZGp%OFBm>}7Q!qX8!#s)P(h;2JMMEmd`!;5VzN`-=+ zJjYzn6`&6KI{z?^ae38YHe<%jWAJRaLdpzI=IYK0OBG;SghXo9R1#2aYyy$(&{Duh zv(|>gzLVQ8-V%3qIYb-HBMJkQ@Y0l{!uxg*8$x0Z1kK!e^QdDJcma$>dy(7GAnMxO z+zxz+l^TYH@z7|}SzKZ-dE&%4 z$PUtLQ?JQr{Tnj?Kq=~?2yLdK;=L;3KB}y(b&%=Gzoq8*HU|wfsm>E*EhE}2pU5NN zJ+s|N-u3gl#rgMb3dGaCk=_Ooxrv4D^d9U&|FGNjfoqJak9cafCL5z}xz29u5_^fZZ`MOmiCI zmMODo`~Ll1eST1Muo+UB7Yke-n%%=6H-yruF`0J0%Opqam>g?V8dOvx|6~eK;lp4* z1=dZT<${-J$x!ry(}!jrnOi&?7Em<&f>QY zR(){NF=fWNt+{i@<^~XSJ+Vbmc#qFv8OakA%hYxYg2L{wFE>>dVf|EfKP z^FGDWZFc&hbUQy!rlJ zl*4<0d(zki4LO;VdhOp>_t*8F_&{jIQ2#LBpm)K7z2pInky*KB&C>ev@s%qKx&F}= zkqPzi;TJLO(DWSob|!R+ER(V_=23#)xqpJIl-_z2=m(ZAn7TE)%66DFjRO~^{O2f( zqRma)uq9J@rJ26KMlCwn`SBy#!b5$)`0@oKhTk?b@}(sx2QRN62|B<0hqE#zDYQ*rUfSKva-y)ggiqa}*Tm zpcO`=zBQL))67F8qob}~ZQ@iTFav-ChXa-{#%z#e)%!6#%si5)J+Djtf)qB}JFE!b zN{xVlZ2h+u`RPM}rY~HGMq>={LTPSnVvhK%-3#4|A0f3HUw%M|$7 zWjr1R=E!Jq88%s<0ZdHP$!*=*c>-1SxN$pa7pohM*q}|xDY&87JbLO>m+S3>7@3XpF)FTw!BD;= zQQlDmj^uScf>#i;yV{#+q; zMqvgIr<{5VkxAFm@MQ=C@qWfj2-T8oAsux|^DY^4hdjT!zK7dyG^v973})O+m5j7N zCPMl#OvG*v&7~)fyF8-(&RP`O{9k<(^R&y9o*1L3^!Xu1bPO4yqpoD$+mDwK`=0Q3 zbPK#vC@#bZ6q~(!C!s>2t}fW`It!^;Z|o;<@1a3K45sOnKH*Pg@u<9iRu=E{zbRF{ zL6knk?qMZR`?DWKP!sS~|7hofQx5YIuU}{Gl#jAxphU|Y);ZFfwIs;l@mWLjNRcqW zKDuk?&ItF3;w~9nA>q+ZTp^jFffOJo?eb-2#8Nw+e{70jnPZyUfQnFEvAcQX=5-?l zp|a*zqg6+~Hk6h~l#oHL6-;=>AA3&sgFLZJd`5Ez2~nIf3(kkW6S)xpXNX@ndi~U^;DM}RsoCxT#3>JWvsYh zrt!Ulg8bWtXUJ?4go=NxHn^gl)JjTfJ^tJq|K*DM@2>p+?Y8)z{mC~UMvV3FIDhDn zOVuh8zkGaJ6l7v=*>hwUq@FjmPNn>qHhUv2)hQYVr~@q5z&?lD^>}-g0~(y_SBMv~ z!9uG0wsLYh>cu`bKK^X)k13@~VHDDvJ_emSRSTEHo_c=Go4+{(~x+`-28|tkKbyux1$!J*S8S>{G}$G#RZfs&Qq5vQ&r8 z9~ULv>!PUkaT^39@+x`myqTZEw%@AA9KJlBGp5Z4F&7{!>nF4s3TFQ>+;SyPo?NUn zd~f}|2VYV%#rx6f@*A7+W*mr#9k%dqZ9!j;l`BEJ0!erF%RnH9{QeGM?<*tWWf7Y% zB3^UB2H;?vlKaQ?3bx{+bg;3}^cO>F{;4sr%aH)i1$aekIzFM0j2*E8EbF>?;vR4r(nZnuHTq3@l4W;&1mm-TgLUwR%u z;O(4u+}VL&=K_e(S>suu3*gy^)3(lk+6!A?(bS>qb-18Nl6QW1}}MXKDVGikJxl3cJd(UbA6;n@h9^IIWP#w-0USs_bHIE;*>wt$jkM9sgZ^5 zp|4R!ZG7!W>@W@N%(5&9jl7ozUFAPdGer%pe}{RUu~)8mO`ookPCi&tYPT$ExR!bv z6aO$!eiW;!!MD>lpmA&6R(aU7pH!e+2UPmVDty4HryNa5QCC&%+hmChkQ!{jGrYlF zHi={w`DouXSkgbpV9{5wt#i@uGRLonnO8%gA|-Cm$|#K%(vW==))<(2gH{G>qm!JU*2ByI-wkoVi#-O`3#Jkrn@V$yzS2 zQKK@j3aSq8KFmF=d?YTg<@JZ;X=Kv5v6xn~z1;fENxAfC!9A9AyGu1jOUK0HzrNt= zP7r59hn}Mr+r<5$5Rpq?o@6>PT8sMcvZTf6(Z`&OqMtSHySWbhqv8_w8FjRmyzUJf znxNBL-%@<(i{H5EQp^e5_N%9j)qi#T)TyMkO<-LRUdY;j21&B$J#Jq?nG%9w)*QSo z0jr=wwnLpgX0CKVKH>TsQ^#&wE53A*wX)f>BDG9<_^7hu7^&Plrqm1-vt?U;*80!{ zEjhdQAG#pthE_YSpzMEm>GAg zI0V-dtT|u8+R_pmjjTp<4>(ay3=npji@3O5IHW5HQI;x{O`SI0%4TEYv)T!0Vn=jS z_#9?+Li~7*9w}YD5w?D$V5Gn1jM!M8)di%0#tDIA;-bIs-&1jMa~3boBf+pN^|xiq z>^Ei%w{38y2Ky!KjzR%~e2D#b@08n_>7ceus34L76HIC=5pX8Py1#NB0cs;g>~EPH ze+GyN0oh&j#{4$;t~G1dz6w=wI`3WxJu0G~hOa%b5K-@Ae?{5=@P)x^Z$!keSC%*5?aWLTcF6v5M_3g& zG!Ulm+T}<%NNDyyYqTvCaw=AU4ijw(YmFNUI_$vPq2zVdfKAh;Bli%c#Mm5XGgF65 zs=N95YPE#a{kC915^M&RkPS^uF^_!IsLd!=w{5$WJF*@J{knCp zeZXn*uxtdB_0v)U^1+vWq4L-Hbx^)G{n|itOE75`$LtG>_9V7iXa^7sW%l^$`qiu3 zU3tOz;Ok*=YRf}H+I^@{*`qzxZAv?apIciYu8Tg!cRqVvN#R@s_Y%jzyIh@4OH)+g z5%yvI18&zDo*~+Y=*-%6H@S?Tl8bAvM}PGsZ}CyvD1ZN>8~BCY{eSg1|Jw%W&!7Bh ZHYr!`+{+~WG|CUbc8tB{NekC4{|k9CLwf)K diff --git a/docs/database/_system/diagrams/orphans/orphans.dot b/docs/database/_system/diagrams/orphans/orphans.dot index e41600f8d..5cc5bbf08 100644 --- a/docs/database/_system/diagrams/orphans/orphans.dot +++ b/docs/database/_system/diagrams/orphans/orphans.dot @@ -4,10 +4,13 @@ digraph "orphans" { label=< - - + + + + +
goose_db_version[table]
id
serial[10]
version_id
int8[19]
version_id
int8[19]
is_applied
bool[1]
tstamp
timestamp[29,6]
id
serial[10]
max_counter
numeric[0]
actual_counter
numeric[0]
terminated_at
timestamp[29,6]
< 0 0 >
> URL="tables/goose_db_version.html" diff --git a/docs/database/_system/diagrams/orphans/orphans.png b/docs/database/_system/diagrams/orphans/orphans.png index 7df36caf3d3b777554f1f1e5ba277592cc141d3a..fcd5e694884e1618e3fddab56e05ef77c0603ec8 100644 GIT binary patch literal 26264 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 Vb-ZQ{C-6X622WQ%mvv4FO#qpWSd;(& diff --git a/docs/database/_system/diagrams/tables/goose_db_version.1degree.dot b/docs/database/_system/diagrams/tables/goose_db_version.1degree.dot index 400a0af51..f885b618e 100644 --- a/docs/database/_system/diagrams/tables/goose_db_version.1degree.dot +++ b/docs/database/_system/diagrams/tables/goose_db_version.1degree.dot @@ -4,10 +4,13 @@ digraph "oneDegreeRelationshipsDiagram" { label=< - - + + + + +
goose_db_version[table]
id
serial[10]
version_id
int8[19]
version_id
int8[19]
is_applied
bool[1]
tstamp
timestamp[29,6]
id
serial[10]
max_counter
numeric[0]
actual_counter
numeric[0]
terminated_at
timestamp[29,6]
< 0 0 >
> URL="goose_db_version.html" diff --git a/docs/database/_system/diagrams/tables/goose_db_version.1degree.png b/docs/database/_system/diagrams/tables/goose_db_version.1degree.png index a0b0c9ea05eec73bd9353ea307055718af17f732..262a6a407beafc5404971ebf7c655ce0a3462999 100644 GIT binary patch literal 15800 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 G-1}eKG1!*? literal 11668 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 diff --git a/go.mod b/go.mod index 57cdb46a0..1088b5270 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 github.com/bluele/gcache v0.0.2 github.com/dop251/goja v0.0.0-20241009100908-5f46f2705ca3 - github.com/formancehq/go-libs/v2 v2.0.1-0.20241030165600-9060fd311289 + github.com/formancehq/go-libs/v2 v2.0.1-0.20241107141636-5509ff77294e github.com/formancehq/ledger/pkg/client v0.0.0-00010101000000-000000000000 github.com/go-chi/chi/v5 v5.1.0 github.com/go-chi/cors v1.2.1 @@ -86,7 +86,7 @@ require ( github.com/buger/jsonparser v1.1.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/containerd/continuity v0.4.3 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 // indirect github.com/docker/cli v27.3.1+incompatible // indirect diff --git a/go.sum b/go.sum index 7370be6e3..f9e76220b 100644 --- a/go.sum +++ b/go.sum @@ -104,12 +104,8 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241029111513-edb146ee0db7 h1:OZz4N9nIj814aIgpqIvojndtae+N9Vqj5MJgKPIzJ5Y= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241029111513-edb146ee0db7/go.mod h1:DTqSp28pYPZa4O1WrOg3kobhgTHdk9geGtxnws9EViM= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241030160027-898dbd1a42af h1:7POEnA2uHO+a8HNO+LGCuSVpBolflAFcNR0N1BhEuRA= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241030160027-898dbd1a42af/go.mod h1:DTqSp28pYPZa4O1WrOg3kobhgTHdk9geGtxnws9EViM= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241030165600-9060fd311289 h1:XPjN3V3ONd+rhoJN2Sv7aVa+4NZbEuWMM1HIfgm22NQ= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241030165600-9060fd311289/go.mod h1:DTqSp28pYPZa4O1WrOg3kobhgTHdk9geGtxnws9EViM= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241107141636-5509ff77294e h1:O7HXIkgcHTY81Ehoex+a2JpmKWP+Co6eYZdoHX0r96w= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241107141636-5509ff77294e/go.mod h1:DTqSp28pYPZa4O1WrOg3kobhgTHdk9geGtxnws9EViM= github.com/formancehq/numscript v0.0.9-0.20241009144012-1150c14a1417 h1:LOd5hxnXDIBcehFrpW1OnXk+VSs0yJXeu1iAOO+Hji4= github.com/formancehq/numscript v0.0.9-0.20241009144012-1150c14a1417/go.mod h1:btuSv05cYwi9BvLRxVs5zrunU+O1vTgigG1T6UsawcY= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= diff --git a/scripts/export-database-schema.sh b/scripts/export-database-schema.sh deleted file mode 100755 index 3f4e549a3..000000000 --- a/scripts/export-database-schema.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -echo "Creating PG server..." -postgresContainerID=$(docker run -d --rm -e POSTGRES_USER=root -e POSTGRES_PASSWORD=root -e POSTGRES_DB=formance --net=host postgres:15-alpine) -wait-for-it -w 127.0.0.1:5432 - -echo "Creating bucket..." -go run main.go buckets upgrade _default --postgres-uri "postgres://root:root@127.0.0.1:5432/formance?sslmode=disable" - -echo "Exporting schemas..." -docker run --rm -u root \ - -v ./docs/database:/output \ - --net=host \ - schemaspy/schemaspy:6.2.4 -u root -db formance -t pgsql11 -host 127.0.0.1 -port 5432 -p root -schemas _system,_default - -docker kill "$postgresContainerID" \ No newline at end of file diff --git a/test/rolling-upgrades/Earthfile b/test/rolling-upgrades/Earthfile new file mode 100644 index 000000000..4941c494b --- /dev/null +++ b/test/rolling-upgrades/Earthfile @@ -0,0 +1,118 @@ +VERSION 0.8 +PROJECT FormanceHQ/ledger + +IMPORT github.com/formancehq/earthly:tags/v0.17.1 AS core + +FROM core+base-image + +CACHE --sharing=shared --id go-mod-cache /go/pkg/mod +CACHE --sharing=shared --id go-cache /root/.cache/go-build + +image-test: + ARG REPOSITORY=ghcr.io + ARG tag=latest + FROM --pass-args ../../tools/generator+build-image + + DO --pass-args core+SAVE_IMAGE --COMPONENT=ledger-rolling-upgrade-test --REPOSITORY=${REPOSITORY} --TAG=$tag + +image-main: + ARG REPOSITORY=ghcr.io + BUILD --pass-args github.com/formancehq/ledger:main+build-image --tag=main + +image-current: + ARG REPOSITORY=ghcr.io + BUILD --pass-args ../..+build-image --tag=current + +sources: + FROM core+builder-image + WORKDIR /src + COPY go.* *.go . + + SAVE ARTIFACT /src + +tidy: + FROM +sources + CACHE --id go-mod-cache /go/pkg/mod + CACHE --id go-cache /root/.cache/go-build + RUN go mod tidy + + SAVE ARTIFACT go.mod AS LOCAL go.mod + SAVE ARTIFACT go.sum AS LOCAL go.sum + +cluster-create: + FROM core+builder-image + RUN apk update && \ + apk add --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community kubectl && \ + apk add --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community kustomize && \ + apk add helm git jq + RUN --secret KUBE_APISERVER kubectl config set clusters.default.server ${KUBE_APISERVER} + RUN kubectl config set clusters.default.insecure-skip-tls-verify true + RUN --secret KUBE_TOKEN kubectl config set-credentials default --token=${KUBE_TOKEN} + RUN kubectl config set-context default --cluster=default --user=default + RUN kubectl config use-context default + RUN apk update && apk add curl docker + ARG TARGETARCH + RUN curl -L -o vcluster "https://github.com/loft-sh/vcluster/releases/download/v0.20.4/vcluster-linux-${TARGETARCH}" + RUN install -c -m 0755 vcluster /usr/local/bin && rm -f vcluster + ARG CLUSTER_NAME=test + RUN --no-cache vcluster create $CLUSTER_NAME --connect=false --upgrade + +run: + ARG CLUSTER_NAME=test + WAIT + BUILD --pass-args +cluster-create + BUILD +image-test + BUILD +image-main + BUILD +image-current + END + + FROM --pass-args +cluster-create + RUN curl -fsSL https://get.pulumi.com | sh -s -- --version + ENV PATH=$PATH:/root/.pulumi/bin + + CACHE --id go-mod-cache /go/pkg/mod + CACHE --id go-cache /root/.cache/go-build + + WORKDIR /src/test/rolling-upgrades + COPY ../../deployments/pulumi+sources/src /src/deployments/pulumi + COPY ../../deployments/helm+sources/src /src/deployments/helm + COPY go.* *.go . + + ARG NO_CLEANUP=false + ARG NO_CLEANUP_ON_FAILURE=false + + RUN --secret PULUMI_ACCESS_TOKEN --secret GITHUB_TOKEN sh -c ' + echo "Connecting to VCluster..." + vcluster connect ${CLUSTER_NAME} --namespace vcluster-${CLUSTER_NAME} & + export KUBECONFIG=/root/.kube/config; + + echo "Waiting for VCluster to be ready..." + until kubectl get nodes; do sleep 1s; done; + + echo "Running test..." + go test \ + --test-image ghcr.io/formancehq/ledger-rolling-upgrade-test:latest \ + --latest-version main \ + --actual-version current \ + --project ledger \ + --stack-prefix-name $CLUSTER_NAME- \ + --no-cleanup=$NO_CLEANUP \ + --no-cleanup-on-failure=$NO_CLEANUP_ON_FAILURE; + ' + IF [ $NO_CLEANUP = "false" ] + RUN vcluster delete $CLUSTER_NAME --delete-namespace + END + +lint: + FROM +tidy + CACHE --id go-mod-cache /go/pkg/mod + CACHE --id go-cache /root/.cache/go-build + CACHE --id golangci-cache /root/.cache/golangci-lint + + RUN golangci-lint run --fix --build-tags it --timeout 5m + + SAVE ARTIFACT main_test.go AS LOCAL main_test.go + +pre-commit: + BUILD +tidy + BUILD +lint \ No newline at end of file diff --git a/test/rolling-upgrades/README.md b/test/rolling-upgrades/README.md new file mode 100644 index 000000000..00beb02a3 --- /dev/null +++ b/test/rolling-upgrades/README.md @@ -0,0 +1,55 @@ +# Rolling upgrade tests + +This directory contains tests for rolling upgrades on K8S. + +## Running the tests + +To run the tests, you need to have a K8S cluster running. You can use the `k3d` tool to create a local K8S cluster. + +You need also a Pulumi access token to run the tests. +You can create one by following the instructions [here](https://www.pulumi.com/docs/pulumi-cloud/access-management/access-tokens/). + +### Install k3d + +```bash +curl -s https://raw.githubusercontent.com/rancher/k3d/main/install.sh | bash +``` + +### Create a K8S cluster + +```bash +k3d cluster create +``` + +### Run the tests + +```bash +kubectl create serviceaccount testing +kubectl create clusterrolebinding testing --clusterrole=cluster-admin --serviceaccount=default:testing + +export KUBE_TOKEN=$(kubectl create token testing --duration=999999h) +export KUBE_APISERVER=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}') +export PULUMI_ACCESS_TOKEN= + +earthly --push --no-output +run \ + --KUBE_APISERVER=$KUBE_APISERVER \ + --KUBE_TOKEN=$KUBE_TOKEN \ + --PULUMI_ACCESS_TOKEN=$PULUMI_ACCESS_TOKEN +``` + +### Delete the K8S cluster + +```bash +k3d cluster delete +``` + +## Test description + +The test : +* creates a K8S deployment with a single replica of the server +* then create a test pod in charge of sending requests to the web server and checking if the response is ok. +* then updates the deployment with a new image and waits for the new pod to be ready. +* then checks if the test pod is still alive. If alive, it indicates no errors during the rolling upgrade. + +Under the hood, the test will create a [VCluster](https://www.vcluster.com/docs/get-started) on your k3d cluster. +This VCluster will be used to run the tests and simulate a real rolling upgrade on a K8S cluster. diff --git a/test/rolling-upgrades/go.mod b/test/rolling-upgrades/go.mod new file mode 100644 index 000000000..c9772f8e9 --- /dev/null +++ b/test/rolling-upgrades/go.mod @@ -0,0 +1,107 @@ +module github.com/formancehq/ledger/test/rolling-upgrades + +go 1.22.0 + +toolchain go1.23.2 + +replace github.com/formancehq/ledger/pkg/client => ../../pkg/client + +require ( + github.com/formancehq/go-libs/v2 v2.0.0 + github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.12.0 + github.com/pulumi/pulumi/sdk/v3 v3.117.0 + github.com/stretchr/testify v1.9.0 +) + +require ( + dario.cat/mergo v1.0.1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.0.0 // indirect + github.com/ThreeDotsLabs/watermill v1.3.7 // indirect + github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect + github.com/agext/levenshtein v1.2.3 // indirect + github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/blang/semver v3.5.1+incompatible // indirect + github.com/charmbracelet/bubbles v0.16.1 // indirect + github.com/charmbracelet/bubbletea v0.24.2 // indirect + github.com/charmbracelet/lipgloss v0.7.1 // indirect + github.com/cheggaaa/pb v1.0.29 // indirect + github.com/cloudflare/circl v1.3.7 // indirect + github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect + github.com/cyphar/filepath-securejoin v0.2.4 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/djherbis/times v1.5.0 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.5.0 // indirect + github.com/go-git/go-git/v5 v5.12.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/glog v1.2.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/hcl/v2 v2.17.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/lithammer/shortuuid/v3 v3.0.7 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mitchellh/go-ps v1.0.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/nxadm/tail v1.4.11 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/opentracing/basictracer-go v1.1.0 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/pgavlin/fx v0.1.6 // indirect + github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pkg/term v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 // indirect + github.com/pulumi/esc v0.6.2 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/rogpeppe/go-internal v1.11.0 // indirect + github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect + github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/skeema/knownhosts v1.2.2 // indirect + github.com/spf13/cast v1.4.1 // indirect + github.com/spf13/cobra v1.8.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/texttheater/golang-levenshtein v1.0.1 // indirect + github.com/tweekmonster/luser v0.0.0-20161003172636-3fa38070dbd7 // indirect + github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect + github.com/uber/jaeger-lib v2.4.1+incompatible // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/zclconf/go-cty v1.13.2 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.19.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect + google.golang.org/grpc v1.67.1 // indirect + google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + lukechampine.com/frand v1.4.2 // indirect +) diff --git a/test/rolling-upgrades/go.sum b/test/rolling-upgrades/go.sum new file mode 100644 index 000000000..f3e3da3ca --- /dev/null +++ b/test/rolling-upgrades/go.sum @@ -0,0 +1,337 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= +github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= +github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/ThreeDotsLabs/watermill v1.3.7 h1:NV0PSTmuACVEOV4dMxRnmGXrmbz8U83LENOvpHekN7o= +github.com/ThreeDotsLabs/watermill v1.3.7/go.mod h1:lBnrLbxOjeMRgcJbv+UiZr8Ylz8RkJ4m6i/VN/Nk+to= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= +github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= +github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= +github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= +github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= +github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY= +github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= +github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= +github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= +github.com/cheggaaa/pb v1.0.29 h1:FckUN5ngEk2LpvuG0fw1GEFx6LtyY2pWI/Z2QgCnEYo= +github.com/cheggaaa/pb v1.0.29/go.mod h1:W40334L7FMC5JKWldsTWbdGjLo0RxUKK73K+TuPxX30= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/djherbis/times v1.5.0 h1:79myA211VwPhFTqUk8xehWrsEO+zcIZj0zT8mXPVARU= +github.com/djherbis/times v1.5.0/go.mod h1:5q7FDLvbNg1L/KaBmPcWlVR9NmoKo3+ucqUA3ijQhA0= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/formancehq/go-libs/v2 v2.0.0 h1:lRa90iNlOgV/H44Q8+xDE5HWaaztg++NZMhPdBs6Es4= +github.com/formancehq/go-libs/v2 v2.0.0/go.mod h1:LgxayMN6wgAQbkB3ioBDTHOVMKp1rC6Q55M1CvG44xY= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= +github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= +github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY= +github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU= +github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/hcl/v2 v2.17.0 h1:z1XvSUyXd1HP10U4lrLg5e0JMVz6CPaJvAgxM0KNZVY= +github.com/hashicorp/hcl/v2 v2.17.0/go.mod h1:gJyW2PTShkJqQBKpAmPO3yxMxIuoXkOF2TpqXzrQyx4= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8= +github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= +github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= +github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= +github.com/opentracing/basictracer-go v1.1.0 h1:Oa1fTSBvAl8pa3U+IJYqrKm0NALwH9OsgwOqDv4xJW0= +github.com/opentracing/basictracer-go v1.1.0/go.mod h1:V2HZueSJEp879yv285Aap1BS69fQMD+MNP1mRs6mBQc= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/pgavlin/fx v0.1.6 h1:r9jEg69DhNoCd3Xh0+5mIbdbS3PqWrVWujkY76MFRTU= +github.com/pgavlin/fx v0.1.6/go.mod h1:KWZJ6fqBBSh8GxHYqwYCf3rYE7Gp2p0N8tJp8xv9u9M= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/term v1.1.0 h1:xIAAdCMh3QIAy+5FrE8Ad8XoDhEU4ufwbaSozViP9kk= +github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 h1:vkHw5I/plNdTr435cARxCW6q9gc0S/Yxz7Mkd38pOb0= +github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231/go.mod h1:murToZ2N9hNJzewjHBgfFdXhZKjY3z5cYC1VXk+lbFE= +github.com/pulumi/esc v0.6.2 h1:+z+l8cuwIauLSwXQS0uoI3rqB+YG4SzsZYtHfNoXBvw= +github.com/pulumi/esc v0.6.2/go.mod h1:jNnYNjzsOgVTjCp0LL24NsCk8ZJxq4IoLQdCT0X7l8k= +github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.12.0 h1:vG/22IHpYupt+ZD+KOnRo5PqIrhShYj2MGYeRz3RFGI= +github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.12.0/go.mod h1:WnJK/yelFkTPdsx7jZuUZixRunf+QQlgCwoRi1mVF3A= +github.com/pulumi/pulumi/sdk/v3 v3.117.0 h1:ImIsukZ2ZIYQG94uWdSZl9dJjJTosQSTsOQTauTNX7U= +github.com/pulumi/pulumi/sdk/v3 v3.117.0/go.mod h1:kNea72+FQk82OjZ3yEP4dl6nbAl2ngE8PDBc0iFAaHg= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI= +github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= +github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 h1:TToq11gyfNlrMFZiYujSekIsPd9AmsA2Bj/iv+s4JHE= +github.com/santhosh-tekuri/jsonschema/v5 v5.0.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= +github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= +github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/texttheater/golang-levenshtein v1.0.1 h1:+cRNoVrfiwufQPhoMzB6N0Yf/Mqajr6t1lOv8GyGE2U= +github.com/texttheater/golang-levenshtein v1.0.1/go.mod h1:PYAKrbF5sAiq9wd+H82hs7gNaen0CplQ9uvm6+enD/8= +github.com/tweekmonster/luser v0.0.0-20161003172636-3fa38070dbd7 h1:X9dsIWPuuEJlPX//UmRKophhOKCGXc46RVIGuttks68= +github.com/tweekmonster/luser v0.0.0-20161003172636-3fa38070dbd7/go.mod h1:UxoP3EypF8JfGEjAII8jx1q8rQyDnX8qdTCs/UQBVIE= +github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= +github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= +github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zclconf/go-cty v1.13.2 h1:4GvrUxe/QUDYuJKAav4EYqdM47/kZa672LwmXFmEKT0= +github.com/zclconf/go-cty v1.13.2/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +lukechampine.com/frand v1.4.2 h1:RzFIpOvkMXuPMBb9maa4ND4wjBn71E1Jpf8BzJHMaVw= +lukechampine.com/frand v1.4.2/go.mod h1:4S/TM2ZgrKejMcKMbeLjISpJMO+/eZ1zu3vYX9dtj3s= +pgregory.net/rapid v0.6.1 h1:4eyrDxyht86tT4Ztm+kvlyNBLIk071gR+ZQdhphc9dQ= +pgregory.net/rapid v0.6.1/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= diff --git a/test/rolling-upgrades/main_test.go b/test/rolling-upgrades/main_test.go new file mode 100644 index 000000000..ed89a6644 --- /dev/null +++ b/test/rolling-upgrades/main_test.go @@ -0,0 +1,255 @@ +package main + +import ( + "context" + "flag" + "fmt" + "github.com/formancehq/go-libs/v2/logging" + corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1" + "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v3" + metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/meta/v1" + "github.com/pulumi/pulumi/sdk/v3/go/auto" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi/config" + "github.com/stretchr/testify/require" + "testing" + "time" +) + +var ( + latestVersion = flag.String("latest-version", "latest", "The version to deploy first") + actualVersion = flag.String("actual-version", "latest", "The version to upgrade") + noCleanup = flag.Bool("no-cleanup", false, "Disable cleanup of created resources") + noCleanupOnFailure = flag.Bool("no-cleanup-on-failure", false, "Disable cleanup of created resources on failure") + projectName = flag.String("project", "", "Pulumi project") + stackPrefixName = flag.String("stack-prefix-name", "", "Pulumi stack prefix for names") + testImage = flag.String("test-image", "", "Test image") +) + +func TestK8SRollingUpgrades(t *testing.T) { + + flag.Parse() + + ctx := logging.TestingContext() + + testFailure := false + cleanup := func(stack auto.Stack) func() { + return func() { + if testFailure && *noCleanupOnFailure { + return + } + cleanup(ctx, stack) + } + } + + logging.FromContext(ctx).Info("Installing Postgres") + pgStack, err := auto.UpsertStackInlineSource(ctx, *stackPrefixName+"postgres", *projectName, deployPostgres) + require.NoError(t, err, "creating ledger stack") + t.Cleanup(cleanup(pgStack)) + + _, err = upAndPrintOutputs(ctx, pgStack) + require.NoError(t, err, "upping pg stack") + + ledgerStack, err := auto.UpsertStackLocalSource(ctx, *stackPrefixName+"ledger", "../../deployments/pulumi") + require.NoError(t, err, "creating ledger stack") + t.Cleanup(cleanup(ledgerStack)) + + pgStackOutputs, err := pgStack.Outputs(ctx) + require.NoError(t, err, "unable to extract pg stack outputs") + + err = ledgerStack.SetAllConfig( + ctx, + auto.ConfigMap{ + "version": auto.ConfigValue{Value: *latestVersion}, + "postgres.uri": auto.ConfigValue{ + Value: "postgres://ledger:ledger@" + pgStackOutputs["service-name"].Value.(string) + ".svc.cluster.local:5432/ledger?sslmode=disable", + }, + "debug": auto.ConfigValue{Value: "true"}, + "image.pullPolicy": auto.ConfigValue{Value: "Always"}, + "replicaCount": auto.ConfigValue{Value: "1"}, + }, + ) + require.NoError(t, err, "setting config on ledger stack") + + _, err = upAndPrintOutputs(ctx, ledgerStack) + require.NoError(t, err, "upping ledger stack first time") + + testStack, err := auto.UpsertStackInlineSource(ctx, *stackPrefixName+"test", *projectName, deployTest) + require.NoError(t, err, "creating test stack") + t.Cleanup(cleanup(testStack)) + + ledgerStackOutputs, err := ledgerStack.Outputs(ctx) + require.NoError(t, err, "unable to extract ledger stack outputs") + + ledgerURL := fmt.Sprintf( + "http://%s.%s.svc.cluster.local:%.0f", + ledgerStackOutputs["service-name"].Value, + ledgerStackOutputs["service-namespace"].Value, + ledgerStackOutputs["service-port"].Value, + ) + + err = testStack.SetAllConfig(ctx, auto.ConfigMap{ + "ledger-url": auto.ConfigValue{Value: ledgerURL}, + "image": auto.ConfigValue{Value: *testImage}, + }) + require.NoError(t, err, "setting config on test stack") + + _, err = testStack.Destroy(ctx) + require.NoError(t, err, "destroying test stack") + + _, err = upAndPrintOutputs(ctx, testStack) + require.NoError(t, err, "upping test stack") + + // Let a moment ensure the test image is actually sending requests + <-time.After(5 * time.Second) + + err = ledgerStack.SetConfig(ctx, "version", auto.ConfigValue{ + Value: *actualVersion, + }) + require.NoError(t, err, "setting version on ledger stack") + + _, err = upAndPrintOutputs(ctx, ledgerStack) + require.NoError(t, err, "upping ledger stack second time") + + testStackOutputs, err := testStack.Outputs(ctx) + require.NoError(t, err, "unable to extract test stack outputs") + + checkStack, err := auto.UpsertStackInlineSource( + ctx, + *stackPrefixName+"check", + *projectName, + func(ctx *pulumi.Context) error { + pod, err := corev1.GetPod(ctx, testStackOutputs["name"].Value.(string), pulumi.ID(testStackOutputs["id"].Value.(string)), nil) + if err != nil { + return err + } + + ctx.Export("phase", pod.Status.Phase().ApplyT(func(phase *string) string { + return *phase + })) + + return nil + }, + ) + require.NoError(t, err, "creating test stack") + t.Cleanup(cleanup(checkStack)) + + ret, err := upAndPrintOutputs(ctx, checkStack) + require.NoError(t, err, "upping check stack") + + require.False(t, ret.Outputs["phase"].Value.(string) == "Failed") +} + +func cleanup(ctx context.Context, stack auto.Stack) { + if *noCleanup { + return + } + + if _, err := stack.Destroy(ctx); err != nil { + logging.FromContext(ctx).Errorf("destroying stack: %v", err) + } +} + +func upAndPrintOutputs(ctx context.Context, stack auto.Stack) (auto.UpResult, error) { + out, err := stack.Up(ctx) + if out.StdErr != "" { + fmt.Println(out.StdErr) + } + if err != nil { + return auto.UpResult{}, fmt.Errorf("upping stack '%s': %w", stack.Name(), err) + } + + if out.StdOut != "" { + fmt.Println(out.StdOut) + } + + return out, nil +} + +func deployTest(ctx *pulumi.Context) error { + conf := config.New(ctx, "") + namespace, err := conf.Try("namespace") + if err != nil { + namespace = "default" + } + image := conf.Require("image") + ledgerURL := conf.Require("ledger-url") + + rel, err := corev1.NewPod( + ctx, + "test", + &corev1.PodArgs{ + Metadata: metav1.ObjectMetaArgs{ + Namespace: pulumi.String(namespace), + }, + Spec: corev1.PodSpecArgs{ + RestartPolicy: pulumi.String("Never"), + Containers: corev1.ContainerArray{ + corev1.ContainerArgs{ + Name: pulumi.String("test"), + Args: pulumi.StringArray{ + pulumi.String(ledgerURL), + pulumi.String("/examples/example1.js"), + pulumi.String("-p"), + pulumi.String("100"), + }, + Image: pulumi.String(image), + ImagePullPolicy: pulumi.String("Always"), + }, + }, + }, + }, + pulumi.Timeouts(&pulumi.CustomTimeouts{ + Create: "10s", + Update: "10s", + Delete: "10s", + }), + pulumi.DeleteBeforeReplace(true), + ) + if err != nil { + return err + } + + ctx.Export("name", rel.Metadata.Name()) + ctx.Export("id", rel.ID()) + + return nil +} + +func deployPostgres(ctx *pulumi.Context) error { + conf := config.New(ctx, "") + namespace, err := conf.Try("namespace") + if err != nil { + namespace = "default" + } + + rel, err := helm.NewRelease(ctx, "postgres", &helm.ReleaseArgs{ + Chart: pulumi.String("oci://registry-1.docker.io/bitnamicharts/postgresql"), + Version: pulumi.String("16.1.1"), + Namespace: pulumi.String(namespace), + Values: pulumi.Map(map[string]pulumi.Input{ + "auth": pulumi.Map{ + "password": pulumi.String("ledger"), + "username": pulumi.String("ledger"), + "database": pulumi.String("ledger"), + }, + }), + CreateNamespace: pulumi.BoolPtr(true), + }) + if err != nil { + return fmt.Errorf("installing release") + } + + svc := pulumi.All(rel.Status.Namespace(), rel.Status.Name()). + ApplyT(func(r any) string { + arr := r.([]interface{}) + namespace := arr[0].(*string) + name := arr[1].(*string) + + return fmt.Sprintf("%s-postgresql.%s", *name, *namespace) + }) + + ctx.Export("service-name", svc) + + return nil +} diff --git a/tools/generator/Earthfile b/tools/generator/Earthfile new file mode 100644 index 000000000..2111309b5 --- /dev/null +++ b/tools/generator/Earthfile @@ -0,0 +1,65 @@ +VERSION 0.8 +PROJECT FormanceHQ/ledger + +IMPORT github.com/formancehq/earthly:tags/v0.17.1 AS core + +FROM core+base-image + +CACHE --sharing=shared --id go-mod-cache /go/pkg/mod +CACHE --sharing=shared --id go-cache /root/.cache/go-build + +sources: + FROM core+builder-image + + COPY ../..+lint/pkg /src/pkg + COPY ../..+lint/internal /src/internal + COPY ../..+lint/cmd /src/cmd + COPY ../..+lint/*.go /src/ + COPY ../..+tidy/go.mod /src/ + COPY ../..+tidy/go.sum /src/ + + WORKDIR /src/tools/generator + COPY --dir cmd . + COPY go.* *.go . + + SAVE ARTIFACT /src + +tidy: + FROM +sources + CACHE --id go-mod-cache /go/pkg/mod + CACHE --id go-cache /root/.cache/go-build + RUN go mod tidy + + SAVE ARTIFACT go.mod AS LOCAL go.mod + SAVE ARTIFACT go.sum AS LOCAL go.sum + +compile: + FROM +tidy + CACHE --id go-mod-cache /go/pkg/mod + CACHE --id go-cache /root/.cache/go-build + RUN go build -o main + SAVE ARTIFACT main + +build-image: + FROM core+final-image + ENTRYPOINT ["/bin/ledger-generator"] + COPY --pass-args (+compile/main) /bin/ledger-generator + COPY examples /examples + ARG REPOSITORY=ghcr.io + ARG tag=latest + DO --pass-args core+SAVE_IMAGE --COMPONENT=ledger-generator --REPOSITORY=${REPOSITORY} --TAG=$tag + +lint: + FROM +tidy + CACHE --id go-mod-cache /go/pkg/mod + CACHE --id go-cache /root/.cache/go-build + CACHE --id golangci-cache /root/.cache/golangci-lint + + RUN golangci-lint run --fix --build-tags it --timeout 5m + + SAVE ARTIFACT cmd AS LOCAL cmd + SAVE ARTIFACT main.go AS LOCAL main.go + +pre-commit: + BUILD +tidy + BUILD +lint \ No newline at end of file diff --git a/tools/generator/cmd/root.go b/tools/generator/cmd/root.go index f82a37be0..da07b4fbf 100644 --- a/tools/generator/cmd/root.go +++ b/tools/generator/cmd/root.go @@ -11,21 +11,21 @@ import ( "github.com/formancehq/ledger/pkg/client/models/operations" "github.com/formancehq/ledger/pkg/client/models/sdkerrors" "github.com/formancehq/ledger/pkg/generate" + "github.com/spf13/cobra" "golang.org/x/oauth2" "golang.org/x/oauth2/clientcredentials" + "golang.org/x/sync/errgroup" "net/http" "os" - "sync" - - "github.com/spf13/cobra" ) var ( rootCmd = &cobra.Command{ - Use: "generator ", - Short: "Generate data for a ledger. WARNING: This is an experimental tool.", - RunE: run, - Args: cobra.ExactArgs(2), + Use: "generator ", + Short: "Generate data for a ledger. WARNING: This is an experimental tool.", + RunE: run, + Args: cobra.ExactArgs(2), + SilenceUsage: true, } parallelFlag = "parallel" ledgerFlag = "ledger" @@ -105,6 +105,7 @@ func run(cmd *cobra.Command, args []string) error { ledgerclient.WithClient(httpClient), ) + logging.FromContext(cmd.Context()).Infof("Creating ledger '%s' if not exists", ledger) _, err = client.Ledger.V2.CreateLedger(cmd.Context(), operations.V2CreateLedgerRequest{ Ledger: ledger, }) @@ -116,21 +117,29 @@ func run(cmd *cobra.Command, args []string) error { } } - wg := sync.WaitGroup{} - wg.Add(vus) + parallelContext, cancel := context.WithCancel(cmd.Context()) + defer cancel() + + errGroup, ctx := errgroup.WithContext(parallelContext) - for i := 0; i < vus; i++ { + logging.FromContext(cmd.Context()).Infof("Starting to generate data with %d vus", vus) + + for vu := 0; vu < vus; vu++ { generator, err := generate.NewGenerator(string(fileContent)) if err != nil { return fmt.Errorf("failed to create generator: %w", err) } - go func() { - defer wg.Done() + + errGroup.Go(func() error { + defer cancel() + + iteration := 0 for { - next := generator.Next(i) + logging.FromContext(ctx).Infof("Run iteration %d/%d", vu, iteration) + next := generator.Next(vu) tx, err := client.Ledger.V2.CreateTransaction( - cmd.Context(), + ctx, operations.V2CreateTransactionRequest{ Ledger: ledger, V2PostTransaction: components.V2PostTransaction{ @@ -142,19 +151,20 @@ func run(cmd *cobra.Command, args []string) error { }, ) if err != nil { - logging.FromContext(cmd.Context()).Errorf("Vu stopped with error: %s", err) - return + if errors.Is(err, context.Canceled) { + return nil + } + return fmt.Errorf("iteration %d/%d failed: %w", vu, iteration, err) } if untilTransactionID != 0 && tx.V2CreateTransactionResponse.Data.ID.Int64() >= untilTransactionID { - return + return nil } + iteration++ } - }() + }) } - wg.Wait() - - return nil + return errGroup.Wait() } func Execute() { diff --git a/tools/generator/go.mod b/tools/generator/go.mod new file mode 100644 index 000000000..e9c1d89bc --- /dev/null +++ b/tools/generator/go.mod @@ -0,0 +1,49 @@ +module github.com/formancehq/ledger/tools/generator + +go 1.22.1 + +toolchain go1.23.2 + +replace github.com/formancehq/ledger => ../.. + +require ( + github.com/formancehq/go-libs/v2 v2.0.1-0.20241107141636-5509ff77294e + github.com/formancehq/ledger v0.0.0-00010101000000-000000000000 + github.com/formancehq/ledger/pkg/client v0.0.0-20241104151610-5314238836a3 + github.com/spf13/cobra v1.8.1 + golang.org/x/oauth2 v0.23.0 + golang.org/x/sync v0.8.0 +) + +require ( + github.com/ThreeDotsLabs/watermill v1.3.7 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/dlclark/regexp2 v1.11.4 // indirect + github.com/dop251/goja v0.0.0-20241009100908-5f46f2705ca3 // indirect + github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect + github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/lithammer/shortuuid/v3 v3.0.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 // indirect + github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 // indirect + go.opentelemetry.io/otel v1.31.0 // indirect + go.opentelemetry.io/otel/log v0.7.0 // indirect + go.opentelemetry.io/otel/metric v1.31.0 // indirect + go.opentelemetry.io/otel/trace v1.31.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect +) diff --git a/tools/generator/go.sum b/tools/generator/go.sum new file mode 100644 index 000000000..c299c4c1e --- /dev/null +++ b/tools/generator/go.sum @@ -0,0 +1,115 @@ +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/ThreeDotsLabs/watermill v1.3.7 h1:NV0PSTmuACVEOV4dMxRnmGXrmbz8U83LENOvpHekN7o= +github.com/ThreeDotsLabs/watermill v1.3.7/go.mod h1:lBnrLbxOjeMRgcJbv+UiZr8Ylz8RkJ4m6i/VN/Nk+to= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= +github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dop251/goja v0.0.0-20241009100908-5f46f2705ca3 h1:MXsAuToxwsTn5BEEYm2DheqIiC4jWGmkEJ1uy+KFhvQ= +github.com/dop251/goja v0.0.0-20241009100908-5f46f2705ca3/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4= +github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731 h1:R/ZjJpjQKsZ6L/+Gf9WHbt31GG8NMVcpRqUE+1mMIyo= +github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731/go.mod h1:M9R1FoZ3y//hwwnJtO51ypFGwm8ZfpxPT/ZLtO1mcgQ= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241107140023-218febd1c023 h1:JWRNtxQs0IsxTK5wdrvIc9wi4Kscm9TNBOn2wuW9szU= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241107140023-218febd1c023/go.mod h1:DTqSp28pYPZa4O1WrOg3kobhgTHdk9geGtxnws9EViM= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241107141636-5509ff77294e/go.mod h1:DTqSp28pYPZa4O1WrOg3kobhgTHdk9geGtxnws9EViM= +github.com/formancehq/ledger/pkg/client v0.0.0-20241104151610-5314238836a3 h1:7Kyd9WIxBBRPk2DlBwhrPP5HM4l30y3zpk77Iss/jt0= +github.com/formancehq/ledger/pkg/client v0.0.0-20241104151610-5314238836a3/go.mod h1:BT02yUK6iBRyiodzWe7gyDtgKswtA/8tv824XqpuOcc= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 h1:c5FlPPgxOn7kJz3VoPLkQYQXGBS3EklQ4Zfi57uOuqQ= +github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8= +github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 h1:H8wwQwTe5sL6x30z71lUgNiwBdeCHQjrphCfLwqIHGo= +github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2/go.mod h1:/kR4beFhlz2g+V5ik8jW+3PMiMQAPt29y6K64NNY53c= +github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 h1:3/aHKUq7qaFMWxyQV0W2ryNgg8x8rVeKVA20KJUkfS0= +github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2/go.mod h1:Zit4b8AQXaXvA68+nzmbyDzqiyFRISyw1JiD5JqUBjw= +go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= +go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= +go.opentelemetry.io/otel/log v0.7.0 h1:d1abJc0b1QQZADKvfe9JqqrfmPYQCz2tUSO+0XZmuV4= +go.opentelemetry.io/otel/log v0.7.0/go.mod h1:2jf2z7uVfnzDNknKTO9G+ahcOAyWcp1fJmk/wJjULRo= +go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= +go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= +go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= +go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= +go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= +go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From ae7ec6d35e6eacb5acc6ad02aa615bfa887a99f9 Mon Sep 17 00:00:00 2001 From: Ragot Geoffrey Date: Fri, 8 Nov 2024 12:50:19 +0100 Subject: [PATCH 07/71] ci: run deployments tests only on PRs (#553) --- .github/workflows/main.yml | 2 +- test/rolling-upgrades/main_test.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 23968870a..52b6bf515 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -80,6 +80,7 @@ jobs: TestsDeployments: runs-on: "formance-runner" + if: github.event_name == 'pull_request' concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-deployments-tests cancel-in-progress: false @@ -117,7 +118,6 @@ jobs: KUBE_TOKEN: ${{ secrets.FORMANCE_DEV_KUBE_TOKEN }} PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }} - GoReleaser: runs-on: "formance-runner" if: contains(github.event.pull_request.labels.*.name, 'build-images') || github.ref == 'refs/heads/main' || github.event_name == 'merge_group' diff --git a/test/rolling-upgrades/main_test.go b/test/rolling-upgrades/main_test.go index ed89a6644..34010b445 100644 --- a/test/rolling-upgrades/main_test.go +++ b/test/rolling-upgrades/main_test.go @@ -137,7 +137,8 @@ func TestK8SRollingUpgrades(t *testing.T) { ret, err := upAndPrintOutputs(ctx, checkStack) require.NoError(t, err, "upping check stack") - require.False(t, ret.Outputs["phase"].Value.(string) == "Failed") + testFailure = ret.Outputs["phase"].Value.(string) == "Failed" + require.False(t, testFailure) } func cleanup(ctx context.Context, stack auto.Stack) { From 19ea7a6e05966dbb933cc602ad67f9f3550e40a9 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 8 Nov 2024 15:11:54 +0100 Subject: [PATCH 08/71] fix: rolling upgrade (bigger pg config) and add isolation --- cmd/serve.go | 2 +- test/rolling-upgrades/Earthfile | 22 ++++++++++++---------- test/rolling-upgrades/main_test.go | 10 +++++++++- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 620a8fd52..96788e230 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -100,7 +100,7 @@ func NewServeCommand() *cobra.Command { Handler chi.Router HealthController *health.HealthController - Logger logging.Logger + Logger logging.Logger MeterProvider *metric.MeterProvider `optional:"true"` Exporter *otlpmetrics.InMemoryExporter `optional:"true"` diff --git a/test/rolling-upgrades/Earthfile b/test/rolling-upgrades/Earthfile index 4941c494b..add728c26 100644 --- a/test/rolling-upgrades/Earthfile +++ b/test/rolling-upgrades/Earthfile @@ -10,18 +10,20 @@ CACHE --sharing=shared --id go-cache /root/.cache/go-build image-test: ARG REPOSITORY=ghcr.io - ARG tag=latest + ARG TAG=latest FROM --pass-args ../../tools/generator+build-image - DO --pass-args core+SAVE_IMAGE --COMPONENT=ledger-rolling-upgrade-test --REPOSITORY=${REPOSITORY} --TAG=$tag + DO --pass-args core+SAVE_IMAGE --COMPONENT=ledger --REPOSITORY=${REPOSITORY} --tag=$TAG image-main: ARG REPOSITORY=ghcr.io - BUILD --pass-args github.com/formancehq/ledger:main+build-image --tag=main + ARG TAG + BUILD --pass-args github.com/formancehq/ledger:main+build-image --tag=$TAG image-current: ARG REPOSITORY=ghcr.io - BUILD --pass-args ../..+build-image --tag=current + ARG TAG + BUILD --pass-args ../..+build-image --tag=$TAG sources: FROM core+builder-image @@ -61,9 +63,9 @@ run: ARG CLUSTER_NAME=test WAIT BUILD --pass-args +cluster-create - BUILD +image-test - BUILD +image-main - BUILD +image-current + BUILD +image-test --TAG=$CLUSTER_NAME-rolling-upgrade-test + BUILD +image-main --TAG=$CLUSTER_NAME-main + BUILD +image-current --TAG=$CLUSTER_NAME-current END FROM --pass-args +cluster-create @@ -91,9 +93,9 @@ run: echo "Running test..." go test \ - --test-image ghcr.io/formancehq/ledger-rolling-upgrade-test:latest \ - --latest-version main \ - --actual-version current \ + --test-image ghcr.io/formancehq/ledger:$CLUSTER_NAME-rolling-upgrade-test \ + --latest-version $CLUSTER_NAME-main \ + --actual-version $CLUSTER_NAME-current \ --project ledger \ --stack-prefix-name $CLUSTER_NAME- \ --no-cleanup=$NO_CLEANUP \ diff --git a/test/rolling-upgrades/main_test.go b/test/rolling-upgrades/main_test.go index 34010b445..e1d9d4d8f 100644 --- a/test/rolling-upgrades/main_test.go +++ b/test/rolling-upgrades/main_test.go @@ -192,7 +192,7 @@ func deployTest(ctx *pulumi.Context) error { pulumi.String(ledgerURL), pulumi.String("/examples/example1.js"), pulumi.String("-p"), - pulumi.String("100"), + pulumi.String("30"), }, Image: pulumi.String(image), ImagePullPolicy: pulumi.String("Always"), @@ -234,6 +234,14 @@ func deployPostgres(ctx *pulumi.Context) error { "username": pulumi.String("ledger"), "database": pulumi.String("ledger"), }, + "primary": pulumi.Map{ + "resources": pulumi.Map{ + "requests": pulumi.Map{ + "memory": pulumi.String("256Mi"), + "cpu": pulumi.String("250m"), + }, + }, + }, }), CreateNamespace: pulumi.BoolPtr(true), }) From b188d0c80eadaab5024d74edc967c7005e155f7c Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 8 Nov 2024 16:49:44 +0100 Subject: [PATCH 09/71] feat: delete stacks after test --- test/rolling-upgrades/main_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/rolling-upgrades/main_test.go b/test/rolling-upgrades/main_test.go index e1d9d4d8f..b2b98339e 100644 --- a/test/rolling-upgrades/main_test.go +++ b/test/rolling-upgrades/main_test.go @@ -146,9 +146,15 @@ func cleanup(ctx context.Context, stack auto.Stack) { return } + fmt.Printf("Destroying stack '%s'...\r\n", stack.Name()) if _, err := stack.Destroy(ctx); err != nil { logging.FromContext(ctx).Errorf("destroying stack: %v", err) } + + fmt.Printf("Removing stack '%s'...\r\n", stack.Name()) + if err := stack.Workspace().RemoveStack(ctx, stack.Name()); err != nil { + logging.FromContext(ctx).Errorf("removing stack: %v", err) + } } func upAndPrintOutputs(ctx context.Context, stack auto.Stack) (auto.UpResult, error) { From 47f3206a68a9ec348f0be63262011670d96fb925 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 13 Nov 2024 13:10:42 +0100 Subject: [PATCH 10/71] test: add test on 'query' query param --- docs/api/README.md | 4 ++ openapi.yaml | 32 +++++++++++ openapi/v2.yaml | 32 +++++++++++ pkg/client/.speakeasy/gen.lock | 6 +- pkg/client/.speakeasy/gen.yaml | 2 +- .../operations/v2countaccountsrequest.md | 11 ++-- .../operations/v2counttransactionsrequest.md | 11 ++-- .../operations/v2listaccountsrequest.md | 1 + .../operations/v2listtransactionsrequest.md | 1 + pkg/client/docs/sdks/v2/README.md | 4 ++ pkg/client/formance.go | 4 +- .../models/operations/v2countaccounts.go | 13 ++++- .../models/operations/v2counttransactions.go | 13 ++++- .../models/operations/v2listaccounts.go | 15 ++++- .../models/operations/v2listtransactions.go | 9 +++ test/e2e/api_accounts_list_test.go | 55 +++++++++++++++++++ 16 files changed, 190 insertions(+), 23 deletions(-) diff --git a/docs/api/README.md b/docs/api/README.md index 2af312ed1..7e5da659d 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -592,6 +592,7 @@ Accept: application/json |---|---|---|---|---| |ledger|path|string|true|Name of the ledger.| |pit|query|string(date-time)|false|none| +|query|query|string|false|Query string to filter accounts. The query string must be a valid JSON object.| |body|body|object|false|none| > Example responses @@ -657,6 +658,7 @@ List accounts from a ledger, sorted by address in descending order. |cursor|query|string|false|Parameter used in pagination requests. Maximum page size is set to 15.| |expand|query|string|false|none| |pit|query|string(date-time)|false|none| +|query|query|string|false|Query string to filter accounts. The query string must be a valid JSON object.| |body|body|object|false|none| #### Detailed descriptions @@ -1004,6 +1006,7 @@ Accept: application/json |---|---|---|---|---| |ledger|path|string|true|Name of the ledger.| |pit|query|string(date-time)|false|none| +|query|query|string|false|Query string to filter transactions. The query string must be a valid JSON object.| |body|body|object|false|none| > Example responses @@ -1065,6 +1068,7 @@ List transactions from a ledger, sorted by id in descending order. |Name|In|Type|Required|Description| |---|---|---|---|---| |ledger|path|string|true|Name of the ledger.| +|query|query|string|false|Query string to filter transactions. The query string must be a valid JSON object.| |pageSize|query|integer(int64)|false|The maximum number of results to return per page.| |cursor|query|string|false|Parameter used in pagination requests. Maximum page size is set to 15.| |expand|query|string|false|none| diff --git a/openapi.yaml b/openapi.yaml index 5661d768a..325d63003 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1472,6 +1472,14 @@ paths: schema: type: string format: date-time + - name: query + in: query + description: Query string to filter accounts. The query string must be a valid JSON object. + schema: + type: string + example: + $match: + address: users:001 requestBody: content: application/json: @@ -1543,6 +1551,14 @@ paths: schema: type: string format: date-time + - name: query + in: query + description: Query string to filter accounts. The query string must be a valid JSON object. + schema: + type: string + example: + $match: + address: users:001 requestBody: content: application/json: @@ -1772,6 +1788,14 @@ paths: schema: type: string format: date-time + - name: query + in: query + description: Query string to filter transactions. The query string must be a valid JSON object. + schema: + type: string + example: + $match: + account: users:001 requestBody: content: application/json: @@ -1811,6 +1835,14 @@ paths: schema: type: string example: ledger001 + - name: query + in: query + description: Query string to filter transactions. The query string must be a valid JSON object. + schema: + type: string + example: + $match: + account: users:001 - name: pageSize in: query description: | diff --git a/openapi/v2.yaml b/openapi/v2.yaml index 2bfa4ec2d..b7cb740f0 100644 --- a/openapi/v2.yaml +++ b/openapi/v2.yaml @@ -303,6 +303,14 @@ paths: schema: type: string format: date-time + - name: query + in: query + description: Query string to filter accounts. The query string must be a valid JSON object. + schema: + type: string + example: + "$match": + "address": "users:001" requestBody: content: application/json: @@ -378,6 +386,14 @@ paths: schema: type: string format: date-time + - name: query + in: query + description: Query string to filter accounts. The query string must be a valid JSON object. + schema: + type: string + example: + "$match": + "address": "users:001" requestBody: content: application/json: @@ -618,6 +634,14 @@ paths: schema: type: string format: date-time + - name: query + in: query + description: Query string to filter transactions. The query string must be a valid JSON object. + schema: + type: string + example: + "$match": + "account": "users:001" requestBody: content: application/json: @@ -657,6 +681,14 @@ paths: schema: type: string example: ledger001 + - name: query + in: query + description: Query string to filter transactions. The query string must be a valid JSON object. + schema: + type: string + example: + "$match": + "account": "users:001" - name: pageSize in: query description: | diff --git a/pkg/client/.speakeasy/gen.lock b/pkg/client/.speakeasy/gen.lock index e108584fd..0474584bd 100644 --- a/pkg/client/.speakeasy/gen.lock +++ b/pkg/client/.speakeasy/gen.lock @@ -1,12 +1,12 @@ lockVersion: 2.0.0 id: a9ac79e1-e429-4ee3-96c4-ec973f19bec3 management: - docChecksum: 46d538f65c61649e934b6991843b1e67 + docChecksum: b360e18f2833b14257df2742a09a1999 docVersion: v1 speakeasyVersion: 1.351.0 generationVersion: 2.384.1 - releaseVersion: 0.4.21 - configChecksum: 2dae5ea4cfeda429fc8fea96da1ab7b6 + releaseVersion: 0.4.27 + configChecksum: 9a9eff6bf575499aecf0bdcfa3f6d80b features: go: additionalDependencies: 0.1.0 diff --git a/pkg/client/.speakeasy/gen.yaml b/pkg/client/.speakeasy/gen.yaml index fbf4e6602..1cbee3f5c 100644 --- a/pkg/client/.speakeasy/gen.yaml +++ b/pkg/client/.speakeasy/gen.yaml @@ -15,7 +15,7 @@ generation: auth: oAuth2ClientCredentialsEnabled: true go: - version: 0.4.21 + version: 0.4.27 additionalDependencies: {} allowUnknownFieldsInWeakUnions: false clientServerStatusCodesAsErrors: true diff --git a/pkg/client/docs/models/operations/v2countaccountsrequest.md b/pkg/client/docs/models/operations/v2countaccountsrequest.md index fdc614bbe..dc139fe4e 100644 --- a/pkg/client/docs/models/operations/v2countaccountsrequest.md +++ b/pkg/client/docs/models/operations/v2countaccountsrequest.md @@ -3,8 +3,9 @@ ## Fields -| Field | Type | Required | Description | Example | -| ------------------------------------------ | ------------------------------------------ | ------------------------------------------ | ------------------------------------------ | ------------------------------------------ | -| `Ledger` | *string* | :heavy_check_mark: | Name of the ledger. | ledger001 | -| `Pit` | [*time.Time](https://pkg.go.dev/time#Time) | :heavy_minus_sign: | N/A | | -| `RequestBody` | map[string]*any* | :heavy_minus_sign: | N/A | | \ No newline at end of file +| Field | Type | Required | Description | Example | +| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | +| `Ledger` | *string* | :heavy_check_mark: | Name of the ledger. | ledger001 | +| `Pit` | [*time.Time](https://pkg.go.dev/time#Time) | :heavy_minus_sign: | N/A | | +| `Query` | **string* | :heavy_minus_sign: | Query string to filter accounts. The query string must be a valid JSON object. | {
"$match": {
"address": "users:001"
}
} | +| `RequestBody` | map[string]*any* | :heavy_minus_sign: | N/A | | \ No newline at end of file diff --git a/pkg/client/docs/models/operations/v2counttransactionsrequest.md b/pkg/client/docs/models/operations/v2counttransactionsrequest.md index 9e4c5cd88..17625a992 100644 --- a/pkg/client/docs/models/operations/v2counttransactionsrequest.md +++ b/pkg/client/docs/models/operations/v2counttransactionsrequest.md @@ -3,8 +3,9 @@ ## Fields -| Field | Type | Required | Description | Example | -| ------------------------------------------ | ------------------------------------------ | ------------------------------------------ | ------------------------------------------ | ------------------------------------------ | -| `Ledger` | *string* | :heavy_check_mark: | Name of the ledger. | ledger001 | -| `Pit` | [*time.Time](https://pkg.go.dev/time#Time) | :heavy_minus_sign: | N/A | | -| `RequestBody` | map[string]*any* | :heavy_minus_sign: | N/A | | \ No newline at end of file +| Field | Type | Required | Description | Example | +| ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | +| `Ledger` | *string* | :heavy_check_mark: | Name of the ledger. | ledger001 | +| `Pit` | [*time.Time](https://pkg.go.dev/time#Time) | :heavy_minus_sign: | N/A | | +| `Query` | **string* | :heavy_minus_sign: | Query string to filter transactions. The query string must be a valid JSON object. | {
"$match": {
"account": "users:001"
}
} | +| `RequestBody` | map[string]*any* | :heavy_minus_sign: | N/A | | \ No newline at end of file diff --git a/pkg/client/docs/models/operations/v2listaccountsrequest.md b/pkg/client/docs/models/operations/v2listaccountsrequest.md index a55a6bf00..b2c76f3ab 100644 --- a/pkg/client/docs/models/operations/v2listaccountsrequest.md +++ b/pkg/client/docs/models/operations/v2listaccountsrequest.md @@ -10,4 +10,5 @@ | `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 | | +| `Query` | **string* | :heavy_minus_sign: | Query string to filter accounts. The query string must be a valid JSON object. | {
"$match": {
"address": "users:001"
}
} | | `RequestBody` | map[string]*any* | :heavy_minus_sign: | N/A | | \ 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 eb6f8c5ea..1ad961552 100644 --- a/pkg/client/docs/models/operations/v2listtransactionsrequest.md +++ b/pkg/client/docs/models/operations/v2listtransactionsrequest.md @@ -6,6 +6,7 @@ | Field | Type | Required | Description | Example | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Ledger` | *string* | :heavy_check_mark: | Name of the ledger. | ledger001 | +| `Query` | **string* | :heavy_minus_sign: | Query string to filter transactions. The query string must be a valid JSON object. | {
"$match": {
"account": "users:001"
}
} | | `PageSize` | **int64* | :heavy_minus_sign: | The maximum number of results to return per page.
| 100 | | `Cursor` | **string* | :heavy_minus_sign: | Parameter used in pagination requests. Maximum page size is set to 15.
Set to the value of next for the next page of results.
Set to the value of previous for the previous page of results.
No other parameters can be set when this parameter is set.
| aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ== | | `Expand` | **string* | :heavy_minus_sign: | N/A | | diff --git a/pkg/client/docs/sdks/v2/README.md b/pkg/client/docs/sdks/v2/README.md index 9292a94ed..04807c180 100644 --- a/pkg/client/docs/sdks/v2/README.md +++ b/pkg/client/docs/sdks/v2/README.md @@ -485,6 +485,7 @@ func main() { ) request := operations.V2CountAccountsRequest{ Ledger: "ledger001", + Query: client.String("{\"$match\":{\"address\":\"users:001\"}}"), } ctx := context.Background() res, err := s.Ledger.V2.CountAccounts(ctx, request) @@ -542,6 +543,7 @@ func main() { Ledger: "ledger001", PageSize: client.Int64(100), Cursor: client.String("aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ=="), + Query: client.String("{\"$match\":{\"address\":\"users:001\"}}"), } ctx := context.Background() res, err := s.Ledger.V2.ListAccounts(ctx, request) @@ -826,6 +828,7 @@ func main() { ) request := operations.V2CountTransactionsRequest{ Ledger: "ledger001", + Query: client.String("{\"$match\":{\"account\":\"users:001\"}}"), } ctx := context.Background() res, err := s.Ledger.V2.CountTransactions(ctx, request) @@ -881,6 +884,7 @@ func main() { ) request := operations.V2ListTransactionsRequest{ Ledger: "ledger001", + Query: client.String("{\"$match\":{\"account\":\"users:001\"}}"), PageSize: client.Int64(100), Cursor: client.String("aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ=="), } diff --git a/pkg/client/formance.go b/pkg/client/formance.go index 67588f42a..9246adb7e 100644 --- a/pkg/client/formance.go +++ b/pkg/client/formance.go @@ -143,9 +143,9 @@ func New(opts ...SDKOption) *Formance { sdkConfiguration: sdkConfiguration{ Language: "go", OpenAPIDocVersion: "v1", - SDKVersion: "0.4.21", + SDKVersion: "0.4.27", GenVersion: "2.384.1", - UserAgent: "speakeasy-sdk/go 0.4.21 2.384.1 v1 github.com/formancehq/ledger/pkg/client", + UserAgent: "speakeasy-sdk/go 0.4.27 2.384.1 v1 github.com/formancehq/ledger/pkg/client", Hooks: hooks.New(), }, } diff --git a/pkg/client/models/operations/v2countaccounts.go b/pkg/client/models/operations/v2countaccounts.go index d5aa0588d..a350b8c91 100644 --- a/pkg/client/models/operations/v2countaccounts.go +++ b/pkg/client/models/operations/v2countaccounts.go @@ -10,8 +10,10 @@ import ( type V2CountAccountsRequest struct { // Name of the ledger. - Ledger string `pathParam:"style=simple,explode=false,name=ledger"` - Pit *time.Time `queryParam:"style=form,explode=true,name=pit"` + Ledger string `pathParam:"style=simple,explode=false,name=ledger"` + Pit *time.Time `queryParam:"style=form,explode=true,name=pit"` + // Query string to filter accounts. The query string must be a valid JSON object. + Query *string `queryParam:"style=form,explode=true,name=query"` RequestBody map[string]any `request:"mediaType=application/json"` } @@ -40,6 +42,13 @@ func (o *V2CountAccountsRequest) GetPit() *time.Time { return o.Pit } +func (o *V2CountAccountsRequest) GetQuery() *string { + if o == nil { + return nil + } + return o.Query +} + func (o *V2CountAccountsRequest) GetRequestBody() map[string]any { if o == nil { return nil diff --git a/pkg/client/models/operations/v2counttransactions.go b/pkg/client/models/operations/v2counttransactions.go index f6dfc324f..c37ed0a65 100644 --- a/pkg/client/models/operations/v2counttransactions.go +++ b/pkg/client/models/operations/v2counttransactions.go @@ -10,8 +10,10 @@ import ( type V2CountTransactionsRequest struct { // Name of the ledger. - Ledger string `pathParam:"style=simple,explode=false,name=ledger"` - Pit *time.Time `queryParam:"style=form,explode=true,name=pit"` + Ledger string `pathParam:"style=simple,explode=false,name=ledger"` + Pit *time.Time `queryParam:"style=form,explode=true,name=pit"` + // Query string to filter transactions. The query string must be a valid JSON object. + Query *string `queryParam:"style=form,explode=true,name=query"` RequestBody map[string]any `request:"mediaType=application/json"` } @@ -40,6 +42,13 @@ func (o *V2CountTransactionsRequest) GetPit() *time.Time { return o.Pit } +func (o *V2CountTransactionsRequest) GetQuery() *string { + if o == nil { + return nil + } + return o.Query +} + func (o *V2CountTransactionsRequest) GetRequestBody() map[string]any { if o == nil { return nil diff --git a/pkg/client/models/operations/v2listaccounts.go b/pkg/client/models/operations/v2listaccounts.go index 899d40a7b..85dd6ec8a 100644 --- a/pkg/client/models/operations/v2listaccounts.go +++ b/pkg/client/models/operations/v2listaccounts.go @@ -19,9 +19,11 @@ type V2ListAccountsRequest struct { // Set to the value of previous for the previous page of results. // No other parameters can be set when this parameter is set. // - Cursor *string `queryParam:"style=form,explode=true,name=cursor"` - Expand *string `queryParam:"style=form,explode=true,name=expand"` - Pit *time.Time `queryParam:"style=form,explode=true,name=pit"` + Cursor *string `queryParam:"style=form,explode=true,name=cursor"` + Expand *string `queryParam:"style=form,explode=true,name=expand"` + Pit *time.Time `queryParam:"style=form,explode=true,name=pit"` + // Query string to filter accounts. The query string must be a valid JSON object. + Query *string `queryParam:"style=form,explode=true,name=query"` RequestBody map[string]any `request:"mediaType=application/json"` } @@ -71,6 +73,13 @@ func (o *V2ListAccountsRequest) GetPit() *time.Time { return o.Pit } +func (o *V2ListAccountsRequest) GetQuery() *string { + if o == nil { + return nil + } + return o.Query +} + func (o *V2ListAccountsRequest) GetRequestBody() map[string]any { if o == nil { return nil diff --git a/pkg/client/models/operations/v2listtransactions.go b/pkg/client/models/operations/v2listtransactions.go index 88a233ac3..4030fdab9 100644 --- a/pkg/client/models/operations/v2listtransactions.go +++ b/pkg/client/models/operations/v2listtransactions.go @@ -36,6 +36,8 @@ func (e *Order) UnmarshalJSON(data []byte) error { type V2ListTransactionsRequest struct { // Name of the ledger. Ledger string `pathParam:"style=simple,explode=false,name=ledger"` + // Query string to filter transactions. The query string must be a valid JSON object. + Query *string `queryParam:"style=form,explode=true,name=query"` // The maximum number of results to return per page. // PageSize *int64 `queryParam:"style=form,explode=true,name=pageSize"` @@ -70,6 +72,13 @@ func (o *V2ListTransactionsRequest) GetLedger() string { return o.Ledger } +func (o *V2ListTransactionsRequest) GetQuery() *string { + if o == nil { + return nil + } + return o.Query +} + func (o *V2ListTransactionsRequest) GetPageSize() *int64 { if o == nil { return nil diff --git a/test/e2e/api_accounts_list_test.go b/test/e2e/api_accounts_list_test.go index bbfaedcfb..0853e0132 100644 --- a/test/e2e/api_accounts_list_test.go +++ b/test/e2e/api_accounts_list_test.go @@ -3,6 +3,7 @@ package test_suite import ( + "encoding/json" "fmt" "github.com/formancehq/go-libs/v2/logging" . "github.com/formancehq/go-libs/v2/testing/api" @@ -209,6 +210,60 @@ var _ = Context("Ledger accounts list API tests", func() { Metadata: metadata1, })) }) + It("should be listed on api using address filters on query param", func() { + + filtersAsJSON, err := json.Marshal(map[string]interface{}{ + "$match": map[string]any{ + "address": "foo:", + }, + }) + Expect(err).To(Succeed()) + + response, err := ListAccounts( + ctx, + testServer.GetValue(), + operations.V2ListAccountsRequest{ + Ledger: "default", + Query: pointer.For(string(filtersAsJSON)), + }, + ) + Expect(err).ToNot(HaveOccurred()) + + accountsCursorResponse := response.Data + Expect(accountsCursorResponse).To(HaveLen(2)) + Expect(accountsCursorResponse[0]).To(Equal(components.V2Account{ + Address: "foo:bar", + Metadata: metadata2, + })) + Expect(accountsCursorResponse[1]).To(Equal(components.V2Account{ + Address: "foo:foo", + Metadata: metadata1, + })) + + filtersAsJSON, err = json.Marshal(map[string]interface{}{ + "$match": map[string]any{ + "address": ":foo", + }, + }) + Expect(err).To(Succeed()) + + response, err = ListAccounts( + ctx, + testServer.GetValue(), + operations.V2ListAccountsRequest{ + Ledger: "default", + Query: pointer.For(string(filtersAsJSON)), + }, + ) + Expect(err).ToNot(HaveOccurred()) + + accountsCursorResponse = response.Data + Expect(accountsCursorResponse).To(HaveLen(1)) + Expect(accountsCursorResponse[0]).To(Equal(components.V2Account{ + Address: "foo:foo", + Metadata: metadata1, + })) + }) It("should be listed on api using metadata filters", func() { response, err := ListAccounts( ctx, From 61b2c6324fd5b75f4c95112adb6d4bbdb8e76adc Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 13 Nov 2024 14:29:03 +0100 Subject: [PATCH 11/71] feat: add query params on traces when debug is enabled --- internal/api/router.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/internal/api/router.go b/internal/api/router.go index a9025f2ca..a8407ac79 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -3,6 +3,7 @@ package api import ( "github.com/formancehq/go-libs/v2/api" "github.com/formancehq/ledger/internal/controller/system" + "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" nooptracer "go.opentelemetry.io/otel/trace/noop" "net/http" @@ -47,6 +48,17 @@ func NewRouter( middleware.RequestLogger(api.NewLogFormatter()), } + if debug { + commonMiddlewares = append(commonMiddlewares, func(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + trace.SpanFromContext(r.Context()). + SetAttributes(attribute.String("raw-query", r.URL.RawQuery)) + + handler.ServeHTTP(w, r) + }) + }) + } + v2Router := v2.NewRouter( systemController, authenticator, From 391d894243389886268e81955064a9b4959a0e9e Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 13 Nov 2024 11:01:46 +0100 Subject: [PATCH 12/71] feat: improve generator package by allowing to pass any write action on the ledger --- docs/api/README.md | 16 +- .../common/mocks_ledger_controller_test.go | 46 +- .../v1/controllers_accounts_add_metadata.go | 2 +- .../controllers_accounts_add_metadata_test.go | 3 +- .../controllers_accounts_delete_metadata.go | 2 +- ...ntrollers_accounts_delete_metadata_test.go | 3 +- .../controllers_transactions_add_metadata.go | 2 +- ...trollers_transactions_add_metadata_test.go | 3 +- .../api/v1/controllers_transactions_create.go | 4 +- .../controllers_transactions_create_test.go | 25 +- ...ontrollers_transactions_delete_metadata.go | 2 +- ...llers_transactions_delete_metadata_test.go | 3 +- .../api/v1/controllers_transactions_revert.go | 2 +- .../controllers_transactions_revert_test.go | 2 +- .../api/v1/mocks_ledger_controller_test.go | 46 +- internal/api/v2/common.go | 38 +- .../v2/controllers_accounts_add_metadata.go | 2 +- .../controllers_accounts_add_metadata_test.go | 3 +- .../controllers_accounts_delete_metadata.go | 2 +- ...ntrollers_accounts_delete_metadata_test.go | 3 +- internal/api/v2/controllers_bulk.go | 111 +++-- internal/api/v2/controllers_bulk_test.go | 20 +- .../controllers_transactions_add_metadata.go | 2 +- ...trollers_transactions_add_metadata_test.go | 2 +- .../api/v2/controllers_transactions_create.go | 2 +- .../controllers_transactions_create_test.go | 4 +- ...ontrollers_transactions_delete_metadata.go | 2 +- ...llers_transactions_delete_metadata_test.go | 3 +- .../api/v2/controllers_transactions_revert.go | 2 +- .../controllers_transactions_revert_test.go | 2 +- .../api/v2/mocks_ledger_controller_test.go | 46 +- internal/controller/ledger/controller.go | 12 +- .../controller/ledger/controller_default.go | 28 +- .../ledger/controller_default_test.go | 8 +- .../ledger/controller_generated_test.go | 46 +- .../ledger/controller_with_events.go | 48 +-- ...ontroller_with_too_many_client_handling.go | 53 +-- ...ller_with_too_many_client_handling_test.go | 10 +- .../ledger/controller_with_traces.go | 54 +-- internal/controller/ledger/log_process.go | 43 +- .../controller/ledger/log_process_test.go | 4 +- openapi.yaml | 3 + openapi/v2.yaml | 3 + pkg/client/.speakeasy/gen.lock | 6 +- pkg/client/.speakeasy/gen.yaml | 2 +- .../v2bulkelementresultaddmetadata.md | 3 +- .../v2bulkelementresultcreatetransaction.md | 1 + .../v2bulkelementresultdeletemetadata.md | 3 +- .../components/v2bulkelementresulterror.md | 1 + .../v2bulkelementresultreverttransaction.md | 1 + pkg/client/formance.go | 4 +- .../models/components/v2bulkelementresult.go | 40 ++ pkg/generate/generator.go | 220 +++++++++- pkg/generate/generator_test.go | 109 +++++ test/performance/benchmark_test.go | 90 +--- test/performance/example_scripts/example1.js | 13 +- .../performance/scripts/any_bounded_to_any.js | 25 +- .../scripts/any_unbounded_to_any.js | 25 +- test/performance/scripts/world_to_any.js | 21 +- test/performance/scripts/world_to_bank.js | 13 +- test/performance/write_test.go | 6 +- tools/generator/cmd/root.go | 29 +- tools/generator/examples/example1.js | 13 +- tools/generator/examples_test.go | 30 ++ tools/generator/go.mod | 137 +++++- tools/generator/go.sum | 393 +++++++++++++++++- 66 files changed, 1424 insertions(+), 478 deletions(-) create mode 100644 pkg/generate/generator_test.go create mode 100644 tools/generator/examples_test.go diff --git a/docs/api/README.md b/docs/api/README.md index 7e5da659d..616d7e693 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -463,6 +463,7 @@ Accept: application/json "data": [ { "responseType": "string", + "logID": 0, "data": { "insertedAt": "2019-08-24T14:15:22Z", "timestamp": "2019-08-24T14:15:22Z", @@ -3655,6 +3656,7 @@ and "data": [ { "responseType": "string", + "logID": 0, "data": { "insertedAt": "2019-08-24T14:15:22Z", "timestamp": "2019-08-24T14:15:22Z", @@ -3760,6 +3762,7 @@ and ```json { "responseType": "string", + "logID": 0, "data": { "insertedAt": "2019-08-24T14:15:22Z", "timestamp": "2019-08-24T14:15:22Z", @@ -3888,7 +3891,8 @@ xor ```json { - "responseType": "string" + "responseType": "string", + "logID": 0 } ``` @@ -3898,6 +3902,7 @@ xor |Name|Type|Required|Restrictions|Description| |---|---|---|---|---| |responseType|string|true|none|none| +|logID|integer|true|none|none|

V2BulkElementResultCreateTransaction

@@ -3909,6 +3914,7 @@ xor ```json { "responseType": "string", + "logID": 0, "data": { "insertedAt": "2019-08-24T14:15:22Z", "timestamp": "2019-08-24T14:15:22Z", @@ -4020,7 +4026,8 @@ and ```json { - "responseType": "string" + "responseType": "string", + "logID": 0 } ``` @@ -4039,6 +4046,7 @@ and ```json { "responseType": "string", + "logID": 0, "data": { "insertedAt": "2019-08-24T14:15:22Z", "timestamp": "2019-08-24T14:15:22Z", @@ -4150,7 +4158,8 @@ and ```json { - "responseType": "string" + "responseType": "string", + "logID": 0 } ``` @@ -4169,6 +4178,7 @@ and ```json { "responseType": "string", + "logID": 0, "errorCode": "string", "errorDescription": "string", "errorDetails": "string" diff --git a/internal/api/common/mocks_ledger_controller_test.go b/internal/api/common/mocks_ledger_controller_test.go index 3b9b07eaf..cfa5e8724 100644 --- a/internal/api/common/mocks_ledger_controller_test.go +++ b/internal/api/common/mocks_ledger_controller_test.go @@ -70,12 +70,13 @@ func (mr *LedgerControllerMockRecorder) CountTransactions(ctx, query any) *gomoc } // CreateTransaction mocks base method. -func (m *LedgerController) CreateTransaction(ctx context.Context, parameters ledger0.Parameters[ledger0.RunScript]) (*ledger.CreatedTransaction, error) { +func (m *LedgerController) CreateTransaction(ctx context.Context, parameters ledger0.Parameters[ledger0.RunScript]) (*ledger.Log, *ledger.CreatedTransaction, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateTransaction", ctx, parameters) - ret0, _ := ret[0].(*ledger.CreatedTransaction) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(*ledger.CreatedTransaction) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 } // CreateTransaction indicates an expected call of CreateTransaction. @@ -85,11 +86,12 @@ func (mr *LedgerControllerMockRecorder) CreateTransaction(ctx, parameters any) * } // DeleteAccountMetadata mocks base method. -func (m *LedgerController) DeleteAccountMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.DeleteAccountMetadata]) error { +func (m *LedgerController) DeleteAccountMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.DeleteAccountMetadata]) (*ledger.Log, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DeleteAccountMetadata", ctx, parameters) - ret0, _ := ret[0].(error) - return ret0 + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(error) + return ret0, ret1 } // DeleteAccountMetadata indicates an expected call of DeleteAccountMetadata. @@ -99,11 +101,12 @@ func (mr *LedgerControllerMockRecorder) DeleteAccountMetadata(ctx, parameters an } // DeleteTransactionMetadata mocks base method. -func (m *LedgerController) DeleteTransactionMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.DeleteTransactionMetadata]) error { +func (m *LedgerController) DeleteTransactionMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.DeleteTransactionMetadata]) (*ledger.Log, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DeleteTransactionMetadata", ctx, parameters) - ret0, _ := ret[0].(error) - return ret0 + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(error) + return ret0, ret1 } // DeleteTransactionMetadata indicates an expected call of DeleteTransactionMetadata. @@ -291,12 +294,13 @@ func (mr *LedgerControllerMockRecorder) ListTransactions(ctx, query any) *gomock } // RevertTransaction mocks base method. -func (m *LedgerController) RevertTransaction(ctx context.Context, parameters ledger0.Parameters[ledger0.RevertTransaction]) (*ledger.RevertedTransaction, error) { +func (m *LedgerController) RevertTransaction(ctx context.Context, parameters ledger0.Parameters[ledger0.RevertTransaction]) (*ledger.Log, *ledger.RevertedTransaction, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "RevertTransaction", ctx, parameters) - ret0, _ := ret[0].(*ledger.RevertedTransaction) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(*ledger.RevertedTransaction) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 } // RevertTransaction indicates an expected call of RevertTransaction. @@ -306,11 +310,12 @@ func (mr *LedgerControllerMockRecorder) RevertTransaction(ctx, parameters any) * } // SaveAccountMetadata mocks base method. -func (m *LedgerController) SaveAccountMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.SaveAccountMetadata]) error { +func (m *LedgerController) SaveAccountMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.SaveAccountMetadata]) (*ledger.Log, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SaveAccountMetadata", ctx, parameters) - ret0, _ := ret[0].(error) - return ret0 + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(error) + return ret0, ret1 } // SaveAccountMetadata indicates an expected call of SaveAccountMetadata. @@ -320,11 +325,12 @@ func (mr *LedgerControllerMockRecorder) SaveAccountMetadata(ctx, parameters any) } // SaveTransactionMetadata mocks base method. -func (m *LedgerController) SaveTransactionMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.SaveTransactionMetadata]) error { +func (m *LedgerController) SaveTransactionMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.SaveTransactionMetadata]) (*ledger.Log, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SaveTransactionMetadata", ctx, parameters) - ret0, _ := ret[0].(error) - return ret0 + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(error) + return ret0, ret1 } // SaveTransactionMetadata indicates an expected call of SaveTransactionMetadata. diff --git a/internal/api/v1/controllers_accounts_add_metadata.go b/internal/api/v1/controllers_accounts_add_metadata.go index eabbaebec..8263f3e06 100644 --- a/internal/api/v1/controllers_accounts_add_metadata.go +++ b/internal/api/v1/controllers_accounts_add_metadata.go @@ -33,7 +33,7 @@ func addAccountMetadata(w http.ResponseWriter, r *http.Request) { return } - err = l.SaveAccountMetadata(r.Context(), getCommandParameters(r, ledger.SaveAccountMetadata{ + _, err = l.SaveAccountMetadata(r.Context(), getCommandParameters(r, ledger.SaveAccountMetadata{ Address: address, Metadata: m, })) diff --git a/internal/api/v1/controllers_accounts_add_metadata_test.go b/internal/api/v1/controllers_accounts_add_metadata_test.go index 08f78771a..0a8a42750 100644 --- a/internal/api/v1/controllers_accounts_add_metadata_test.go +++ b/internal/api/v1/controllers_accounts_add_metadata_test.go @@ -1,6 +1,7 @@ package v1 import ( + ledger "github.com/formancehq/ledger/internal" "net/http" "net/http/httptest" "net/url" @@ -94,7 +95,7 @@ func TestAccountsAddMetadata(t *testing.T) { Metadata: testCase.body.(metadata.Metadata), }, }). - Return(nil) + Return(&ledger.Log{}, nil) } router := NewRouter(systemController, auth.NewNoAuth(), "develop", os.Getenv("DEBUG") == "true") diff --git a/internal/api/v1/controllers_accounts_delete_metadata.go b/internal/api/v1/controllers_accounts_delete_metadata.go index db1d2b6c9..2e57831dc 100644 --- a/internal/api/v1/controllers_accounts_delete_metadata.go +++ b/internal/api/v1/controllers_accounts_delete_metadata.go @@ -17,7 +17,7 @@ func deleteAccountMetadata(w http.ResponseWriter, r *http.Request) { return } - if err := common.LedgerFromContext(r.Context()). + if _, err := common.LedgerFromContext(r.Context()). DeleteAccountMetadata( r.Context(), getCommandParameters(r, ledger.DeleteAccountMetadata{ diff --git a/internal/api/v1/controllers_accounts_delete_metadata_test.go b/internal/api/v1/controllers_accounts_delete_metadata_test.go index 85d2d2eaf..6934cffe7 100644 --- a/internal/api/v1/controllers_accounts_delete_metadata_test.go +++ b/internal/api/v1/controllers_accounts_delete_metadata_test.go @@ -2,6 +2,7 @@ package v1 import ( "encoding/json" + ledger "github.com/formancehq/ledger/internal" "net/http" "net/http/httptest" "net/url" @@ -70,7 +71,7 @@ func TestAccountsDeleteMetadata(t *testing.T) { }, }, ). - Return(tc.returnErr) + Return(&ledger.Log{}, tc.returnErr) } router := NewRouter(systemController, auth.NewNoAuth(), "develop", os.Getenv("DEBUG") == "true") diff --git a/internal/api/v1/controllers_transactions_add_metadata.go b/internal/api/v1/controllers_transactions_add_metadata.go index 8982b4250..6e09db6ac 100644 --- a/internal/api/v1/controllers_transactions_add_metadata.go +++ b/internal/api/v1/controllers_transactions_add_metadata.go @@ -29,7 +29,7 @@ func addTransactionMetadata(w http.ResponseWriter, r *http.Request) { return } - if err := l.SaveTransactionMetadata(r.Context(), getCommandParameters(r, ledgercontroller.SaveTransactionMetadata{ + if _, err := l.SaveTransactionMetadata(r.Context(), getCommandParameters(r, ledgercontroller.SaveTransactionMetadata{ TransactionID: int(txID), Metadata: m, })); err != nil { diff --git a/internal/api/v1/controllers_transactions_add_metadata_test.go b/internal/api/v1/controllers_transactions_add_metadata_test.go index 30e2df4b3..f46c5e971 100644 --- a/internal/api/v1/controllers_transactions_add_metadata_test.go +++ b/internal/api/v1/controllers_transactions_add_metadata_test.go @@ -1,6 +1,7 @@ package v1 import ( + ledger "github.com/formancehq/ledger/internal" "net/http" "net/http/httptest" "net/url" @@ -58,7 +59,7 @@ func TestTransactionsAddMetadata(t *testing.T) { Metadata: testCase.body.(metadata.Metadata), }, }). - Return(nil) + Return(&ledger.Log{}, nil) } router := NewRouter(systemController, auth.NewNoAuth(), "develop", os.Getenv("DEBUG") == "true") diff --git a/internal/api/v1/controllers_transactions_create.go b/internal/api/v1/controllers_transactions_create.go index 71385b9a1..e1acb5e32 100644 --- a/internal/api/v1/controllers_transactions_create.go +++ b/internal/api/v1/controllers_transactions_create.go @@ -83,7 +83,7 @@ func createTransaction(w http.ResponseWriter, r *http.Request) { Metadata: payload.Metadata, } - res, err := l.CreateTransaction(r.Context(), getCommandParameters(r, common.TxToScriptData(txData, false))) + _, res, err := l.CreateTransaction(r.Context(), getCommandParameters(r, common.TxToScriptData(txData, false))) if err != nil { switch { case errors.Is(err, &ledgercontroller.ErrInsufficientFunds{}): @@ -119,7 +119,7 @@ func createTransaction(w http.ResponseWriter, r *http.Request) { Metadata: payload.Metadata, } - res, err := l.CreateTransaction(r.Context(), getCommandParameters(r, runScript)) + _, res, err := l.CreateTransaction(r.Context(), getCommandParameters(r, runScript)) if err != nil { switch { case errors.Is(err, &ledgercontroller.ErrInsufficientFunds{}): diff --git a/internal/api/v1/controllers_transactions_create_test.go b/internal/api/v1/controllers_transactions_create_test.go index 1ff7be7b7..f90ed338f 100644 --- a/internal/api/v1/controllers_transactions_create_test.go +++ b/internal/api/v1/controllers_transactions_create_test.go @@ -211,11 +211,10 @@ func TestTransactionsCreate(t *testing.T) { }, } - for _, testCase := range testCases { - tc := testCase + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusOK + if tc.expectedStatusCode == 0 { + tc.expectedStatusCode = http.StatusOK } expectedTx := ledger.NewTransaction().WithPostings( @@ -223,35 +222,35 @@ func TestTransactionsCreate(t *testing.T) { ) systemController, ledgerController := newTestingSystemController(t, true) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - testCase.expectedRunScript.Timestamp = time.Time{} + if tc.expectedStatusCode < 300 && tc.expectedStatusCode >= 200 { + tc.expectedRunScript.Timestamp = time.Time{} ledgerController.EXPECT(). CreateTransaction(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.RunScript]{ DryRun: tc.expectedPreview, - Input: testCase.expectedRunScript, + Input: tc.expectedRunScript, }). - Return(&ledger.CreatedTransaction{ + Return(&ledger.Log{}, &ledger.CreatedTransaction{ Transaction: expectedTx, }, nil) } router := NewRouter(systemController, auth.NewNoAuth(), "develop", os.Getenv("DEBUG") == "true") - req := httptest.NewRequest(http.MethodPost, "/xxx/transactions", api.Buffer(t, testCase.payload)) + req := httptest.NewRequest(http.MethodPost, "/xxx/transactions", api.Buffer(t, tc.payload)) rec := httptest.NewRecorder() - req.URL.RawQuery = testCase.queryParams.Encode() + req.URL.RawQuery = tc.queryParams.Encode() router.ServeHTTP(rec, req) - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { + require.Equal(t, tc.expectedStatusCode, rec.Code) + if tc.expectedStatusCode < 300 && tc.expectedStatusCode >= 200 { tx, ok := api.DecodeSingleResponse[[]ledger.Transaction](t, rec.Body) require.True(t, ok) require.Equal(t, expectedTx, tx[0]) } else { err := api.ErrorResponse{} api.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) + require.EqualValues(t, tc.expectedErrorCode, err.ErrorCode) } }) } diff --git a/internal/api/v1/controllers_transactions_delete_metadata.go b/internal/api/v1/controllers_transactions_delete_metadata.go index 1f706fca4..e9129b24d 100644 --- a/internal/api/v1/controllers_transactions_delete_metadata.go +++ b/internal/api/v1/controllers_transactions_delete_metadata.go @@ -23,7 +23,7 @@ func deleteTransactionMetadata(w http.ResponseWriter, r *http.Request) { metadataKey := chi.URLParam(r, "key") - if err := l.DeleteTransactionMetadata(r.Context(), getCommandParameters(r, ledgercontroller.DeleteTransactionMetadata{ + if _, err := l.DeleteTransactionMetadata(r.Context(), getCommandParameters(r, ledgercontroller.DeleteTransactionMetadata{ TransactionID: int(transactionID), Key: metadataKey, })); err != nil { diff --git a/internal/api/v1/controllers_transactions_delete_metadata_test.go b/internal/api/v1/controllers_transactions_delete_metadata_test.go index a9006dc92..ae3fc6eb3 100644 --- a/internal/api/v1/controllers_transactions_delete_metadata_test.go +++ b/internal/api/v1/controllers_transactions_delete_metadata_test.go @@ -2,6 +2,7 @@ package v1 import ( "encoding/json" + ledger "github.com/formancehq/ledger/internal" "net/http" "net/http/httptest" "net/url" @@ -64,7 +65,7 @@ func TestTransactionsDeleteMetadata(t *testing.T) { Key: "foo", }, }). - Return(tc.returnErr) + Return(&ledger.Log{}, tc.returnErr) } router := NewRouter(systemController, auth.NewNoAuth(), "develop", os.Getenv("DEBUG") == "true") diff --git a/internal/api/v1/controllers_transactions_revert.go b/internal/api/v1/controllers_transactions_revert.go index 611f96e71..5612b836d 100644 --- a/internal/api/v1/controllers_transactions_revert.go +++ b/internal/api/v1/controllers_transactions_revert.go @@ -21,7 +21,7 @@ func revertTransaction(w http.ResponseWriter, r *http.Request) { return } - ret, err := l.RevertTransaction( + _, ret, err := l.RevertTransaction( r.Context(), getCommandParameters(r, ledgercontroller.RevertTransaction{ Force: api.QueryParamBool(r, "disableChecks"), diff --git a/internal/api/v1/controllers_transactions_revert_test.go b/internal/api/v1/controllers_transactions_revert_test.go index 301fff877..b2603e32e 100644 --- a/internal/api/v1/controllers_transactions_revert_test.go +++ b/internal/api/v1/controllers_transactions_revert_test.go @@ -76,7 +76,7 @@ func TestTransactionsRevert(t *testing.T) { Force: tc.expectForce, }, }). - Return(pointer.For(ledger.RevertedTransaction{ + Return(&ledger.Log{}, pointer.For(ledger.RevertedTransaction{ RevertTransaction: tc.returnTx, }), tc.returnErr) diff --git a/internal/api/v1/mocks_ledger_controller_test.go b/internal/api/v1/mocks_ledger_controller_test.go index 4e7fadbf2..a849cf030 100644 --- a/internal/api/v1/mocks_ledger_controller_test.go +++ b/internal/api/v1/mocks_ledger_controller_test.go @@ -70,12 +70,13 @@ func (mr *LedgerControllerMockRecorder) CountTransactions(ctx, query any) *gomoc } // CreateTransaction mocks base method. -func (m *LedgerController) CreateTransaction(ctx context.Context, parameters ledger0.Parameters[ledger0.RunScript]) (*ledger.CreatedTransaction, error) { +func (m *LedgerController) CreateTransaction(ctx context.Context, parameters ledger0.Parameters[ledger0.RunScript]) (*ledger.Log, *ledger.CreatedTransaction, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateTransaction", ctx, parameters) - ret0, _ := ret[0].(*ledger.CreatedTransaction) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(*ledger.CreatedTransaction) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 } // CreateTransaction indicates an expected call of CreateTransaction. @@ -85,11 +86,12 @@ func (mr *LedgerControllerMockRecorder) CreateTransaction(ctx, parameters any) * } // DeleteAccountMetadata mocks base method. -func (m *LedgerController) DeleteAccountMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.DeleteAccountMetadata]) error { +func (m *LedgerController) DeleteAccountMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.DeleteAccountMetadata]) (*ledger.Log, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DeleteAccountMetadata", ctx, parameters) - ret0, _ := ret[0].(error) - return ret0 + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(error) + return ret0, ret1 } // DeleteAccountMetadata indicates an expected call of DeleteAccountMetadata. @@ -99,11 +101,12 @@ func (mr *LedgerControllerMockRecorder) DeleteAccountMetadata(ctx, parameters an } // DeleteTransactionMetadata mocks base method. -func (m *LedgerController) DeleteTransactionMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.DeleteTransactionMetadata]) error { +func (m *LedgerController) DeleteTransactionMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.DeleteTransactionMetadata]) (*ledger.Log, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DeleteTransactionMetadata", ctx, parameters) - ret0, _ := ret[0].(error) - return ret0 + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(error) + return ret0, ret1 } // DeleteTransactionMetadata indicates an expected call of DeleteTransactionMetadata. @@ -291,12 +294,13 @@ func (mr *LedgerControllerMockRecorder) ListTransactions(ctx, query any) *gomock } // RevertTransaction mocks base method. -func (m *LedgerController) RevertTransaction(ctx context.Context, parameters ledger0.Parameters[ledger0.RevertTransaction]) (*ledger.RevertedTransaction, error) { +func (m *LedgerController) RevertTransaction(ctx context.Context, parameters ledger0.Parameters[ledger0.RevertTransaction]) (*ledger.Log, *ledger.RevertedTransaction, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "RevertTransaction", ctx, parameters) - ret0, _ := ret[0].(*ledger.RevertedTransaction) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(*ledger.RevertedTransaction) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 } // RevertTransaction indicates an expected call of RevertTransaction. @@ -306,11 +310,12 @@ func (mr *LedgerControllerMockRecorder) RevertTransaction(ctx, parameters any) * } // SaveAccountMetadata mocks base method. -func (m *LedgerController) SaveAccountMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.SaveAccountMetadata]) error { +func (m *LedgerController) SaveAccountMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.SaveAccountMetadata]) (*ledger.Log, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SaveAccountMetadata", ctx, parameters) - ret0, _ := ret[0].(error) - return ret0 + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(error) + return ret0, ret1 } // SaveAccountMetadata indicates an expected call of SaveAccountMetadata. @@ -320,11 +325,12 @@ func (mr *LedgerControllerMockRecorder) SaveAccountMetadata(ctx, parameters any) } // SaveTransactionMetadata mocks base method. -func (m *LedgerController) SaveTransactionMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.SaveTransactionMetadata]) error { +func (m *LedgerController) SaveTransactionMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.SaveTransactionMetadata]) (*ledger.Log, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SaveTransactionMetadata", ctx, parameters) - ret0, _ := ret[0].(error) - return ret0 + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(error) + return ret0, ret1 } // SaveTransactionMetadata indicates an expected call of SaveTransactionMetadata. diff --git a/internal/api/v2/common.go b/internal/api/v2/common.go index 3351bc524..82fbc486e 100644 --- a/internal/api/v2/common.go +++ b/internal/api/v2/common.go @@ -1,9 +1,6 @@ package v2 import ( - "github.com/formancehq/go-libs/v2/metadata" - "github.com/formancehq/ledger/internal" - "github.com/formancehq/ledger/internal/api/common" "io" "net/http" "slices" @@ -179,37 +176,4 @@ func getPaginatedQueryOptionsOfFiltersForVolumes(r *http.Request) (*ledgercontro return pointer.For(ledgercontroller.NewPaginatedQueryOptions(*filtersForVolumes). WithPageSize(pageSize). WithQueryBuilder(qb)), nil -} - -type TransactionRequest struct { - Postings ledger.Postings `json:"postings"` - Script ledgercontroller.ScriptV1 `json:"script"` - Timestamp time.Time `json:"timestamp"` - Reference string `json:"reference"` - Metadata metadata.Metadata `json:"metadata" swaggertype:"object"` -} - -func (req *TransactionRequest) ToRunScript(allowUnboundedOverdrafts bool) (*ledgercontroller.RunScript, error) { - - if _, err := req.Postings.Validate(); err != nil { - return nil, err - } - - if len(req.Postings) > 0 { - txData := ledger.TransactionData{ - Postings: req.Postings, - Timestamp: req.Timestamp, - Reference: req.Reference, - Metadata: req.Metadata, - } - - return pointer.For(common.TxToScriptData(txData, allowUnboundedOverdrafts)), nil - } - - return &ledgercontroller.RunScript{ - Script: req.Script.ToCore(), - Timestamp: req.Timestamp, - Reference: req.Reference, - Metadata: req.Metadata, - }, nil -} +} \ No newline at end of file diff --git a/internal/api/v2/controllers_accounts_add_metadata.go b/internal/api/v2/controllers_accounts_add_metadata.go index b38dee690..b0a12038a 100644 --- a/internal/api/v2/controllers_accounts_add_metadata.go +++ b/internal/api/v2/controllers_accounts_add_metadata.go @@ -28,7 +28,7 @@ func addAccountMetadata(w http.ResponseWriter, r *http.Request) { return } - err = l.SaveAccountMetadata(r.Context(), getCommandParameters(r, ledger.SaveAccountMetadata{ + _, err = l.SaveAccountMetadata(r.Context(), getCommandParameters(r, ledger.SaveAccountMetadata{ Address: address, Metadata: m, })) diff --git a/internal/api/v2/controllers_accounts_add_metadata_test.go b/internal/api/v2/controllers_accounts_add_metadata_test.go index 505170334..7d6b73fd4 100644 --- a/internal/api/v2/controllers_accounts_add_metadata_test.go +++ b/internal/api/v2/controllers_accounts_add_metadata_test.go @@ -1,6 +1,7 @@ package v2 import ( + ledger "github.com/formancehq/ledger/internal" "net/http" "net/http/httptest" "net/url" @@ -67,7 +68,7 @@ func TestAccountsAddMetadata(t *testing.T) { Metadata: testCase.body.(metadata.Metadata), }, }). - Return(nil) + Return(&ledger.Log{}, nil) } router := NewRouter(systemController, auth.NewNoAuth(), os.Getenv("DEBUG") == "true") diff --git a/internal/api/v2/controllers_accounts_delete_metadata.go b/internal/api/v2/controllers_accounts_delete_metadata.go index 0e3115b91..fda3ac613 100644 --- a/internal/api/v2/controllers_accounts_delete_metadata.go +++ b/internal/api/v2/controllers_accounts_delete_metadata.go @@ -18,7 +18,7 @@ func deleteAccountMetadata(w http.ResponseWriter, r *http.Request) { return } - if err := common.LedgerFromContext(r.Context()). + if _, err := common.LedgerFromContext(r.Context()). DeleteAccountMetadata( r.Context(), getCommandParameters(r, ledger.DeleteAccountMetadata{ diff --git a/internal/api/v2/controllers_accounts_delete_metadata_test.go b/internal/api/v2/controllers_accounts_delete_metadata_test.go index 3130d49f1..c3d1fde78 100644 --- a/internal/api/v2/controllers_accounts_delete_metadata_test.go +++ b/internal/api/v2/controllers_accounts_delete_metadata_test.go @@ -2,6 +2,7 @@ package v2 import ( "encoding/json" + ledger "github.com/formancehq/ledger/internal" "net/http" "net/http/httptest" "net/url" @@ -67,7 +68,7 @@ func TestAccountsDeleteMetadata(t *testing.T) { Key: "foo", }, }). - Return(tc.returnErr) + Return(&ledger.Log{}, tc.returnErr) } router := NewRouter(systemController, auth.NewNoAuth(), os.Getenv("DEBUG") == "true") diff --git a/internal/api/v2/controllers_bulk.go b/internal/api/v2/controllers_bulk.go index afa1218d9..ab1a0dbce 100644 --- a/internal/api/v2/controllers_bulk.go +++ b/internal/api/v2/controllers_bulk.go @@ -4,6 +4,8 @@ import ( "context" "encoding/json" "fmt" + "github.com/formancehq/go-libs/v2/pointer" + "github.com/formancehq/go-libs/v2/time" "net/http" "errors" @@ -49,9 +51,9 @@ const ( ActionDeleteMetadata = "DELETE_METADATA" ) -type Bulk []Element +type Bulk []BulkElement -type Element struct { +type BulkElement struct { Action string `json:"action"` IdempotencyKey string `json:"ik"` Data json.RawMessage `json:"data"` @@ -63,6 +65,58 @@ type Result struct { ErrorDetails string `json:"errorDetails,omitempty"` Data any `json:"data,omitempty"` ResponseType string `json:"responseType"` // Added for sdk generation (discriminator in oneOf) + LogID int `json:"logID"` +} + +type AddMetadataRequest struct { + TargetType string `json:"targetType"` + TargetID json.RawMessage `json:"targetId"` + Metadata metadata.Metadata `json:"metadata"` +} + +type RevertTransactionRequest struct { + ID int `json:"id"` + Force bool `json:"force"` + AtEffectiveDate bool `json:"atEffectiveDate"` +} + +type DeleteMetadataRequest struct { + TargetType string `json:"targetType"` + TargetID json.RawMessage `json:"targetId"` + Key string `json:"key"` +} + +type TransactionRequest struct { + Postings ledger.Postings `json:"postings"` + Script ledgercontroller.ScriptV1 `json:"script"` + Timestamp time.Time `json:"timestamp"` + Reference string `json:"reference"` + Metadata metadata.Metadata `json:"metadata" swaggertype:"object"` +} + +func (req *TransactionRequest) ToRunScript(allowUnboundedOverdrafts bool) (*ledgercontroller.RunScript, error) { + + if _, err := req.Postings.Validate(); err != nil { + return nil, err + } + + if len(req.Postings) > 0 { + txData := ledger.TransactionData{ + Postings: req.Postings, + Timestamp: req.Timestamp, + Reference: req.Reference, + Metadata: req.Metadata, + } + + return pointer.For(common.TxToScriptData(txData, allowUnboundedOverdrafts)), nil + } + + return &ledgercontroller.RunScript{ + Script: req.Script.ToCore(), + Timestamp: req.Timestamp, + Reference: req.Reference, + Metadata: req.Metadata, + }, nil } func ProcessBulk( @@ -96,7 +150,7 @@ func ProcessBulk( return nil, errorsInBulk, fmt.Errorf("error parsing element %d: %s", i, err) } - createTransactionResult, err := l.CreateTransaction(ctx, ledgercontroller.Parameters[ledgercontroller.RunScript]{ + log, createTransactionResult, err := l.CreateTransaction(ctx, ledgercontroller.Parameters[ledgercontroller.RunScript]{ DryRun: false, IdempotencyKey: element.IdempotencyKey, Input: *rs, @@ -131,27 +185,26 @@ func ProcessBulk( ret = append(ret, Result{ Data: createTransactionResult.Transaction, ResponseType: element.Action, + LogID: log.ID, }) } case ActionAddMetadata: - type addMetadataRequest struct { - TargetType string `json:"targetType"` - TargetID json.RawMessage `json:"targetId"` - Metadata metadata.Metadata `json:"metadata"` - } - req := &addMetadataRequest{} + req := &AddMetadataRequest{} if err := json.Unmarshal(element.Data, req); err != nil { return nil, errorsInBulk, fmt.Errorf("error parsing element %d: %s", i, err) } - var err error + var ( + log *ledger.Log + err error + ) switch req.TargetType { case ledger.MetaTargetTypeAccount: address := "" if err := json.Unmarshal(req.TargetID, &address); err != nil { return nil, errorsInBulk, err } - err = l.SaveAccountMetadata(ctx, ledgercontroller.Parameters[ledgercontroller.SaveAccountMetadata]{ + log, err = l.SaveAccountMetadata(ctx, ledgercontroller.Parameters[ledgercontroller.SaveAccountMetadata]{ DryRun: false, IdempotencyKey: element.IdempotencyKey, Input: ledgercontroller.SaveAccountMetadata{ @@ -164,7 +217,7 @@ func ProcessBulk( if err := json.Unmarshal(req.TargetID, &transactionID); err != nil { return nil, errorsInBulk, err } - err = l.SaveTransactionMetadata(ctx, ledgercontroller.Parameters[ledgercontroller.SaveTransactionMetadata]{ + log, err = l.SaveTransactionMetadata(ctx, ledgercontroller.Parameters[ledgercontroller.SaveTransactionMetadata]{ DryRun: false, IdempotencyKey: element.IdempotencyKey, Input: ledgercontroller.SaveTransactionMetadata{ @@ -172,6 +225,8 @@ func ProcessBulk( Metadata: req.Metadata, }, }) + default: + return nil, errorsInBulk, fmt.Errorf("invalid target type: %s", req.TargetType) } if err != nil { var code string @@ -188,20 +243,16 @@ func ProcessBulk( } else { ret = append(ret, Result{ ResponseType: element.Action, + LogID: log.ID, }) } case ActionRevertTransaction: - type revertTransactionRequest struct { - ID int `json:"id"` - Force bool `json:"force"` - AtEffectiveDate bool `json:"atEffectiveDate"` - } - req := &revertTransactionRequest{} + req := &RevertTransactionRequest{} if err := json.Unmarshal(element.Data, req); err != nil { return nil, errorsInBulk, fmt.Errorf("error parsing element %d: %s", i, err) } - revertTransactionResult, err := l.RevertTransaction(ctx, ledgercontroller.Parameters[ledgercontroller.RevertTransaction]{ + log, revertTransactionResult, err := l.RevertTransaction(ctx, ledgercontroller.Parameters[ledgercontroller.RevertTransaction]{ DryRun: false, IdempotencyKey: element.IdempotencyKey, Input: ledgercontroller.RevertTransaction{ @@ -226,27 +277,27 @@ func ProcessBulk( ret = append(ret, Result{ Data: revertTransactionResult.RevertTransaction, ResponseType: element.Action, + LogID: log.ID, }) } case ActionDeleteMetadata: - type deleteMetadataRequest struct { - TargetType string `json:"targetType"` - TargetID json.RawMessage `json:"targetId"` - Key string `json:"key"` - } - req := &deleteMetadataRequest{} + req := &DeleteMetadataRequest{} if err := json.Unmarshal(element.Data, req); err != nil { return nil, errorsInBulk, fmt.Errorf("error parsing element %d: %s", i, err) } - var err error + var ( + log *ledger.Log + err error + ) switch req.TargetType { case ledger.MetaTargetTypeAccount: address := "" if err := json.Unmarshal(req.TargetID, &address); err != nil { return nil, errorsInBulk, err } - err = l.DeleteAccountMetadata(ctx, ledgercontroller.Parameters[ledgercontroller.DeleteAccountMetadata]{ + + log, err = l.DeleteAccountMetadata(ctx, ledgercontroller.Parameters[ledgercontroller.DeleteAccountMetadata]{ DryRun: false, IdempotencyKey: element.IdempotencyKey, Input: ledgercontroller.DeleteAccountMetadata{ @@ -259,7 +310,8 @@ func ProcessBulk( if err := json.Unmarshal(req.TargetID, &transactionID); err != nil { return nil, errorsInBulk, err } - err = l.DeleteTransactionMetadata(ctx, ledgercontroller.Parameters[ledgercontroller.DeleteTransactionMetadata]{ + + log, err = l.DeleteTransactionMetadata(ctx, ledgercontroller.Parameters[ledgercontroller.DeleteTransactionMetadata]{ DryRun: false, IdempotencyKey: element.IdempotencyKey, Input: ledgercontroller.DeleteTransactionMetadata{ @@ -267,6 +319,8 @@ func ProcessBulk( Key: req.Key, }, }) + default: + return nil, errorsInBulk, fmt.Errorf("unsupported target type: %s", req.TargetType) } if err != nil { var code string @@ -283,6 +337,7 @@ func ProcessBulk( } else { ret = append(ret, Result{ ResponseType: element.Action, + LogID: log.ID, }) } } diff --git a/internal/api/v2/controllers_bulk_test.go b/internal/api/v2/controllers_bulk_test.go index aa6b6f38b..31f8e857d 100644 --- a/internal/api/v2/controllers_bulk_test.go +++ b/internal/api/v2/controllers_bulk_test.go @@ -68,7 +68,7 @@ func TestBulk(t *testing.T) { Timestamp: now, }, false), }). - Return(&ledger.CreatedTransaction{ + Return(&ledger.Log{}, &ledger.CreatedTransaction{ Transaction: ledger.Transaction{ TransactionData: ledger.TransactionData{ Postings: postings, @@ -118,7 +118,7 @@ func TestBulk(t *testing.T) { }, }, }). - Return(nil) + Return(&ledger.Log{}, nil) }, expectResults: []Result{{ ResponseType: ActionAddMetadata, @@ -146,7 +146,7 @@ func TestBulk(t *testing.T) { }, }, }). - Return(nil) + Return(&ledger.Log{}, nil) }, expectResults: []Result{{ ResponseType: ActionAddMetadata, @@ -167,7 +167,7 @@ func TestBulk(t *testing.T) { TransactionID: 1, }, }). - Return(&ledger.RevertedTransaction{}, nil) + Return(&ledger.Log{}, &ledger.RevertedTransaction{}, nil) }, expectResults: []Result{{ Data: map[string]any{ @@ -198,7 +198,7 @@ func TestBulk(t *testing.T) { Key: "foo", }, }). - Return(nil) + Return(&ledger.Log{}, nil) }, expectResults: []Result{{ ResponseType: ActionDeleteMetadata, @@ -248,7 +248,7 @@ func TestBulk(t *testing.T) { }, }, }). - Return(nil) + Return(&ledger.Log{}, nil) mockLedger.EXPECT(). SaveAccountMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.SaveAccountMetadata]{ Input: ledgercontroller.SaveAccountMetadata{ @@ -258,7 +258,7 @@ func TestBulk(t *testing.T) { }, }, }). - Return(errors.New("unexpected error")) + Return(nil, errors.New("unexpected error")) }, expectResults: []Result{{ ResponseType: ActionAddMetadata, @@ -316,7 +316,7 @@ func TestBulk(t *testing.T) { }, }, }). - Return(nil) + Return(&ledger.Log{}, nil) mockLedger.EXPECT(). SaveAccountMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.SaveAccountMetadata]{ Input: ledgercontroller.SaveAccountMetadata{ @@ -326,7 +326,7 @@ func TestBulk(t *testing.T) { }, }, }). - Return(errors.New("unexpected error")) + Return(nil, errors.New("unexpected error")) mockLedger.EXPECT(). SaveAccountMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.SaveAccountMetadata]{ Input: ledgercontroller.SaveAccountMetadata{ @@ -336,7 +336,7 @@ func TestBulk(t *testing.T) { }, }, }). - Return(nil) + Return(&ledger.Log{}, nil) }, expectResults: []Result{{ ResponseType: ActionAddMetadata, diff --git a/internal/api/v2/controllers_transactions_add_metadata.go b/internal/api/v2/controllers_transactions_add_metadata.go index fa81f7292..9b1118b72 100644 --- a/internal/api/v2/controllers_transactions_add_metadata.go +++ b/internal/api/v2/controllers_transactions_add_metadata.go @@ -29,7 +29,7 @@ func addTransactionMetadata(w http.ResponseWriter, r *http.Request) { return } - if err := l.SaveTransactionMetadata(r.Context(), getCommandParameters(r, ledgercontroller.SaveTransactionMetadata{ + if _, err := l.SaveTransactionMetadata(r.Context(), getCommandParameters(r, ledgercontroller.SaveTransactionMetadata{ TransactionID: int(txID), Metadata: m, })); err != nil { diff --git a/internal/api/v2/controllers_transactions_add_metadata_test.go b/internal/api/v2/controllers_transactions_add_metadata_test.go index eadc9de5a..8f108cd9b 100644 --- a/internal/api/v2/controllers_transactions_add_metadata_test.go +++ b/internal/api/v2/controllers_transactions_add_metadata_test.go @@ -95,7 +95,7 @@ func TestTransactionsAddMetadata(t *testing.T) { Metadata: testCase.body.(metadata.Metadata), }, }). - Return(testCase.returnErr) + Return(nil, testCase.returnErr) } router := NewRouter(systemController, auth.NewNoAuth(), os.Getenv("DEBUG") == "true") diff --git a/internal/api/v2/controllers_transactions_create.go b/internal/api/v2/controllers_transactions_create.go index 912c18b45..2147d146e 100644 --- a/internal/api/v2/controllers_transactions_create.go +++ b/internal/api/v2/controllers_transactions_create.go @@ -37,7 +37,7 @@ func createTransaction(w http.ResponseWriter, r *http.Request) { return } - res, err := l.CreateTransaction(r.Context(), getCommandParameters(r, *runScript)) + _, res, err := l.CreateTransaction(r.Context(), getCommandParameters(r, *runScript)) if err != nil { switch { case errors.Is(err, &ledgercontroller.ErrInsufficientFunds{}): diff --git a/internal/api/v2/controllers_transactions_create_test.go b/internal/api/v2/controllers_transactions_create_test.go index d243b675e..e4870caf2 100644 --- a/internal/api/v2/controllers_transactions_create_test.go +++ b/internal/api/v2/controllers_transactions_create_test.go @@ -407,11 +407,11 @@ func TestTransactionCreate(t *testing.T) { }) if tc.returnError == nil { - expect.Return(&ledger.CreatedTransaction{ + expect.Return(&ledger.Log{}, &ledger.CreatedTransaction{ Transaction: expectedTx, }, nil) } else { - expect.Return(nil, tc.returnError) + expect.Return(nil, nil, tc.returnError) } } diff --git a/internal/api/v2/controllers_transactions_delete_metadata.go b/internal/api/v2/controllers_transactions_delete_metadata.go index 0d7c281a1..f14067c30 100644 --- a/internal/api/v2/controllers_transactions_delete_metadata.go +++ b/internal/api/v2/controllers_transactions_delete_metadata.go @@ -25,7 +25,7 @@ func deleteTransactionMetadata(w http.ResponseWriter, r *http.Request) { metadataKey := chi.URLParam(r, "key") - if err := l.DeleteTransactionMetadata(r.Context(), getCommandParameters(r, ledgercontroller.DeleteTransactionMetadata{ + if _, err := l.DeleteTransactionMetadata(r.Context(), getCommandParameters(r, ledgercontroller.DeleteTransactionMetadata{ TransactionID: int(txID), Key: metadataKey, })); err != nil { diff --git a/internal/api/v2/controllers_transactions_delete_metadata_test.go b/internal/api/v2/controllers_transactions_delete_metadata_test.go index 882ca685e..a239b4ecf 100644 --- a/internal/api/v2/controllers_transactions_delete_metadata_test.go +++ b/internal/api/v2/controllers_transactions_delete_metadata_test.go @@ -2,6 +2,7 @@ package v2 import ( "encoding/json" + ledger "github.com/formancehq/ledger/internal" "net/http" "net/http/httptest" "net/url" @@ -64,7 +65,7 @@ func TestTransactionsDeleteMetadata(t *testing.T) { Key: "foo", }, }). - Return(tc.returnErr) + Return(&ledger.Log{}, tc.returnErr) } router := NewRouter(systemController, auth.NewNoAuth(), os.Getenv("DEBUG") == "true") diff --git a/internal/api/v2/controllers_transactions_revert.go b/internal/api/v2/controllers_transactions_revert.go index 2d7af3255..aae713ef1 100644 --- a/internal/api/v2/controllers_transactions_revert.go +++ b/internal/api/v2/controllers_transactions_revert.go @@ -21,7 +21,7 @@ func revertTransaction(w http.ResponseWriter, r *http.Request) { return } - ret, err := l.RevertTransaction( + _, ret, err := l.RevertTransaction( r.Context(), getCommandParameters(r, ledgercontroller.RevertTransaction{ Force: api.QueryParamBool(r, "force"), diff --git a/internal/api/v2/controllers_transactions_revert_test.go b/internal/api/v2/controllers_transactions_revert_test.go index 987c4d513..8b03710a8 100644 --- a/internal/api/v2/controllers_transactions_revert_test.go +++ b/internal/api/v2/controllers_transactions_revert_test.go @@ -76,7 +76,7 @@ func TestTransactionsRevert(t *testing.T) { Force: tc.expectForce, }, }). - Return(&ledger.RevertedTransaction{ + Return(&ledger.Log{}, &ledger.RevertedTransaction{ RevertTransaction: tc.returnTx, }, tc.returnErr) diff --git a/internal/api/v2/mocks_ledger_controller_test.go b/internal/api/v2/mocks_ledger_controller_test.go index 28d3291a2..0613e2e93 100644 --- a/internal/api/v2/mocks_ledger_controller_test.go +++ b/internal/api/v2/mocks_ledger_controller_test.go @@ -70,12 +70,13 @@ func (mr *LedgerControllerMockRecorder) CountTransactions(ctx, query any) *gomoc } // CreateTransaction mocks base method. -func (m *LedgerController) CreateTransaction(ctx context.Context, parameters ledger0.Parameters[ledger0.RunScript]) (*ledger.CreatedTransaction, error) { +func (m *LedgerController) CreateTransaction(ctx context.Context, parameters ledger0.Parameters[ledger0.RunScript]) (*ledger.Log, *ledger.CreatedTransaction, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateTransaction", ctx, parameters) - ret0, _ := ret[0].(*ledger.CreatedTransaction) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(*ledger.CreatedTransaction) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 } // CreateTransaction indicates an expected call of CreateTransaction. @@ -85,11 +86,12 @@ func (mr *LedgerControllerMockRecorder) CreateTransaction(ctx, parameters any) * } // DeleteAccountMetadata mocks base method. -func (m *LedgerController) DeleteAccountMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.DeleteAccountMetadata]) error { +func (m *LedgerController) DeleteAccountMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.DeleteAccountMetadata]) (*ledger.Log, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DeleteAccountMetadata", ctx, parameters) - ret0, _ := ret[0].(error) - return ret0 + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(error) + return ret0, ret1 } // DeleteAccountMetadata indicates an expected call of DeleteAccountMetadata. @@ -99,11 +101,12 @@ func (mr *LedgerControllerMockRecorder) DeleteAccountMetadata(ctx, parameters an } // DeleteTransactionMetadata mocks base method. -func (m *LedgerController) DeleteTransactionMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.DeleteTransactionMetadata]) error { +func (m *LedgerController) DeleteTransactionMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.DeleteTransactionMetadata]) (*ledger.Log, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DeleteTransactionMetadata", ctx, parameters) - ret0, _ := ret[0].(error) - return ret0 + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(error) + return ret0, ret1 } // DeleteTransactionMetadata indicates an expected call of DeleteTransactionMetadata. @@ -291,12 +294,13 @@ func (mr *LedgerControllerMockRecorder) ListTransactions(ctx, query any) *gomock } // RevertTransaction mocks base method. -func (m *LedgerController) RevertTransaction(ctx context.Context, parameters ledger0.Parameters[ledger0.RevertTransaction]) (*ledger.RevertedTransaction, error) { +func (m *LedgerController) RevertTransaction(ctx context.Context, parameters ledger0.Parameters[ledger0.RevertTransaction]) (*ledger.Log, *ledger.RevertedTransaction, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "RevertTransaction", ctx, parameters) - ret0, _ := ret[0].(*ledger.RevertedTransaction) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(*ledger.RevertedTransaction) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 } // RevertTransaction indicates an expected call of RevertTransaction. @@ -306,11 +310,12 @@ func (mr *LedgerControllerMockRecorder) RevertTransaction(ctx, parameters any) * } // SaveAccountMetadata mocks base method. -func (m *LedgerController) SaveAccountMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.SaveAccountMetadata]) error { +func (m *LedgerController) SaveAccountMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.SaveAccountMetadata]) (*ledger.Log, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SaveAccountMetadata", ctx, parameters) - ret0, _ := ret[0].(error) - return ret0 + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(error) + return ret0, ret1 } // SaveAccountMetadata indicates an expected call of SaveAccountMetadata. @@ -320,11 +325,12 @@ func (mr *LedgerControllerMockRecorder) SaveAccountMetadata(ctx, parameters any) } // SaveTransactionMetadata mocks base method. -func (m *LedgerController) SaveTransactionMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.SaveTransactionMetadata]) error { +func (m *LedgerController) SaveTransactionMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.SaveTransactionMetadata]) (*ledger.Log, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SaveTransactionMetadata", ctx, parameters) - ret0, _ := ret[0].(error) - return ret0 + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(error) + return ret0, ret1 } // SaveTransactionMetadata indicates an expected call of SaveTransactionMetadata. diff --git a/internal/controller/ledger/controller.go b/internal/controller/ledger/controller.go index fc1ca32ba..feb401262 100644 --- a/internal/controller/ledger/controller.go +++ b/internal/controller/ledger/controller.go @@ -37,7 +37,7 @@ type Controller interface { // * ErrTransactionReferenceConflict // * ErrIdempotencyKeyConflict // * ErrInsufficientFunds - CreateTransaction(ctx context.Context, parameters Parameters[RunScript]) (*ledger.CreatedTransaction, error) + CreateTransaction(ctx context.Context, parameters Parameters[RunScript]) (*ledger.Log, *ledger.CreatedTransaction, error) // RevertTransaction allow to revert a transaction. // It can return following errors: // * ErrInsufficientFunds @@ -45,22 +45,22 @@ type Controller interface { // * ErrNotFound // Parameter force indicate we want to force revert the transaction even if the accounts does not have funds // Parameter atEffectiveDate indicate we want to set the timestamp of the newly created transaction on the timestamp of the reverted transaction - RevertTransaction(ctx context.Context, parameters Parameters[RevertTransaction]) (*ledger.RevertedTransaction, error) + RevertTransaction(ctx context.Context, parameters Parameters[RevertTransaction]) (*ledger.Log, *ledger.RevertedTransaction, error) // SaveTransactionMetadata allow to add metadata to an existing transaction // It can return following errors: // * ErrNotFound - SaveTransactionMetadata(ctx context.Context, parameters Parameters[SaveTransactionMetadata]) error + SaveTransactionMetadata(ctx context.Context, parameters Parameters[SaveTransactionMetadata]) (*ledger.Log, error) // SaveAccountMetadata allow to add metadata to an account // If the account does not exist, it is created - SaveAccountMetadata(ctx context.Context, parameters Parameters[SaveAccountMetadata]) error + SaveAccountMetadata(ctx context.Context, parameters Parameters[SaveAccountMetadata]) (*ledger.Log, error) // DeleteTransactionMetadata allow to remove metadata of a transaction // It can return following errors: // * ErrNotFound : indicate the transaction was not found OR the metadata does not exist on the transaction - DeleteTransactionMetadata(ctx context.Context, parameters Parameters[DeleteTransactionMetadata]) error + DeleteTransactionMetadata(ctx context.Context, parameters Parameters[DeleteTransactionMetadata]) (*ledger.Log, error) // DeleteAccountMetadata allow to remove metadata of an account // It can return following errors: // * ErrNotFound : indicate the account was not found OR the metadata does not exist on the account - DeleteAccountMetadata(ctx context.Context, parameters Parameters[DeleteAccountMetadata]) error + DeleteAccountMetadata(ctx context.Context, parameters Parameters[DeleteAccountMetadata]) (*ledger.Log, error) // Import allow to import the logs of an existing ledger // It can return following errors: // * ErrImport diff --git a/internal/controller/ledger/controller_default.go b/internal/controller/ledger/controller_default.go index d5d8bdf56..f8dcc70ef 100644 --- a/internal/controller/ledger/controller_default.go +++ b/internal/controller/ledger/controller_default.go @@ -306,7 +306,7 @@ func (ctrl *DefaultController) createTransaction(ctx context.Context, sqlTX TX, }, err } -func (ctrl *DefaultController) CreateTransaction(ctx context.Context, parameters Parameters[RunScript]) (*ledger.CreatedTransaction, error) { +func (ctrl *DefaultController) CreateTransaction(ctx context.Context, parameters Parameters[RunScript]) (*ledger.Log, *ledger.CreatedTransaction, error) { return ctrl.createTransactionLp.forgeLog(ctx, ctrl.store, parameters, ctrl.createTransaction) } @@ -372,7 +372,7 @@ func (ctrl *DefaultController) revertTransaction(ctx context.Context, sqlTX TX, }, nil } -func (ctrl *DefaultController) RevertTransaction(ctx context.Context, parameters Parameters[RevertTransaction]) (*ledger.RevertedTransaction, error) { +func (ctrl *DefaultController) RevertTransaction(ctx context.Context, parameters Parameters[RevertTransaction]) (*ledger.Log, *ledger.RevertedTransaction, error) { return ctrl.revertTransactionLp.forgeLog(ctx, ctrl.store, parameters, ctrl.revertTransaction) } @@ -388,9 +388,9 @@ func (ctrl *DefaultController) saveTransactionMetadata(ctx context.Context, sqlT }, nil } -func (ctrl *DefaultController) SaveTransactionMetadata(ctx context.Context, parameters Parameters[SaveTransactionMetadata]) error { - _, err := ctrl.saveTransactionMetadataLp.forgeLog(ctx, ctrl.store, parameters, ctrl.saveTransactionMetadata) - return err +func (ctrl *DefaultController) SaveTransactionMetadata(ctx context.Context, parameters Parameters[SaveTransactionMetadata]) (*ledger.Log, error) { + log, _, err := ctrl.saveTransactionMetadataLp.forgeLog(ctx, ctrl.store, parameters, ctrl.saveTransactionMetadata) + return log, err } func (ctrl *DefaultController) saveAccountMetadata(ctx context.Context, sqlTX TX, parameters Parameters[SaveAccountMetadata]) (*ledger.SavedMetadata, error) { @@ -408,10 +408,10 @@ func (ctrl *DefaultController) saveAccountMetadata(ctx context.Context, sqlTX TX }, nil } -func (ctrl *DefaultController) SaveAccountMetadata(ctx context.Context, parameters Parameters[SaveAccountMetadata]) error { - _, err := ctrl.saveAccountMetadataLp.forgeLog(ctx, ctrl.store, parameters, ctrl.saveAccountMetadata) +func (ctrl *DefaultController) SaveAccountMetadata(ctx context.Context, parameters Parameters[SaveAccountMetadata]) (*ledger.Log, error) { + log, _, err := ctrl.saveAccountMetadataLp.forgeLog(ctx, ctrl.store, parameters, ctrl.saveAccountMetadata) - return err + return log, err } func (ctrl *DefaultController) deleteTransactionMetadata(ctx context.Context, sqlTX TX, parameters Parameters[DeleteTransactionMetadata]) (*ledger.DeletedMetadata, error) { @@ -431,9 +431,9 @@ func (ctrl *DefaultController) deleteTransactionMetadata(ctx context.Context, sq }, nil } -func (ctrl *DefaultController) DeleteTransactionMetadata(ctx context.Context, parameters Parameters[DeleteTransactionMetadata]) error { - _, err := ctrl.deleteTransactionMetadataLp.forgeLog(ctx, ctrl.store, parameters, ctrl.deleteTransactionMetadata) - return err +func (ctrl *DefaultController) DeleteTransactionMetadata(ctx context.Context, parameters Parameters[DeleteTransactionMetadata]) (*ledger.Log, error) { + log, _, err := ctrl.deleteTransactionMetadataLp.forgeLog(ctx, ctrl.store, parameters, ctrl.deleteTransactionMetadata) + return log, err } func (ctrl *DefaultController) deleteAccountMetadata(ctx context.Context, sqlTX TX, parameters Parameters[DeleteAccountMetadata]) (*ledger.DeletedMetadata, error) { @@ -449,9 +449,9 @@ func (ctrl *DefaultController) deleteAccountMetadata(ctx context.Context, sqlTX }, nil } -func (ctrl *DefaultController) DeleteAccountMetadata(ctx context.Context, parameters Parameters[DeleteAccountMetadata]) error { - _, err := ctrl.deleteAccountMetadataLp.forgeLog(ctx, ctrl.store, parameters, ctrl.deleteAccountMetadata) - return err +func (ctrl *DefaultController) DeleteAccountMetadata(ctx context.Context, parameters Parameters[DeleteAccountMetadata]) (*ledger.Log, error) { + log, _, err := ctrl.deleteAccountMetadataLp.forgeLog(ctx, ctrl.store, parameters, ctrl.deleteAccountMetadata) + return log, err } var _ Controller = (*DefaultController)(nil) diff --git a/internal/controller/ledger/controller_default_test.go b/internal/controller/ledger/controller_default_test.go index e6165068f..ed3974972 100644 --- a/internal/controller/ledger/controller_default_test.go +++ b/internal/controller/ledger/controller_default_test.go @@ -62,7 +62,7 @@ func TestCreateTransaction(t *testing.T) { return x }) - _, err := l.CreateTransaction(context.Background(), Parameters[RunScript]{ + _, _, err := l.CreateTransaction(context.Background(), Parameters[RunScript]{ Input: runScript, }) require.NoError(t, err) @@ -108,7 +108,7 @@ func TestRevertTransaction(t *testing.T) { })). Return(nil) - _, err := l.RevertTransaction(ctx, Parameters[RevertTransaction]{ + _, _, err := l.RevertTransaction(ctx, Parameters[RevertTransaction]{ Input: RevertTransaction{ TransactionID: 1, }, @@ -147,7 +147,7 @@ func TestSaveTransactionMetadata(t *testing.T) { })). Return(nil) - err := l.SaveTransactionMetadata(ctx, Parameters[SaveTransactionMetadata]{ + _, err := l.SaveTransactionMetadata(ctx, Parameters[SaveTransactionMetadata]{ Input: SaveTransactionMetadata{ Metadata: m, TransactionID: 1, @@ -184,7 +184,7 @@ func TestDeleteTransactionMetadata(t *testing.T) { })). Return(nil) - err := l.DeleteTransactionMetadata(ctx, Parameters[DeleteTransactionMetadata]{ + _, err := l.DeleteTransactionMetadata(ctx, Parameters[DeleteTransactionMetadata]{ Input: DeleteTransactionMetadata{ TransactionID: 1, Key: "foo", diff --git a/internal/controller/ledger/controller_generated_test.go b/internal/controller/ledger/controller_generated_test.go index 4fae9acac..7416ba889 100644 --- a/internal/controller/ledger/controller_generated_test.go +++ b/internal/controller/ledger/controller_generated_test.go @@ -69,12 +69,13 @@ func (mr *MockControllerMockRecorder) CountTransactions(ctx, query any) *gomock. } // CreateTransaction mocks base method. -func (m *MockController) CreateTransaction(ctx context.Context, parameters Parameters[RunScript]) (*ledger.CreatedTransaction, error) { +func (m *MockController) CreateTransaction(ctx context.Context, parameters Parameters[RunScript]) (*ledger.Log, *ledger.CreatedTransaction, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateTransaction", ctx, parameters) - ret0, _ := ret[0].(*ledger.CreatedTransaction) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(*ledger.CreatedTransaction) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 } // CreateTransaction indicates an expected call of CreateTransaction. @@ -84,11 +85,12 @@ func (mr *MockControllerMockRecorder) CreateTransaction(ctx, parameters any) *go } // DeleteAccountMetadata mocks base method. -func (m *MockController) DeleteAccountMetadata(ctx context.Context, parameters Parameters[DeleteAccountMetadata]) error { +func (m *MockController) DeleteAccountMetadata(ctx context.Context, parameters Parameters[DeleteAccountMetadata]) (*ledger.Log, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DeleteAccountMetadata", ctx, parameters) - ret0, _ := ret[0].(error) - return ret0 + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(error) + return ret0, ret1 } // DeleteAccountMetadata indicates an expected call of DeleteAccountMetadata. @@ -98,11 +100,12 @@ func (mr *MockControllerMockRecorder) DeleteAccountMetadata(ctx, parameters any) } // DeleteTransactionMetadata mocks base method. -func (m *MockController) DeleteTransactionMetadata(ctx context.Context, parameters Parameters[DeleteTransactionMetadata]) error { +func (m *MockController) DeleteTransactionMetadata(ctx context.Context, parameters Parameters[DeleteTransactionMetadata]) (*ledger.Log, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DeleteTransactionMetadata", ctx, parameters) - ret0, _ := ret[0].(error) - return ret0 + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(error) + return ret0, ret1 } // DeleteTransactionMetadata indicates an expected call of DeleteTransactionMetadata. @@ -290,12 +293,13 @@ func (mr *MockControllerMockRecorder) ListTransactions(ctx, query any) *gomock.C } // RevertTransaction mocks base method. -func (m *MockController) RevertTransaction(ctx context.Context, parameters Parameters[RevertTransaction]) (*ledger.RevertedTransaction, error) { +func (m *MockController) RevertTransaction(ctx context.Context, parameters Parameters[RevertTransaction]) (*ledger.Log, *ledger.RevertedTransaction, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "RevertTransaction", ctx, parameters) - ret0, _ := ret[0].(*ledger.RevertedTransaction) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(*ledger.RevertedTransaction) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 } // RevertTransaction indicates an expected call of RevertTransaction. @@ -305,11 +309,12 @@ func (mr *MockControllerMockRecorder) RevertTransaction(ctx, parameters any) *go } // SaveAccountMetadata mocks base method. -func (m *MockController) SaveAccountMetadata(ctx context.Context, parameters Parameters[SaveAccountMetadata]) error { +func (m *MockController) SaveAccountMetadata(ctx context.Context, parameters Parameters[SaveAccountMetadata]) (*ledger.Log, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SaveAccountMetadata", ctx, parameters) - ret0, _ := ret[0].(error) - return ret0 + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(error) + return ret0, ret1 } // SaveAccountMetadata indicates an expected call of SaveAccountMetadata. @@ -319,11 +324,12 @@ func (mr *MockControllerMockRecorder) SaveAccountMetadata(ctx, parameters any) * } // SaveTransactionMetadata mocks base method. -func (m *MockController) SaveTransactionMetadata(ctx context.Context, parameters Parameters[SaveTransactionMetadata]) error { +func (m *MockController) SaveTransactionMetadata(ctx context.Context, parameters Parameters[SaveTransactionMetadata]) (*ledger.Log, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SaveTransactionMetadata", ctx, parameters) - ret0, _ := ret[0].(error) - return ret0 + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(error) + return ret0, ret1 } // SaveTransactionMetadata indicates an expected call of SaveTransactionMetadata. diff --git a/internal/controller/ledger/controller_with_events.go b/internal/controller/ledger/controller_with_events.go index 16d4886bd..e704bae78 100644 --- a/internal/controller/ledger/controller_with_events.go +++ b/internal/controller/ledger/controller_with_events.go @@ -19,22 +19,22 @@ func NewControllerWithEvents(ledger ledger.Ledger, underlying Controller, listen listener: listener, } } -func (ctrl *ControllerWithEvents) CreateTransaction(ctx context.Context, parameters Parameters[RunScript]) (*ledger.CreatedTransaction, error) { - ret, err := ctrl.Controller.CreateTransaction(ctx, parameters) +func (ctrl *ControllerWithEvents) CreateTransaction(ctx context.Context, parameters Parameters[RunScript]) (*ledger.Log, *ledger.CreatedTransaction, error) { + log, ret, err := ctrl.Controller.CreateTransaction(ctx, parameters) if err != nil { - return nil, err + return nil, nil, err } if !parameters.DryRun { ctrl.listener.CommittedTransactions(ctx, ctrl.ledger.Name, ret.Transaction, ret.AccountMetadata) } - return ret, nil + return log, ret, nil } -func (ctrl *ControllerWithEvents) RevertTransaction(ctx context.Context, parameters Parameters[RevertTransaction]) (*ledger.RevertedTransaction, error) { - ret, err := ctrl.Controller.RevertTransaction(ctx, parameters) +func (ctrl *ControllerWithEvents) RevertTransaction(ctx context.Context, parameters Parameters[RevertTransaction]) (*ledger.Log, *ledger.RevertedTransaction, error) { + log, ret, err := ctrl.Controller.RevertTransaction(ctx, parameters) if err != nil { - return nil, err + return nil, nil, err } if !parameters.DryRun { ctrl.listener.RevertedTransaction( @@ -45,13 +45,13 @@ func (ctrl *ControllerWithEvents) RevertTransaction(ctx context.Context, paramet ) } - return ret, nil + return log, ret, nil } -func (ctrl *ControllerWithEvents) SaveTransactionMetadata(ctx context.Context, parameters Parameters[SaveTransactionMetadata]) error { - err := ctrl.Controller.SaveTransactionMetadata(ctx, parameters) +func (ctrl *ControllerWithEvents) SaveTransactionMetadata(ctx context.Context, parameters Parameters[SaveTransactionMetadata]) (*ledger.Log, error) { + log, err := ctrl.Controller.SaveTransactionMetadata(ctx, parameters) if err != nil { - return err + return nil, err } if !parameters.DryRun { ctrl.listener.SavedMetadata( @@ -63,13 +63,13 @@ func (ctrl *ControllerWithEvents) SaveTransactionMetadata(ctx context.Context, p ) } - return nil + return log, nil } -func (ctrl *ControllerWithEvents) SaveAccountMetadata(ctx context.Context, parameters Parameters[SaveAccountMetadata]) error { - err := ctrl.Controller.SaveAccountMetadata(ctx, parameters) +func (ctrl *ControllerWithEvents) SaveAccountMetadata(ctx context.Context, parameters Parameters[SaveAccountMetadata]) (*ledger.Log, error) { + log, err := ctrl.Controller.SaveAccountMetadata(ctx, parameters) if err != nil { - return err + return nil, err } if !parameters.DryRun { ctrl.listener.SavedMetadata( @@ -81,13 +81,13 @@ func (ctrl *ControllerWithEvents) SaveAccountMetadata(ctx context.Context, param ) } - return nil + return log, nil } -func (ctrl *ControllerWithEvents) DeleteTransactionMetadata(ctx context.Context, parameters Parameters[DeleteTransactionMetadata]) error { - err := ctrl.Controller.DeleteTransactionMetadata(ctx, parameters) +func (ctrl *ControllerWithEvents) DeleteTransactionMetadata(ctx context.Context, parameters Parameters[DeleteTransactionMetadata]) (*ledger.Log, error) { + log, err := ctrl.Controller.DeleteTransactionMetadata(ctx, parameters) if err != nil { - return err + return nil, err } if !parameters.DryRun { ctrl.listener.DeletedMetadata( @@ -99,13 +99,13 @@ func (ctrl *ControllerWithEvents) DeleteTransactionMetadata(ctx context.Context, ) } - return nil + return log, nil } -func (ctrl *ControllerWithEvents) DeleteAccountMetadata(ctx context.Context, parameters Parameters[DeleteAccountMetadata]) error { - err := ctrl.Controller.DeleteAccountMetadata(ctx, parameters) +func (ctrl *ControllerWithEvents) DeleteAccountMetadata(ctx context.Context, parameters Parameters[DeleteAccountMetadata]) (*ledger.Log, error) { + log, err := ctrl.Controller.DeleteAccountMetadata(ctx, parameters) if err != nil { - return err + return nil, err } if !parameters.DryRun { ctrl.listener.DeletedMetadata( @@ -117,7 +117,7 @@ func (ctrl *ControllerWithEvents) DeleteAccountMetadata(ctx context.Context, par ) } - return nil + return log, nil } var _ Controller = (*ControllerWithEvents)(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 911fcb6fb..01b864d63 100644 --- a/internal/controller/ledger/controller_with_too_many_client_handling.go +++ b/internal/controller/ledger/controller_with_too_many_client_handling.go @@ -38,40 +38,45 @@ func NewControllerWithTooManyClientHandling( } } -func (ctrl *ControllerWithTooManyClientHandling) CreateTransaction(ctx context.Context, parameters Parameters[RunScript]) (*ledger.CreatedTransaction, error) { +func (ctrl *ControllerWithTooManyClientHandling) CreateTransaction(ctx context.Context, parameters Parameters[RunScript]) (*ledger.Log, *ledger.CreatedTransaction, error) { return handleRetry(ctx, ctrl.tracer, ctrl.delayCalculator, parameters, ctrl.Controller.CreateTransaction) } -func (ctrl *ControllerWithTooManyClientHandling) RevertTransaction(ctx context.Context, parameters Parameters[RevertTransaction]) (*ledger.RevertedTransaction, error) { +func (ctrl *ControllerWithTooManyClientHandling) RevertTransaction(ctx context.Context, parameters Parameters[RevertTransaction]) (*ledger.Log, *ledger.RevertedTransaction, error) { return handleRetry(ctx, ctrl.tracer, ctrl.delayCalculator, parameters, ctrl.Controller.RevertTransaction) } -func (ctrl *ControllerWithTooManyClientHandling) SaveTransactionMetadata(ctx context.Context, parameters Parameters[SaveTransactionMetadata]) error { - _, err := handleRetry(ctx, ctrl.tracer, ctrl.delayCalculator, parameters, func(ctx context.Context, parameters Parameters[SaveTransactionMetadata]) (*struct{}, error) { - return nil, ctrl.Controller.SaveTransactionMetadata(ctx, parameters) +func (ctrl *ControllerWithTooManyClientHandling) SaveTransactionMetadata(ctx context.Context, parameters Parameters[SaveTransactionMetadata]) (*ledger.Log, error) { + log, _, err := handleRetry(ctx, ctrl.tracer, ctrl.delayCalculator, parameters, func(ctx context.Context, parameters Parameters[SaveTransactionMetadata]) (*ledger.Log, *struct{}, error) { + log, err := ctrl.Controller.SaveTransactionMetadata(ctx, parameters) + return log, nil, err }) - return err + + return log, err } -func (ctrl *ControllerWithTooManyClientHandling) SaveAccountMetadata(ctx context.Context, parameters Parameters[SaveAccountMetadata]) error { - _, err := handleRetry(ctx, ctrl.tracer, ctrl.delayCalculator, parameters, func(ctx context.Context, parameters Parameters[SaveAccountMetadata]) (*struct{}, error) { - return nil, ctrl.Controller.SaveAccountMetadata(ctx, parameters) +func (ctrl *ControllerWithTooManyClientHandling) SaveAccountMetadata(ctx context.Context, parameters Parameters[SaveAccountMetadata]) (*ledger.Log, error) { + log, _, err := handleRetry(ctx, ctrl.tracer, ctrl.delayCalculator, parameters, func(ctx context.Context, parameters Parameters[SaveAccountMetadata]) (*ledger.Log, *struct{}, error) { + log, err := ctrl.Controller.SaveAccountMetadata(ctx, parameters) + return log, nil, err }) - return err + return log, err } -func (ctrl *ControllerWithTooManyClientHandling) DeleteTransactionMetadata(ctx context.Context, parameters Parameters[DeleteTransactionMetadata]) error { - _, err := handleRetry(ctx, ctrl.tracer, ctrl.delayCalculator, parameters, func(ctx context.Context, parameters Parameters[DeleteTransactionMetadata]) (*struct{}, error) { - return nil, ctrl.Controller.DeleteTransactionMetadata(ctx, parameters) +func (ctrl *ControllerWithTooManyClientHandling) DeleteTransactionMetadata(ctx context.Context, parameters Parameters[DeleteTransactionMetadata]) (*ledger.Log, error) { + log, _, err := handleRetry(ctx, ctrl.tracer, ctrl.delayCalculator, parameters, func(ctx context.Context, parameters Parameters[DeleteTransactionMetadata]) (*ledger.Log, *struct{}, error) { + log, err := ctrl.Controller.DeleteTransactionMetadata(ctx, parameters) + return log, nil, err }) - return err + return log, err } -func (ctrl *ControllerWithTooManyClientHandling) DeleteAccountMetadata(ctx context.Context, parameters Parameters[DeleteAccountMetadata]) error { - _, err := handleRetry(ctx, ctrl.tracer, ctrl.delayCalculator, parameters, func(ctx context.Context, parameters Parameters[DeleteAccountMetadata]) (*struct{}, error) { - return nil, ctrl.Controller.DeleteAccountMetadata(ctx, parameters) +func (ctrl *ControllerWithTooManyClientHandling) DeleteAccountMetadata(ctx context.Context, parameters Parameters[DeleteAccountMetadata]) (*ledger.Log, error) { + log, _, err := handleRetry(ctx, ctrl.tracer, ctrl.delayCalculator, parameters, func(ctx context.Context, parameters Parameters[DeleteAccountMetadata]) (*ledger.Log, *struct{}, error) { + log, err := ctrl.Controller.DeleteAccountMetadata(ctx, parameters) + return log, nil, err }) - return err + return log, err } var _ Controller = (*ControllerWithTooManyClientHandling)(nil) @@ -81,29 +86,29 @@ func handleRetry[INPUT, OUTPUT any]( tracer trace.Tracer, delayCalculator DelayCalculator, parameters Parameters[INPUT], - fn func(ctx context.Context, parameters Parameters[INPUT]) (*OUTPUT, error), -) (*OUTPUT, error) { + fn func(ctx context.Context, parameters Parameters[INPUT]) (*ledger.Log, *OUTPUT, error), +) (*ledger.Log, *OUTPUT, error) { ctx, span := tracer.Start(ctx, "TooManyClientRetrier") defer span.End() count := 0 for { - output, err := fn(ctx, parameters) + log, output, err := fn(ctx, parameters) if err != nil && errors.Is(err, postgres.ErrTooManyClient{}) { delay := delayCalculator.Next(count) if delay == 0 { - return nil, err + return nil, nil, err } select { case <-ctx.Done(): - return nil, ctx.Err() + return nil, nil, ctx.Err() case <-time.After(delay): count++ span.SetAttributes(attribute.Int("retry", count)) continue } } - return output, err + return log, output, err } } diff --git a/internal/controller/ledger/controller_with_too_many_client_handling_test.go b/internal/controller/ledger/controller_with_too_many_client_handling_test.go index 2dca515f8..1d172da60 100644 --- a/internal/controller/ledger/controller_with_too_many_client_handling_test.go +++ b/internal/controller/ledger/controller_with_too_many_client_handling_test.go @@ -27,12 +27,12 @@ func TestNewControllerWithTooManyClientHandling(t *testing.T) { underlyingLedgerController.EXPECT(). CreateTransaction(gomock.Any(), parameters). - Return(nil, postgres.ErrTooManyClient{}). + Return(nil, nil, postgres.ErrTooManyClient{}). Times(2) underlyingLedgerController.EXPECT(). CreateTransaction(gomock.Any(), parameters). - Return(&ledger.CreatedTransaction{ + Return(&ledger.Log{}, &ledger.CreatedTransaction{ Transaction: ledger.NewTransaction(), }, nil) @@ -45,7 +45,7 @@ func TestNewControllerWithTooManyClientHandling(t *testing.T) { Return(10 * time.Millisecond) ledgerController := NewControllerWithTooManyClientHandling(underlyingLedgerController, noop.Tracer{}, delayCalculator) - _, err := ledgerController.CreateTransaction(ctx, parameters) + _, _, err := ledgerController.CreateTransaction(ctx, parameters) require.NoError(t, err) }) @@ -61,7 +61,7 @@ func TestNewControllerWithTooManyClientHandling(t *testing.T) { underlyingLedgerController.EXPECT(). CreateTransaction(gomock.Any(), parameters). - Return(nil, postgres.ErrTooManyClient{}). + Return(nil, nil, postgres.ErrTooManyClient{}). Times(2) delayCalculator.EXPECT(). @@ -73,7 +73,7 @@ func TestNewControllerWithTooManyClientHandling(t *testing.T) { Return(time.Duration(0)) ledgerController := NewControllerWithTooManyClientHandling(underlyingLedgerController, noop.Tracer{}, delayCalculator) - _, err := ledgerController.CreateTransaction(ctx, parameters) + _, _, err := ledgerController.CreateTransaction(ctx, parameters) require.Error(t, err) require.True(t, errors.Is(err, postgres.ErrTooManyClient{})) }) diff --git a/internal/controller/ledger/controller_with_traces.go b/internal/controller/ledger/controller_with_traces.go index 9c25bea1a..20b505323 100644 --- a/internal/controller/ledger/controller_with_traces.go +++ b/internal/controller/ledger/controller_with_traces.go @@ -98,40 +98,46 @@ func (ctrl *ControllerWithTraces) GetVolumesWithBalances(ctx context.Context, q }) } -func (ctrl *ControllerWithTraces) CreateTransaction(ctx context.Context, parameters Parameters[RunScript]) (*ledger.CreatedTransaction, error) { - return tracing.Trace(ctx, ctrl.tracer, "CreateTransaction", func(ctx context.Context) (*ledger.CreatedTransaction, error) { - return ctrl.underlying.CreateTransaction(ctx, parameters) - }) +func (ctrl *ControllerWithTraces) CreateTransaction(ctx context.Context, parameters Parameters[RunScript]) (*ledger.Log, *ledger.CreatedTransaction, error) { + ctx, span := ctrl.tracer.Start(ctx, "CreateTransaction") + defer span.End() + + return ctrl.underlying.CreateTransaction(ctx, parameters) } -func (ctrl *ControllerWithTraces) RevertTransaction(ctx context.Context, parameters Parameters[RevertTransaction]) (*ledger.RevertedTransaction, error) { - return tracing.Trace(ctx, ctrl.tracer, "RevertTransaction", func(ctx context.Context) (*ledger.RevertedTransaction, error) { - return ctrl.underlying.RevertTransaction(ctx, parameters) - }) +func (ctrl *ControllerWithTraces) RevertTransaction(ctx context.Context, parameters Parameters[RevertTransaction]) (*ledger.Log, *ledger.RevertedTransaction, error) { + ctx, span := ctrl.tracer.Start(ctx, "RevertTransaction") + defer span.End() + + return ctrl.underlying.RevertTransaction(ctx, parameters) } -func (ctrl *ControllerWithTraces) SaveTransactionMetadata(ctx context.Context, parameters Parameters[SaveTransactionMetadata]) error { - return tracing.SkipResult(tracing.Trace(ctx, ctrl.tracer, "SaveTransactionMetadata", tracing.NoResult(func(ctx context.Context) error { - return ctrl.underlying.SaveTransactionMetadata(ctx, parameters) - }))) +func (ctrl *ControllerWithTraces) SaveTransactionMetadata(ctx context.Context, parameters Parameters[SaveTransactionMetadata]) (*ledger.Log, error) { + ctx, span := ctrl.tracer.Start(ctx, "SaveTransactionMetadata") + defer span.End() + + return ctrl.underlying.SaveTransactionMetadata(ctx, parameters) } -func (ctrl *ControllerWithTraces) SaveAccountMetadata(ctx context.Context, parameters Parameters[SaveAccountMetadata]) error { - return tracing.SkipResult(tracing.Trace(ctx, ctrl.tracer, "SaveAccountMetadata", tracing.NoResult(func(ctx context.Context) error { - return ctrl.underlying.SaveAccountMetadata(ctx, parameters) - }))) +func (ctrl *ControllerWithTraces) SaveAccountMetadata(ctx context.Context, parameters Parameters[SaveAccountMetadata]) (*ledger.Log, error) { + ctx, span := ctrl.tracer.Start(ctx, "SaveAccountMetadata") + defer span.End() + + return ctrl.underlying.SaveAccountMetadata(ctx, parameters) } -func (ctrl *ControllerWithTraces) DeleteTransactionMetadata(ctx context.Context, parameters Parameters[DeleteTransactionMetadata]) error { - return tracing.SkipResult(tracing.Trace(ctx, ctrl.tracer, "DeleteTransactionMetadata", tracing.NoResult(func(ctx context.Context) error { - return ctrl.underlying.DeleteTransactionMetadata(ctx, parameters) - }))) +func (ctrl *ControllerWithTraces) DeleteTransactionMetadata(ctx context.Context, parameters Parameters[DeleteTransactionMetadata]) (*ledger.Log, error) { + ctx, span := ctrl.tracer.Start(ctx, "DeleteTransactionMetadata") + defer span.End() + + return ctrl.underlying.DeleteTransactionMetadata(ctx, parameters) } -func (ctrl *ControllerWithTraces) DeleteAccountMetadata(ctx context.Context, parameters Parameters[DeleteAccountMetadata]) error { - return tracing.SkipResult(tracing.Trace(ctx, ctrl.tracer, "DeleteAccountMetadata", tracing.NoResult(func(ctx context.Context) error { - return ctrl.underlying.DeleteAccountMetadata(ctx, parameters) - }))) +func (ctrl *ControllerWithTraces) DeleteAccountMetadata(ctx context.Context, parameters Parameters[DeleteAccountMetadata]) (*ledger.Log, error) { + ctx, span := ctrl.tracer.Start(ctx, "DeleteAccountMetadata") + defer span.End() + + return ctrl.underlying.DeleteAccountMetadata(ctx, parameters) } func (ctrl *ControllerWithTraces) GetStats(ctx context.Context) (Stats, error) { diff --git a/internal/controller/ledger/log_process.go b/internal/controller/ledger/log_process.go index cb09770d9..6415a812d 100644 --- a/internal/controller/ledger/log_process.go +++ b/internal/controller/ledger/log_process.go @@ -30,14 +30,17 @@ func (lp *logProcessor[INPUT, OUTPUT]) runTx( store Store, parameters Parameters[INPUT], fn func(ctx context.Context, sqlTX TX, parameters Parameters[INPUT]) (*OUTPUT, error), -) (*OUTPUT, error) { - var payload *OUTPUT +) (*ledger.Log, *OUTPUT, error) { + var ( + output *OUTPUT + log ledger.Log + ) err := store.WithTX(ctx, nil, func(tx TX) (commit bool, err error) { - payload, err = fn(ctx, tx, parameters) + output, err = fn(ctx, tx, parameters) if err != nil { return false, err } - log := ledger.NewLog(*payload) + log = ledger.NewLog(*output) log.IdempotencyKey = parameters.IdempotencyKey log.IdempotencyHash = ledger.ComputeIdempotencyHash(parameters.Input) @@ -53,7 +56,7 @@ func (lp *logProcessor[INPUT, OUTPUT]) runTx( return true, nil }) - return payload, err + return &log, output, err } func (lp *logProcessor[INPUT, OUTPUT]) forgeLog( @@ -61,19 +64,19 @@ func (lp *logProcessor[INPUT, OUTPUT]) forgeLog( store Store, parameters Parameters[INPUT], fn func(ctx context.Context, sqlTX TX, parameters Parameters[INPUT]) (*OUTPUT, error), -) (*OUTPUT, error) { +) (*ledger.Log, *OUTPUT, error) { if parameters.IdempotencyKey != "" { - output, err := lp.fetchLogWithIK(ctx, store, parameters) + log, output, err := lp.fetchLogWithIK(ctx, store, parameters) if err != nil { - return nil, err + return nil, nil, err } if output != nil { - return output, nil + return log, output, nil } } for { - output, err := lp.runTx(ctx, store, parameters, fn) + log, output, err := lp.runTx(ctx, store, parameters, fn) if err != nil { switch { case errors.Is(err, postgres.ErrDeadlockDetected): @@ -85,39 +88,39 @@ func (lp *logProcessor[INPUT, OUTPUT]) forgeLog( continue // A log with the IK could have been inserted in the meantime, read again the database to retrieve it case errors.Is(err, ErrIdempotencyKeyConflict{}): - output, err := lp.fetchLogWithIK(ctx, store, parameters) + log, output, err := lp.fetchLogWithIK(ctx, store, parameters) if err != nil { - return nil, err + return nil, nil, err } if output == nil { panic("incoherent error, received duplicate IK but log not found in database") } - return output, nil + return log, output, nil default: - return nil, fmt.Errorf("unexpected error while forging log: %w", err) + return nil, nil, fmt.Errorf("unexpected error while forging log: %w", err) } } - return output, nil + return log, output, nil } } -func (lp *logProcessor[INPUT, OUTPUT]) fetchLogWithIK(ctx context.Context, store Store, parameters Parameters[INPUT]) (*OUTPUT, error) { +func (lp *logProcessor[INPUT, OUTPUT]) fetchLogWithIK(ctx context.Context, store Store, parameters Parameters[INPUT]) (*ledger.Log, *OUTPUT, error) { log, err := store.ReadLogWithIdempotencyKey(ctx, parameters.IdempotencyKey) if err != nil && !errors.Is(err, postgres.ErrNotFound) { - return nil, err + return nil, nil, err } if err == nil { // notes(gfyrag): idempotency hash should never be empty in this case, but data from previous // ledger version does not have this field and it cannot be recomputed if log.IdempotencyHash != "" { if computedHash := ledger.ComputeIdempotencyHash(parameters.Input); log.IdempotencyHash != computedHash { - return nil, newErrInvalidIdempotencyInputs(log.IdempotencyKey, log.IdempotencyHash, computedHash) + return nil, nil, newErrInvalidIdempotencyInputs(log.IdempotencyKey, log.IdempotencyHash, computedHash) } } - return pointer.For(log.Data.(OUTPUT)), nil + return log, pointer.For(log.Data.(OUTPUT)), nil } - return nil, nil + return nil, nil, nil } diff --git a/internal/controller/ledger/log_process_test.go b/internal/controller/ledger/log_process_test.go index 8b58981e2..544b42848 100644 --- a/internal/controller/ledger/log_process_test.go +++ b/internal/controller/ledger/log_process_test.go @@ -32,7 +32,7 @@ func TestForgeLogWithIKConflict(t *testing.T) { }, nil) lp := newLogProcessor[RunScript, ledger.CreatedTransaction]("foo", noop.Int64Counter{}) - _, err := lp.forgeLog(ctx, store, Parameters[RunScript]{ + _, _, err := lp.forgeLog(ctx, store, Parameters[RunScript]{ IdempotencyKey: "foo", }, nil) require.NoError(t, err) @@ -56,6 +56,6 @@ func TestForgeLogWithDeadlock(t *testing.T) { Return(nil) lp := newLogProcessor[RunScript, ledger.CreatedTransaction]("foo", noop.Int64Counter{}) - _, err := lp.forgeLog(ctx, store, Parameters[RunScript]{}, nil) + _, _, err := lp.forgeLog(ctx, store, Parameters[RunScript]{}, nil) require.NoError(t, err) } diff --git a/openapi.yaml b/openapi.yaml index 325d63003..81042b8ec 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -3658,8 +3658,11 @@ components: properties: responseType: type: string + logID: + type: integer required: - responseType + - logID V2BulkElementResultCreateTransaction: allOf: - $ref: '#/components/schemas/V2BaseBulkElementResult' diff --git a/openapi/v2.yaml b/openapi/v2.yaml index b7cb740f0..137eb09c8 100644 --- a/openapi/v2.yaml +++ b/openapi/v2.yaml @@ -1928,8 +1928,11 @@ components: properties: responseType: type: string + logID: + type: integer required: - responseType + - logID V2BulkElementResultCreateTransaction: allOf: - $ref: "#/components/schemas/V2BaseBulkElementResult" diff --git a/pkg/client/.speakeasy/gen.lock b/pkg/client/.speakeasy/gen.lock index 0474584bd..984658c2d 100644 --- a/pkg/client/.speakeasy/gen.lock +++ b/pkg/client/.speakeasy/gen.lock @@ -1,12 +1,12 @@ lockVersion: 2.0.0 id: a9ac79e1-e429-4ee3-96c4-ec973f19bec3 management: - docChecksum: b360e18f2833b14257df2742a09a1999 + docChecksum: 5e24fc96851e508606f6b6668ed3ffb3 docVersion: v1 speakeasyVersion: 1.351.0 generationVersion: 2.384.1 - releaseVersion: 0.4.27 - configChecksum: 9a9eff6bf575499aecf0bdcfa3f6d80b + releaseVersion: 0.4.28 + configChecksum: 9eb898e4ab7291afdada27c15d20aa5d features: go: additionalDependencies: 0.1.0 diff --git a/pkg/client/.speakeasy/gen.yaml b/pkg/client/.speakeasy/gen.yaml index 1cbee3f5c..943f49877 100644 --- a/pkg/client/.speakeasy/gen.yaml +++ b/pkg/client/.speakeasy/gen.yaml @@ -15,7 +15,7 @@ generation: auth: oAuth2ClientCredentialsEnabled: true go: - version: 0.4.27 + version: 0.4.28 additionalDependencies: {} allowUnknownFieldsInWeakUnions: false clientServerStatusCodesAsErrors: true diff --git a/pkg/client/docs/models/components/v2bulkelementresultaddmetadata.md b/pkg/client/docs/models/components/v2bulkelementresultaddmetadata.md index 5f8d1fefb..ae6b68121 100644 --- a/pkg/client/docs/models/components/v2bulkelementresultaddmetadata.md +++ b/pkg/client/docs/models/components/v2bulkelementresultaddmetadata.md @@ -5,4 +5,5 @@ | Field | Type | Required | Description | | ------------------ | ------------------ | ------------------ | ------------------ | -| `ResponseType` | *string* | :heavy_check_mark: | N/A | \ No newline at end of file +| `ResponseType` | *string* | :heavy_check_mark: | N/A | +| `LogID` | *int64* | :heavy_check_mark: | N/A | \ No newline at end of file diff --git a/pkg/client/docs/models/components/v2bulkelementresultcreatetransaction.md b/pkg/client/docs/models/components/v2bulkelementresultcreatetransaction.md index eac3e3d86..dc6c43bba 100644 --- a/pkg/client/docs/models/components/v2bulkelementresultcreatetransaction.md +++ b/pkg/client/docs/models/components/v2bulkelementresultcreatetransaction.md @@ -6,4 +6,5 @@ | Field | Type | Required | Description | | -------------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------- | | `ResponseType` | *string* | :heavy_check_mark: | N/A | +| `LogID` | *int64* | :heavy_check_mark: | N/A | | `Data` | [components.V2Transaction](../../models/components/v2transaction.md) | :heavy_check_mark: | N/A | \ No newline at end of file diff --git a/pkg/client/docs/models/components/v2bulkelementresultdeletemetadata.md b/pkg/client/docs/models/components/v2bulkelementresultdeletemetadata.md index 40a884420..ca68c3046 100644 --- a/pkg/client/docs/models/components/v2bulkelementresultdeletemetadata.md +++ b/pkg/client/docs/models/components/v2bulkelementresultdeletemetadata.md @@ -5,4 +5,5 @@ | Field | Type | Required | Description | | ------------------ | ------------------ | ------------------ | ------------------ | -| `ResponseType` | *string* | :heavy_check_mark: | N/A | \ No newline at end of file +| `ResponseType` | *string* | :heavy_check_mark: | N/A | +| `LogID` | *int64* | :heavy_check_mark: | N/A | \ No newline at end of file diff --git a/pkg/client/docs/models/components/v2bulkelementresulterror.md b/pkg/client/docs/models/components/v2bulkelementresulterror.md index 1fdefd125..e0af2417a 100644 --- a/pkg/client/docs/models/components/v2bulkelementresulterror.md +++ b/pkg/client/docs/models/components/v2bulkelementresulterror.md @@ -6,6 +6,7 @@ | Field | Type | Required | Description | | ------------------ | ------------------ | ------------------ | ------------------ | | `ResponseType` | *string* | :heavy_check_mark: | N/A | +| `LogID` | *int64* | :heavy_check_mark: | N/A | | `ErrorCode` | *string* | :heavy_check_mark: | N/A | | `ErrorDescription` | *string* | :heavy_check_mark: | N/A | | `ErrorDetails` | **string* | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/pkg/client/docs/models/components/v2bulkelementresultreverttransaction.md b/pkg/client/docs/models/components/v2bulkelementresultreverttransaction.md index b85016b60..23bf770d5 100644 --- a/pkg/client/docs/models/components/v2bulkelementresultreverttransaction.md +++ b/pkg/client/docs/models/components/v2bulkelementresultreverttransaction.md @@ -6,4 +6,5 @@ | Field | Type | Required | Description | | -------------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------- | | `ResponseType` | *string* | :heavy_check_mark: | N/A | +| `LogID` | *int64* | :heavy_check_mark: | N/A | | `Data` | [components.V2Transaction](../../models/components/v2transaction.md) | :heavy_check_mark: | N/A | \ No newline at end of file diff --git a/pkg/client/formance.go b/pkg/client/formance.go index 9246adb7e..27f37ab7a 100644 --- a/pkg/client/formance.go +++ b/pkg/client/formance.go @@ -143,9 +143,9 @@ func New(opts ...SDKOption) *Formance { sdkConfiguration: sdkConfiguration{ Language: "go", OpenAPIDocVersion: "v1", - SDKVersion: "0.4.27", + SDKVersion: "0.4.28", GenVersion: "2.384.1", - UserAgent: "speakeasy-sdk/go 0.4.27 2.384.1 v1 github.com/formancehq/ledger/pkg/client", + UserAgent: "speakeasy-sdk/go 0.4.28 2.384.1 v1 github.com/formancehq/ledger/pkg/client", Hooks: hooks.New(), }, } diff --git a/pkg/client/models/components/v2bulkelementresult.go b/pkg/client/models/components/v2bulkelementresult.go index 31623ee90..0f18b1cb2 100644 --- a/pkg/client/models/components/v2bulkelementresult.go +++ b/pkg/client/models/components/v2bulkelementresult.go @@ -11,6 +11,7 @@ import ( type V2BulkElementResultError struct { ResponseType string `json:"responseType"` + LogID int64 `json:"logID"` ErrorCode string `json:"errorCode"` ErrorDescription string `json:"errorDescription"` ErrorDetails *string `json:"errorDetails,omitempty"` @@ -23,6 +24,13 @@ func (o *V2BulkElementResultError) GetResponseType() string { return o.ResponseType } +func (o *V2BulkElementResultError) GetLogID() int64 { + if o == nil { + return 0 + } + return o.LogID +} + func (o *V2BulkElementResultError) GetErrorCode() string { if o == nil { return "" @@ -46,6 +54,7 @@ func (o *V2BulkElementResultError) GetErrorDetails() *string { type V2BulkElementResultDeleteMetadata struct { ResponseType string `json:"responseType"` + LogID int64 `json:"logID"` } func (o *V2BulkElementResultDeleteMetadata) GetResponseType() string { @@ -55,8 +64,16 @@ func (o *V2BulkElementResultDeleteMetadata) GetResponseType() string { return o.ResponseType } +func (o *V2BulkElementResultDeleteMetadata) GetLogID() int64 { + if o == nil { + return 0 + } + return o.LogID +} + type V2BulkElementResultRevertTransaction struct { ResponseType string `json:"responseType"` + LogID int64 `json:"logID"` Data V2Transaction `json:"data"` } @@ -67,6 +84,13 @@ func (o *V2BulkElementResultRevertTransaction) GetResponseType() string { return o.ResponseType } +func (o *V2BulkElementResultRevertTransaction) GetLogID() int64 { + if o == nil { + return 0 + } + return o.LogID +} + func (o *V2BulkElementResultRevertTransaction) GetData() V2Transaction { if o == nil { return V2Transaction{} @@ -76,6 +100,7 @@ func (o *V2BulkElementResultRevertTransaction) GetData() V2Transaction { type V2BulkElementResultAddMetadata struct { ResponseType string `json:"responseType"` + LogID int64 `json:"logID"` } func (o *V2BulkElementResultAddMetadata) GetResponseType() string { @@ -85,8 +110,16 @@ func (o *V2BulkElementResultAddMetadata) GetResponseType() string { return o.ResponseType } +func (o *V2BulkElementResultAddMetadata) GetLogID() int64 { + if o == nil { + return 0 + } + return o.LogID +} + type V2BulkElementResultCreateTransaction struct { ResponseType string `json:"responseType"` + LogID int64 `json:"logID"` Data V2Transaction `json:"data"` } @@ -97,6 +130,13 @@ func (o *V2BulkElementResultCreateTransaction) GetResponseType() string { return o.ResponseType } +func (o *V2BulkElementResultCreateTransaction) GetLogID() int64 { + if o == nil { + return 0 + } + return o.LogID +} + func (o *V2BulkElementResultCreateTransaction) GetData() V2Transaction { if o == nil { return V2Transaction{} diff --git a/pkg/generate/generator.go b/pkg/generate/generator.go index 52d041b4e..0b1bfcd5d 100644 --- a/pkg/generate/generator.go +++ b/pkg/generate/generator.go @@ -1,42 +1,248 @@ package generate import ( + "context" + "encoding/json" + "errors" + "fmt" "github.com/dop251/goja" + ledger "github.com/formancehq/ledger/internal" + v2 "github.com/formancehq/ledger/internal/api/v2" + ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" + "github.com/formancehq/ledger/pkg/client" + "github.com/formancehq/ledger/pkg/client/models/components" + "github.com/formancehq/ledger/pkg/client/models/operations" "github.com/google/uuid" + "math/big" + "time" ) +type Action struct { + v2.BulkElement +} + type Result struct { - Script string `json:"script"` - Variables map[string]string `json:"variables"` + components.V2BulkElementResult +} + +func (r Result) GetLogID() int64 { + switch r.Type { + case components.V2BulkElementResultTypeCreateTransaction: + return r.V2BulkElementResultCreateTransaction.LogID + case components.V2BulkElementResultTypeAddMetadata: + return r.V2BulkElementResultAddMetadata.LogID + case components.V2BulkElementResultTypeDeleteMetadata: + return r.V2BulkElementResultDeleteMetadata.LogID + case components.V2BulkElementResultTypeRevertTransaction: + return r.V2BulkElementResultRevertTransaction.LogID + default: + panic(fmt.Sprintf("unexpected result type: %s", r.Type)) + } +} + +func (r Action) Apply(ctx context.Context, client *client.V2, l string) (*Result, error) { + + var bulkElement components.V2BulkElement + switch r.Action { + case v2.ActionCreateTransaction, "": // Handling "" as CREATE_TRANSACTION for backward compatibility + transactionRequest := &ledgercontroller.RunScript{} + err := json.Unmarshal(r.Data, transactionRequest) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal transaction request: %w", err) + } + + bulkElement = components.CreateV2BulkElementCreateTransaction(components.V2BulkElementCreateTransaction{ + Data: &components.V2PostTransaction{ + Timestamp: func() *time.Time { + if transactionRequest.Timestamp.IsZero() { + return nil + } + return &transactionRequest.Timestamp.Time + }(), + Script: &components.V2PostTransactionScript{ + Plain: transactionRequest.Script.Plain, + Vars: transactionRequest.Script.Vars, + }, + Reference: func() *string { + if transactionRequest.Reference == "" { + return nil + } + return &transactionRequest.Reference + }(), + Metadata: transactionRequest.Metadata, + }, + }) + case v2.ActionAddMetadata: + addMetadataRequest := &v2.AddMetadataRequest{} + err := json.Unmarshal(r.Data, addMetadataRequest) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal add metadata request: %w", err) + } + + var targetID components.V2TargetID + switch addMetadataRequest.TargetType { + case ledger.MetaTargetTypeAccount: + var targetIDStr string + if err := json.Unmarshal(addMetadataRequest.TargetID, &targetIDStr); err != nil { + return nil, fmt.Errorf("failed to unmarshal target id: %w", err) + } + targetID = components.CreateV2TargetIDStr(targetIDStr) + case ledger.MetaTargetTypeTransaction: + var targetIDInt int + if err := json.Unmarshal(addMetadataRequest.TargetID, &targetIDInt); err != nil { + return nil, fmt.Errorf("failed to unmarshal target id: %w", err) + } + targetID = components.CreateV2TargetIDBigint(big.NewInt(int64(targetIDInt))) + default: + panic("unexpected target id type") + } + + bulkElement = components.CreateV2BulkElementAddMetadata(components.V2BulkElementAddMetadata{ + Data: &components.Data{ + TargetID: targetID, + TargetType: components.V2TargetType(addMetadataRequest.TargetType), + Metadata: addMetadataRequest.Metadata, + }, + }) + case v2.ActionDeleteMetadata: + deleteMetadataRequest := &v2.DeleteMetadataRequest{} + err := json.Unmarshal(r.Data, deleteMetadataRequest) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal delete metadata request: %w", err) + } + + var targetID components.V2TargetID + switch deleteMetadataRequest.TargetType { + case ledger.MetaTargetTypeAccount: + var targetIDStr string + if err := json.Unmarshal(deleteMetadataRequest.TargetID, &targetIDStr); err != nil { + return nil, fmt.Errorf("failed to unmarshal target id: %w", err) + } + targetID = components.CreateV2TargetIDStr(targetIDStr) + case ledger.MetaTargetTypeTransaction: + var targetIDInt int + if err := json.Unmarshal(deleteMetadataRequest.TargetID, &targetIDInt); err != nil { + return nil, fmt.Errorf("failed to unmarshal target id: %w", err) + } + targetID = components.CreateV2TargetIDBigint(big.NewInt(int64(targetIDInt))) + default: + panic("unexpected target id type") + } + + bulkElement = components.CreateV2BulkElementDeleteMetadata(components.V2BulkElementDeleteMetadata{ + Data: &components.V2BulkElementDeleteMetadataData{ + TargetID: targetID, + TargetType: components.V2TargetType(deleteMetadataRequest.TargetType), + Key: deleteMetadataRequest.Key, + }, + }) + case v2.ActionRevertTransaction: + revertMetadataRequest := &v2.RevertTransactionRequest{} + err := json.Unmarshal(r.Data, revertMetadataRequest) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal delete metadata request: %w", err) + } + + bulkElement = components.CreateV2BulkElementRevertTransaction(components.V2BulkElementRevertTransaction{ + Data: &components.V2BulkElementRevertTransactionData{ + ID: big.NewInt(int64(revertMetadataRequest.ID)), + Force: &revertMetadataRequest.Force, + AtEffectiveDate: &revertMetadataRequest.AtEffectiveDate, + }, + }) + default: + panic("unexpected action") + } + + response, err := client.CreateBulk(ctx, operations.V2CreateBulkRequest{ + Ledger: l, + RequestBody: []components.V2BulkElement{bulkElement}, + }) + if err != nil { + return nil, fmt.Errorf("creating transaction: %w", err) + } + + return &Result{response.V2BulkResponse.Data[0]}, nil } type Generator struct { - next func(int) Result + next func(int) (*Action, error) } -func (g *Generator) Next(iteration int) Result { +func (g *Generator) Next(iteration int) (*Action, error) { return g.next(iteration) } func NewGenerator(script string) (*Generator, error) { runtime := goja.New() + _, err := runtime.RunString(script) if err != nil { return nil, err } + runtime.SetFieldNameMapper(goja.TagFieldNameMapper("json", true)) + err = runtime.Set("uuid", uuid.NewString) if err != nil { return nil, err } - var next func(int) Result + var next func(int) map[string]any err = runtime.ExportTo(runtime.Get("next"), &next) if err != nil { panic(err) } return &Generator{ - next: next, + next: func(i int) (*Action, error) { + ret := next(i) + + var ( + action string + ik string + data map[string]any + ok bool + ) + rawAction := ret["action"] + if rawAction == nil { + return nil, errors.New("'action' must be set") + } + + action, ok = rawAction.(string) + if !ok { + return nil, errors.New("'action' must be a string") + } + + rawData := ret["data"] + if rawData == nil { + return nil, errors.New("'data' must be set") + } + data, ok = rawData.(map[string]any) + if !ok { + return nil, errors.New("'data' must be a map[string]any") + } + + dataAsJsonRawMessage, err := json.Marshal(data) + if err != nil { + return nil, err + } + + rawIK := ret["ik"] + if rawIK != nil { + ik, ok = rawIK.(string) + if !ok { + return nil, errors.New("'ik' must be a string") + } + } + + return &Action{ + BulkElement: v2.BulkElement{ + Action: action, + IdempotencyKey: ik, + Data: dataAsJsonRawMessage, + }, + }, nil + }, }, nil -} \ No newline at end of file +} diff --git a/pkg/generate/generator_test.go b/pkg/generate/generator_test.go new file mode 100644 index 000000000..8f99dc6ba --- /dev/null +++ b/pkg/generate/generator_test.go @@ -0,0 +1,109 @@ +//go:build it + +package generate + +import ( + "github.com/formancehq/go-libs/v2/bun/bunconnect" + "github.com/formancehq/go-libs/v2/logging" + "github.com/formancehq/go-libs/v2/testing/docker" + "github.com/formancehq/go-libs/v2/testing/platform/pgtesting" + "github.com/formancehq/ledger/pkg/client/models/operations" + . "github.com/formancehq/ledger/pkg/testserver" + "github.com/stretchr/testify/require" + "os" + "testing" +) + +func TestGenerator(t *testing.T) { + + dockerPool := docker.NewPool(t, logging.Testing()) + pgServer := pgtesting.CreatePostgresServer(t, dockerPool) + ctx := logging.TestingContext() + + testServer := New(t, Configuration{ + PostgresConfiguration: bunconnect.ConnectionOptions{ + DatabaseSourceName: pgServer.GetDSN(), + }, + Debug: os.Getenv("DEBUG") == "true", + }) + require.NoError(t, testServer.Start()) + t.Cleanup(func() { + require.NoError(t, testServer.Stop(ctx)) + }) + + _, err := testServer.Client().Ledger.V2.CreateLedger(ctx, operations.V2CreateLedgerRequest{ + Ledger: "default", + }) + require.NoError(t, err) + + generator, err := NewGenerator(script) + require.NoError(t, err) + + const ledgerName = "default" + + for i := 0; i < 4; i++ { + action, err := generator.Next(i) + require.NoError(t, err) + + _, err = action.Apply(ctx, testServer.Client().Ledger.V2, ledgerName) + require.NoError(t, err) + } + + txs, err := ListTransactions(ctx, testServer, operations.V2ListTransactionsRequest{ + Ledger: ledgerName, + }) + require.NoError(t, err) + require.Len(t, txs.Data, 2) + require.True(t, txs.Data[1].Reverted) + require.False(t, txs.Data[0].Reverted) + require.Equal(t, map[string]string{ + "foo": "bar", + }, txs.Data[1].Metadata) +} + +const script = ` +function next(iteration) { + switch (iteration % 4) { + case 0: + return { + action: 'CREATE_TRANSACTION', + data: { + plain: ` + "`" + ` +send [USD/2 100] ( + source = @world + destination = @bank +) +` + "`" + ` + } + } + case 1: + return { + action: 'ADD_METADATA', + data: { + targetID: 1, + targetType: 'TRANSACTION', + metadata: { + "foo": "bar", + "foo2": "bar2" + } + } + } + case 2: + return { + action: 'DELETE_METADATA', + data: { + targetID: 1, + targetType: 'TRANSACTION', + key: "foo2" + } + } + case 3: + return { + action: 'REVERT_TRANSACTION', + data: { + id: 1 + } + } + } +} +` diff --git a/test/performance/benchmark_test.go b/test/performance/benchmark_test.go index 3ac8b0f21..2182d827c 100644 --- a/test/performance/benchmark_test.go +++ b/test/performance/benchmark_test.go @@ -7,9 +7,6 @@ import ( "encoding/json" "fmt" . "github.com/formancehq/go-libs/v2/collectionutils" - ledgerclient "github.com/formancehq/ledger/pkg/client" - "github.com/formancehq/ledger/pkg/client/models/components" - "github.com/formancehq/ledger/pkg/client/models/operations" "github.com/formancehq/ledger/pkg/generate" "net/http" "sort" @@ -22,42 +19,41 @@ import ( "github.com/stretchr/testify/require" ) -type TransactionProvider interface { - Get(iteration int) (string, map[string]string) +type ActionProvider interface { + Get(iteration int) (*generate.Action, error) } -type TransactionProviderFn func(iteration int) (string, map[string]string) +type ActionProviderFn func(iteration int) (*generate.Action, error) -func (fn TransactionProviderFn) Get(iteration int) (string, map[string]string) { +func (fn ActionProviderFn) Get(iteration int) (*generate.Action, error) { return fn(iteration) } -type TransactionProviderFactory interface { - Create() (TransactionProvider, error) +type ActionProviderFactory interface { + Create() (ActionProvider, error) } -type TransactionProviderFactoryFn func() (TransactionProvider, error) +type ActionProviderFactoryFn func() (ActionProvider, error) -func (fn TransactionProviderFactoryFn) Create() (TransactionProvider, error) { +func (fn ActionProviderFactoryFn) Create() (ActionProvider, error) { return fn() } -func NewJSTransactionProviderFactory(script string) TransactionProviderFactoryFn { - return func() (TransactionProvider, error) { +func NewJSActionProviderFactory(script string) ActionProviderFactoryFn { + return func() (ActionProvider, error) { generator, err := generate.NewGenerator(script) if err != nil { return nil, err } - return TransactionProviderFn(func(iteration int) (string, map[string]string) { - ret := generator.Next(iteration) - return ret.Script, ret.Variables + return ActionProviderFn(func(iteration int) (*generate.Action, error) { + return generator.Next(iteration) }), nil } } type Benchmark struct { EnvFactory EnvFactory - Scenarios map[string]TransactionProviderFactory + Scenarios map[string]ActionProviderFactory reports map[string]map[string]*report b *testing.B @@ -98,17 +94,18 @@ func (benchmark *Benchmark) Run(ctx context.Context) map[string][]Result { b.RunParallel(func(pb *testing.PB) { - transactionProvider, err := benchmark.Scenarios[scenario].Create() + actionProvider, err := benchmark.Scenarios[scenario].Create() require.NoError(b, err) for pb.Next() { iteration := int(cpt.Add(1)) - script, vars := transactionProvider.Get(iteration) + action, err := actionProvider.Get(iteration) + require.NoError(b, err) now := time.Now() - _, err := benchmark.createTransaction(ctx, env.Client(), l, script, vars) + _, err = action.Apply(ctx, env.Client().Ledger.V2, l.Name) require.NoError(b, err) report.registerTransactionLatency(time.Since(now)) @@ -149,58 +146,7 @@ func (benchmark *Benchmark) Run(ctx context.Context) map[string][]Result { return results } -func (benchmark *Benchmark) createTransaction( - ctx context.Context, - client *ledgerclient.Formance, - l ledger.Ledger, - script string, - vars map[string]string, -) (*ledger.Transaction, error) { - response, err := client.Ledger.V2.CreateTransaction(ctx, operations.V2CreateTransactionRequest{ - Ledger: l.Name, - V2PostTransaction: components.V2PostTransaction{ - Script: &components.V2PostTransactionScript{ - Plain: script, - Vars: vars, - }, - }, - }) - if err != nil { - return nil, fmt.Errorf("creating transaction: %w", err) - } - - return &ledger.Transaction{ - TransactionData: ledger.TransactionData{ - Postings: Map(response.V2CreateTransactionResponse.Data.Postings, func(from components.V2Posting) ledger.Posting { - return ledger.Posting{ - Source: from.Source, - Destination: from.Destination, - Amount: from.Amount, - Asset: from.Asset, - } - }), - Metadata: response.V2CreateTransactionResponse.Data.Metadata, - Timestamp: time.Time{ - Time: response.V2CreateTransactionResponse.Data.Timestamp, - }, - Reference: func() string { - if response.V2CreateTransactionResponse.Data.Reference == nil { - return "" - } - return *response.V2CreateTransactionResponse.Data.Reference - }(), - }, - ID: int(response.V2CreateTransactionResponse.Data.ID.Int64()), - RevertedAt: func() *time.Time { - if response.V2CreateTransactionResponse.Data.RevertedAt == nil { - return nil - } - return &time.Time{Time: *response.V2CreateTransactionResponse.Data.RevertedAt} - }(), - }, nil -} - -func New(b *testing.B, envFactory EnvFactory, scenarios map[string]TransactionProviderFactory) *Benchmark { +func New(b *testing.B, envFactory EnvFactory, scenarios map[string]ActionProviderFactory) *Benchmark { return &Benchmark{ b: b, EnvFactory: envFactory, diff --git a/test/performance/example_scripts/example1.js b/test/performance/example_scripts/example1.js index 43e9e65b9..81e7c0997 100644 --- a/test/performance/example_scripts/example1.js +++ b/test/performance/example_scripts/example1.js @@ -1,4 +1,4 @@ -const script = `vars { +const plain = `vars { account $order account $seller } @@ -17,10 +17,13 @@ send [USD/2 99] ( function next(iteration) { return { - script, - variables: { - order: `orders:${uuid()}`, - seller: `sellers:${iteration % 5}` + action: 'CREATE_TRANSACTION', + data: { + plain, + vars: { + order: `orders:${uuid()}`, + seller: `sellers:${iteration % 5}` + } } } } diff --git a/test/performance/scripts/any_bounded_to_any.js b/test/performance/scripts/any_bounded_to_any.js index 755938560..b96b24b9e 100644 --- a/test/performance/scripts/any_bounded_to_any.js +++ b/test/performance/scripts/any_bounded_to_any.js @@ -1,16 +1,19 @@ function next() { return { - script: `vars { - account $source - account $destination - } - send [USD/2 100] ( - source = $source allowing overdraft up to [USD/2 100] - destination = $destination - )`, - variables: { - destination: "dst:" + uuid(), - source: "src:" + uuid() + action: 'CREATE_TRANSACTION', + data: { + plain: `vars { + account $source + account $destination + } + send [USD/2 100] ( + source = $source allowing overdraft up to [USD/2 100] + destination = $destination + )`, + vars: { + destination: "dst:" + uuid(), + source: "src:" + uuid() + } } } } \ No newline at end of file diff --git a/test/performance/scripts/any_unbounded_to_any.js b/test/performance/scripts/any_unbounded_to_any.js index 1c401e1cb..ffbc02224 100644 --- a/test/performance/scripts/any_unbounded_to_any.js +++ b/test/performance/scripts/any_unbounded_to_any.js @@ -1,16 +1,19 @@ function next() { return { - script: `vars { - account $source - account $destination - } - send [USD/2 100] ( - source = $source allowing unbounded overdraft - destination = $destination - )`, - variables: { - destination: "dst:" + uuid(), - source: "src:" + uuid() + action: 'CREATE_TRANSACTION', + data: { + plain: `vars { + account $source + account $destination + } + send [USD/2 100] ( + source = $source allowing unbounded overdraft + destination = $destination + )`, + vars: { + destination: "dst:" + uuid(), + source: "src:" + uuid() + } } } } \ No newline at end of file diff --git a/test/performance/scripts/world_to_any.js b/test/performance/scripts/world_to_any.js index 3f68f8d87..cf61ae531 100644 --- a/test/performance/scripts/world_to_any.js +++ b/test/performance/scripts/world_to_any.js @@ -1,14 +1,17 @@ function next() { return { - script: `vars { - account $destination - } - send [USD/2 100] ( - source = @world - destination = $destination - )`, - variables: { - destination: "dst:" + uuid() + action: 'CREATE_TRANSACTION', + data: { + plain: `vars { + account $destination + } + send [USD/2 100] ( + source = @world + destination = $destination + )`, + vars: { + destination: "dst:" + uuid() + } } } } diff --git a/test/performance/scripts/world_to_bank.js b/test/performance/scripts/world_to_bank.js index ee248e864..66338267d 100644 --- a/test/performance/scripts/world_to_bank.js +++ b/test/performance/scripts/world_to_bank.js @@ -1,9 +1,12 @@ function next() { return { - script: `send [USD/2 100] ( - source = @world - destination = @bank - )`, - variables: {} + action: 'CREATE_TRANSACTION', + data: { + plain: `send [USD/2 100] ( + source = @world + destination = @bank + )`, + vars: {} + } } } \ No newline at end of file diff --git a/test/performance/write_test.go b/test/performance/write_test.go index aa6b03913..a52c03621 100644 --- a/test/performance/write_test.go +++ b/test/performance/write_test.go @@ -36,7 +36,7 @@ var ( envFactory EnvFactory - scripts = map[string]TransactionProviderFactory{} + scripts = map[string]ActionProviderFactory{} ) func init() { @@ -104,13 +104,13 @@ func BenchmarkWrite(b *testing.B) { script, err := scriptsDir.ReadFile(filepath.Join("scripts", entry.Name())) require.NoError(b, err) - scripts[strings.TrimSuffix(entry.Name(), ".js")] = NewJSTransactionProviderFactory(string(script)) + scripts[strings.TrimSuffix(entry.Name(), ".js")] = NewJSActionProviderFactory(string(script)) } } else { file, err := os.ReadFile(scriptFlag) require.NoError(b, err, "reading file "+scriptFlag) - scripts["provided"] = NewJSTransactionProviderFactory(string(file)) + scripts["provided"] = NewJSActionProviderFactory(string(file)) } if envFactory == nil { diff --git a/tools/generator/cmd/root.go b/tools/generator/cmd/root.go index da07b4fbf..938896bda 100644 --- a/tools/generator/cmd/root.go +++ b/tools/generator/cmd/root.go @@ -29,7 +29,7 @@ var ( } parallelFlag = "parallel" ledgerFlag = "ledger" - untilTransactionIDFlag = "until-transaction-id" + untilLogIDFlag = "until-log-id" clientIDFlag = "client-id" clientSecretFlag = "client-secret" authUrlFlag = "auth-url" @@ -55,9 +55,9 @@ func run(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get ledger: %w", err) } - untilTransactionID, err := cmd.Flags().GetInt64(untilTransactionIDFlag) + untilLogID, err := cmd.Flags().GetInt64(untilLogIDFlag) if err != nil { - return fmt.Errorf("failed to get untilTransactionID: %w", err) + return fmt.Errorf("failed to get untilLogID: %w", err) } insecureSkipVerify, err := cmd.Flags().GetBool(insecureSkipVerifyFlag) @@ -137,26 +137,19 @@ func run(cmd *cobra.Command, args []string) error { for { logging.FromContext(ctx).Infof("Run iteration %d/%d", vu, iteration) - next := generator.Next(vu) - tx, err := client.Ledger.V2.CreateTransaction( - ctx, - operations.V2CreateTransactionRequest{ - Ledger: ledger, - V2PostTransaction: components.V2PostTransaction{ - Script: &components.V2PostTransactionScript{ - Plain: next.Script, - Vars: next.Variables, - }, - }, - }, - ) + action, err := generator.Next(vu) + if err != nil { + return fmt.Errorf("iteration %d/%d failed: %w", vu, iteration, err) + } + + ret, err := action.Apply(ctx, client.Ledger.V2, ledger) if err != nil { if errors.Is(err, context.Canceled) { return nil } return fmt.Errorf("iteration %d/%d failed: %w", vu, iteration, err) } - if untilTransactionID != 0 && tx.V2CreateTransactionResponse.Data.ID.Int64() >= untilTransactionID { + if untilLogID != 0 && ret.GetLogID() >= untilLogID { return nil } iteration++ @@ -181,6 +174,6 @@ func init() { rootCmd.Flags().Bool(insecureSkipVerifyFlag, false, "Skip TLS verification") rootCmd.Flags().IntP(parallelFlag, "p", 1, "Number of parallel users") rootCmd.Flags().StringP(ledgerFlag, "l", "default", "Ledger to feed") - rootCmd.Flags().Int64P(untilTransactionIDFlag, "u", 0, "Stop after this transaction ID") + rootCmd.Flags().Int64P(untilLogIDFlag, "u", 0, "Stop after this transaction ID") rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } diff --git a/tools/generator/examples/example1.js b/tools/generator/examples/example1.js index 2517a3c16..cefe6c0b5 100644 --- a/tools/generator/examples/example1.js +++ b/tools/generator/examples/example1.js @@ -1,4 +1,4 @@ -const script = `vars { +const plain = `vars { account $order account $seller } @@ -17,10 +17,13 @@ send [USD/2 99] ( function next(iteration) { return { - script, - variables: { - order: `orders:${uuid()}`, - seller: `sellers:${iteration % 5}` + action: 'CREATE_TRANSACTION', + data: { + plain, + vars: { + order: `orders:${uuid()}`, + seller: `sellers:${iteration % 5}` + } } } } \ No newline at end of file diff --git a/tools/generator/examples_test.go b/tools/generator/examples_test.go new file mode 100644 index 000000000..2dd7f1b3b --- /dev/null +++ b/tools/generator/examples_test.go @@ -0,0 +1,30 @@ +//go:build it + +package main_test + +import ( + "embed" + "github.com/formancehq/ledger/pkg/generate" + "github.com/stretchr/testify/require" + "path/filepath" + "testing" +) + +//go:embed examples +var examples embed.FS + +func TestGenerator(t *testing.T) { + dirEntries, err := examples.ReadDir("examples") + require.NoError(t, err) + + for _, entry := range dirEntries { + example, err := examples.ReadFile(filepath.Join("examples", entry.Name())) + require.NoError(t, err) + + generator, err := generate.NewGenerator(string(example)) + require.NoError(t, err) + + _, err = generator.Next(1) + require.NoError(t, err) + } +} diff --git a/tools/generator/go.mod b/tools/generator/go.mod index e9c1d89bc..0e08ee16d 100644 --- a/tools/generator/go.mod +++ b/tools/generator/go.mod @@ -6,44 +6,179 @@ toolchain go1.23.2 replace github.com/formancehq/ledger => ../.. +replace github.com/formancehq/ledger/pkg/client => ../../pkg/client + require ( github.com/formancehq/go-libs/v2 v2.0.1-0.20241107141636-5509ff77294e github.com/formancehq/ledger v0.0.0-00010101000000-000000000000 - github.com/formancehq/ledger/pkg/client v0.0.0-20241104151610-5314238836a3 + github.com/formancehq/ledger/pkg/client v0.0.0-00010101000000-000000000000 github.com/spf13/cobra v1.8.1 + github.com/stretchr/testify v1.9.0 golang.org/x/oauth2 v0.23.0 golang.org/x/sync v0.8.0 ) require ( + dario.cat/mergo v1.0.1 // indirect + filippo.io/edwards25519 v1.1.0 // indirect + github.com/IBM/sarama v1.43.3 // indirect github.com/ThreeDotsLabs/watermill v1.3.7 // indirect + github.com/ThreeDotsLabs/watermill-http/v2 v2.3.1 // indirect + github.com/ThreeDotsLabs/watermill-kafka/v3 v3.0.5 // indirect + github.com/ThreeDotsLabs/watermill-nats/v2 v2.1.1 // indirect + github.com/ajg/form v1.5.1 // indirect + github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/aws/aws-msk-iam-sasl-signer-go v1.0.0 // indirect + github.com/aws/aws-sdk-go-v2 v1.32.3 // indirect + github.com/aws/aws-sdk-go-v2/config v1.28.1 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.42 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.18 // indirect + github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.22 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.22 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.22 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.3 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.24.3 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.3 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.32.3 // indirect + github.com/aws/smithy-go v1.22.0 // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/bluele/gcache v0.0.2 // indirect + github.com/buger/jsonparser v1.1.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dlclark/regexp2 v1.11.4 // indirect + github.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 // indirect github.com/dop251/goja v0.0.0-20241009100908-5f46f2705ca3 // indirect + github.com/eapache/go-resiliency v1.7.0 // indirect + github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect + github.com/eapache/queue v1.1.0 // indirect + github.com/ebitengine/purego v0.8.1 // indirect github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731 // indirect github.com/fatih/color v1.18.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/formancehq/numscript v0.0.9-0.20241009144012-1150c14a1417 // indirect + github.com/go-chi/chi v4.1.2+incompatible // indirect + github.com/go-chi/chi/v5 v5.1.0 // indirect + github.com/go-chi/cors v1.2.1 // indirect + github.com/go-chi/render v1.0.3 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/gorilla/schema v1.4.1 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/invopop/jsonschema v0.12.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.1 // indirect + github.com/jackc/pgxlisten v0.0.0-20241005155529-9d952acd6a6c // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jcmturner/aescts/v2 v2.0.0 // indirect + github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect + github.com/jcmturner/gofork v1.7.6 // indirect + github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect + github.com/jcmturner/rpc/v2 v2.0.3 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/klauspost/compress v1.17.11 // indirect github.com/lithammer/shortuuid/v3 v3.0.7 // indirect + github.com/logrusorgru/aurora v2.0.3+incompatible // indirect + github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/muhlemmer/gu v0.3.1 // indirect + github.com/muhlemmer/httpforwarded v0.1.0 // indirect + github.com/nats-io/nats.go v1.37.0 // indirect + github.com/nats-io/nkeys v0.4.7 // indirect + github.com/nats-io/nuid v1.0.1 // indirect github.com/oklog/ulid v1.3.1 // indirect + github.com/onsi/ginkgo/v2 v2.20.2 // indirect + github.com/onsi/gomega v1.34.2 // indirect + github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect + github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect + github.com/riandyrn/otelchi v0.10.1 // indirect + github.com/rs/cors v1.11.1 // indirect + github.com/shirou/gopsutil/v4 v4.24.9 // indirect + github.com/shomali11/util v0.0.0-20220717175126-f0771b70947f // indirect + github.com/shomali11/xsql v0.0.0-20190608141458-bf76292144df // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/tklauser/go-sysconf v0.3.14 // indirect + github.com/tklauser/numcpus v0.9.0 // indirect + github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect + github.com/uptrace/bun v1.2.5 // indirect + github.com/uptrace/bun/dialect/pgdialect v1.2.5 // indirect + github.com/uptrace/bun/extra/bunotel v1.2.5 // indirect github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 // indirect + github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + github.com/xo/dburl v0.23.2 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + github.com/zitadel/oidc/v2 v2.12.2 // indirect + go.opentelemetry.io/contrib/instrumentation/host v0.56.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect + go.opentelemetry.io/contrib/instrumentation/runtime v0.56.0 // indirect + go.opentelemetry.io/contrib/propagators/b3 v1.31.0 // indirect go.opentelemetry.io/otel v1.31.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.31.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.31.0 // indirect go.opentelemetry.io/otel/log v0.7.0 // indirect go.opentelemetry.io/otel/metric v1.31.0 // indirect + go.opentelemetry.io/otel/sdk v1.31.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.31.0 // indirect go.opentelemetry.io/otel/trace v1.31.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect + go.uber.org/dig v1.18.0 // indirect + go.uber.org/fx v1.23.0 // indirect + go.uber.org/mock v0.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect + golang.org/x/net v0.30.0 // indirect golang.org/x/sys v0.26.0 // indirect golang.org/x/text v0.19.0 // indirect + golang.org/x/tools v0.26.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect + google.golang.org/grpc v1.67.1 // indirect + google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/tools/generator/go.sum b/tools/generator/go.sum index c299c4c1e..6b801ebf8 100644 --- a/tools/generator/go.sum +++ b/tools/generator/go.sum @@ -1,9 +1,73 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/IBM/sarama v1.43.3 h1:Yj6L2IaNvb2mRBop39N7mmJAHBVY3dTPncr3qGVkxPA= +github.com/IBM/sarama v1.43.3/go.mod h1:FVIRaLrhK3Cla/9FfRF5X9Zua2KpS3SYIXxhac1H+FQ= github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/ThreeDotsLabs/watermill v1.3.7 h1:NV0PSTmuACVEOV4dMxRnmGXrmbz8U83LENOvpHekN7o= github.com/ThreeDotsLabs/watermill v1.3.7/go.mod h1:lBnrLbxOjeMRgcJbv+UiZr8Ylz8RkJ4m6i/VN/Nk+to= +github.com/ThreeDotsLabs/watermill-http/v2 v2.3.1 h1:M0iYM5HsGcoxtiQqprRlYZNZnGk3w5LsE9RbC2R8myQ= +github.com/ThreeDotsLabs/watermill-http/v2 v2.3.1/go.mod h1:RwGHEzGsEEXC/rQNLWQqR83+WPlABgOgnv2kTB56Y4Y= +github.com/ThreeDotsLabs/watermill-kafka/v3 v3.0.5 h1:ud+4txnRgtr3kZXfXZ5+C7kVQEvsLc5HSNUEa0g+X1Q= +github.com/ThreeDotsLabs/watermill-kafka/v3 v3.0.5/go.mod h1:t4o+4A6GB+XC8WL3DandhzPwd265zQuyWMQC/I+WIOU= +github.com/ThreeDotsLabs/watermill-nats/v2 v2.1.1 h1:afAkAFzeooBRQvxElR+6xoigXKCukcZXnE9ACxhwlPI= +github.com/ThreeDotsLabs/watermill-nats/v2 v2.1.1/go.mod h1:stjbT+s4u/s5ime5jdIyvPyjBGwGeJewIN7jxH8gp4k= +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/alitto/pond v1.9.2 h1:9Qb75z/scEZVCoSU+osVmQ0I0JOeLfdTDafrbcJ8CLs= +github.com/alitto/pond v1.9.2/go.mod h1:xQn3P/sHTYcU/1BR3i86IGIrilcrGC2LiS+E2+CJWsI= +github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 h1:yL7+Jz0jTC6yykIK/Wh74gnTJnrGr5AyrNMXuA0gves= +github.com/antlr/antlr4/runtime/Go/antlr v1.4.10/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/aws/aws-msk-iam-sasl-signer-go v1.0.0 h1:UyjtGmO0Uwl/K+zpzPwLoXzMhcN9xmnR2nrqJoBrg3c= +github.com/aws/aws-msk-iam-sasl-signer-go v1.0.0/go.mod h1:TJAXuFs2HcMib3sN5L0gUC+Q01Qvy3DemvA55WuC+iA= +github.com/aws/aws-sdk-go-v2 v1.32.3 h1:T0dRlFBKcdaUPGNtkBSwHZxrtis8CQU17UpNBZYd0wk= +github.com/aws/aws-sdk-go-v2 v1.32.3/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo= +github.com/aws/aws-sdk-go-v2/config v1.28.1 h1:oxIvOUXy8x0U3fR//0eq+RdCKimWI900+SV+10xsCBw= +github.com/aws/aws-sdk-go-v2/config v1.28.1/go.mod h1:bRQcttQJiARbd5JZxw6wG0yIK3eLeSCPdg6uqmmlIiI= +github.com/aws/aws-sdk-go-v2/credentials v1.17.42 h1:sBP0RPjBU4neGpIYyx8mkU2QqLPl5u9cmdTWVzIpHkM= +github.com/aws/aws-sdk-go-v2/credentials v1.17.42/go.mod h1:FwZBfU530dJ26rv9saAbxa9Ej3eF/AK0OAY86k13n4M= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.18 h1:68jFVtt3NulEzojFesM/WVarlFpCaXLKaBxDpzkQ9OQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.18/go.mod h1:Fjnn5jQVIo6VyedMc0/EhPpfNlPl7dHV916O6B+49aE= +github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.22 h1:n89ziXnsp3dyOlodim8OHv0edSu47H7i75UYxDz1YVQ= +github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.22/go.mod h1:7uC80VxwPjAykLSIzkyTgZ+LjFDil+OVndzd8wGMOYY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.22 h1:Jw50LwEkVjuVzE1NzkhNKkBf9cRN7MtE1F/b2cOKTUM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.22/go.mod h1:Y/SmAyPcOTmpeVaWSzSKiILfXTVJwrGmYZhcRbhWuEY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.22 h1:981MHwBaRZM7+9QSR6XamDzF/o7ouUGxFzr+nVSIhrs= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.22/go.mod h1:1RA1+aBEfn+CAB/Mh0MB6LsdCYCnjZm7tKXtnk499ZQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.3 h1:qcxX0JYlgWH3hpPUnd6U0ikcl6LLA9sLkXE2w1fpMvY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.3/go.mod h1:cLSNEmI45soc+Ef8K/L+8sEA3A3pYFEYf5B5UI+6bH4= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.3 h1:UTpsIf0loCIWEbrqdLb+0RxnTXfWh2vhw4nQmFi4nPc= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.3/go.mod h1:FZ9j3PFHHAR+w0BSEjK955w5YD2UwB/l/H0yAK3MJvI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.3 h1:2YCmIXv3tmiItw0LlYf6v7gEHebLY45kBEnPezbUKyU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.3/go.mod h1:u19stRyNPxGhj6dRm+Cdgu6N75qnbW7+QN0q0dsAk58= +github.com/aws/aws-sdk-go-v2/service/sts v1.32.3 h1:wVnQ6tigGsRqSWDEEyH6lSAJ9OyFUsSnbaUWChuSGzs= +github.com/aws/aws-sdk-go-v2/service/sts v1.32.3/go.mod h1:VZa9yTFyj4o10YGsmDO4gbQJUvvhY72fhumT8W4LqsE= +github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= +github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw= +github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= +github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -11,38 +75,157 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 h1:R2zQhFwSCyyd7L43igYjDrH0wkC/i+QBPELuY0HOu84= +github.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0/go.mod h1:2MqLKYJfjs3UriXXF9Fd0Qmh/lhxi/6tHXkqtXxyIHc= +github.com/docker/cli v27.3.1+incompatible h1:qEGdFBF3Xu6SCvCYhc7CzaQTlBmqDuzxPDpigSyeKQQ= +github.com/docker/cli v27.3.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= +github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dop251/goja v0.0.0-20241009100908-5f46f2705ca3 h1:MXsAuToxwsTn5BEEYm2DheqIiC4jWGmkEJ1uy+KFhvQ= github.com/dop251/goja v0.0.0-20241009100908-5f46f2705ca3/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4= +github.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA= +github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= +github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws= +github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= +github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE= +github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731 h1:R/ZjJpjQKsZ6L/+Gf9WHbt31GG8NMVcpRqUE+1mMIyo= github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731/go.mod h1:M9R1FoZ3y//hwwnJtO51ypFGwm8ZfpxPT/ZLtO1mcgQ= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241107140023-218febd1c023 h1:JWRNtxQs0IsxTK5wdrvIc9wi4Kscm9TNBOn2wuW9szU= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241107140023-218febd1c023/go.mod h1:DTqSp28pYPZa4O1WrOg3kobhgTHdk9geGtxnws9EViM= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241107141636-5509ff77294e h1:O7HXIkgcHTY81Ehoex+a2JpmKWP+Co6eYZdoHX0r96w= github.com/formancehq/go-libs/v2 v2.0.1-0.20241107141636-5509ff77294e/go.mod h1:DTqSp28pYPZa4O1WrOg3kobhgTHdk9geGtxnws9EViM= -github.com/formancehq/ledger/pkg/client v0.0.0-20241104151610-5314238836a3 h1:7Kyd9WIxBBRPk2DlBwhrPP5HM4l30y3zpk77Iss/jt0= -github.com/formancehq/ledger/pkg/client v0.0.0-20241104151610-5314238836a3/go.mod h1:BT02yUK6iBRyiodzWe7gyDtgKswtA/8tv824XqpuOcc= +github.com/formancehq/numscript v0.0.9-0.20241009144012-1150c14a1417 h1:LOd5hxnXDIBcehFrpW1OnXk+VSs0yJXeu1iAOO+Hji4= +github.com/formancehq/numscript v0.0.9-0.20241009144012-1150c14a1417/go.mod h1:btuSv05cYwi9BvLRxVs5zrunU+O1vTgigG1T6UsawcY= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/gkampitakis/ciinfo v0.3.0 h1:gWZlOC2+RYYttL0hBqcoQhM7h1qNkVqvRCV1fOvpAv8= +github.com/gkampitakis/ciinfo v0.3.0/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= +github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= +github.com/gkampitakis/go-snaps v0.5.4 h1:GX+dkKmVsRenz7SoTbdIEL4KQARZctkMiZ8ZKprRwT8= +github.com/gkampitakis/go-snaps v0.5.4/go.mod h1:ZABkO14uCuVxBHAXAfKG+bqNz+aa1bGPAg8jkI0Nk8Y= +github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= +github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= +github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= +github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= +github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 h1:c5FlPPgxOn7kJz3VoPLkQYQXGBS3EklQ4Zfi57uOuqQ= github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= +github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI= +github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= +github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= +github.com/jackc/pgxlisten v0.0.0-20241005155529-9d952acd6a6c h1:/u9tWJZ5d+RnlpVuvf352pGb+CzTrJP+r+ETy4JEHyo= +github.com/jackc/pgxlisten v0.0.0-20241005155529-9d952acd6a6c/go.mod h1:EqjCOzkITPCEI0My7BdE2xm3r0fZ7OZycVDP+ki1ASA= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc= +github.com/jeremija/gosubmit v0.2.7/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8= github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts= +github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= +github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= +github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= +github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -52,14 +235,70 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q= +github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= +github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= +github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY= +github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0= +github.com/nats-io/jwt/v2 v2.7.0 h1:J+ZnaaMGQi3xSB8iOhVM5ipiWCDrQvgEoitTwWFyOYw= +github.com/nats-io/jwt/v2 v2.7.0/go.mod h1:ZdWS1nZa6WMZfFwwgpEaqBV8EPGVgOTDHN/wTbz0Y5A= +github.com/nats-io/nats-server/v2 v2.10.21 h1:gfG6T06wBdI25XyY2IsauarOc2srWoFxxfsOKjrzoRA= +github.com/nats-io/nats-server/v2 v2.10.21/go.mod h1:I1YxSAEWbXCfy0bthwvNb5X43WwIWMz7gx5ZVPDr5Rc= +github.com/nats-io/nats.go v1.37.0 h1:07rauXbVnnJvv1gfIyghFEo6lUcYRY0WXc3x7x0vUxE= +github.com/nats-io/nats.go v1.37.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= +github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= +github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= +github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= +github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= +github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/runc v1.1.14 h1:rgSuzbmgz5DUJjeSnw337TxDbRuqjs6iqQck/2weR6w= +github.com/opencontainers/runc v1.1.14/go.mod h1:E4C2z+7BxR7GHXp0hAY53mek+x49X1LjPNeMTfRGvOA= +github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA= +github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI= +github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= +github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= +github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4= +github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/riandyrn/otelchi v0.10.1 h1:x86f8M0pGvjW3tJUxpva4cpdNtMydLPnarIXHssYUy4= +github.com/riandyrn/otelchi v0.10.1/go.mod h1:SWarhA5rdeiCNq+Ygc4p59ZGM5AtYCiyPU/3Q5rzT0M= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shirou/gopsutil/v4 v4.24.9 h1:KIV+/HaHD5ka5f570RZq+2SaeFsb/pq+fp2DGNWYoOI= +github.com/shirou/gopsutil/v4 v4.24.9/go.mod h1:3fkaHNeYsUFCGZ8+9vZVWtbyM1k2eRnlL+bWO8Bxa/Q= +github.com/shomali11/parallelizer v0.0.0-20220717173222-a6776fbf40a9/go.mod h1:QsLM53l8gzX0sQbOjVir85bzOUucuJEF8JgE39wD7w0= +github.com/shomali11/util v0.0.0-20180607005212-e0f70fd665ff/go.mod h1:WWE2GJM9B5UpdOiwH2val10w/pvJ2cUUQOOA/4LgOng= +github.com/shomali11/util v0.0.0-20220717175126-f0771b70947f h1:OM0LVaVycWC+/j5Qra7USyCg2sc+shg3KwygAA+pYvA= +github.com/shomali11/util v0.0.0-20220717175126-f0771b70947f/go.mod h1:9POpw/crb6YrseaYBOwraL0lAYy0aOW79eU3bvMxgbM= +github.com/shomali11/xsql v0.0.0-20190608141458-bf76292144df h1:SVCDTuzM3KEk8WBwSSw7RTPLw9ajzBaXDg39Bo6xIeU= +github.com/shomali11/xsql v0.0.0-20190608141458-bf76292144df/go.mod h1:K8jR5lDI2MGs9Ky+X2jIF4MwIslI0L8o8ijIlEq7/Vw= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= @@ -67,47 +306,193 @@ github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3k github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= +github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= +github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= +github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo= +github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= +github.com/uptrace/bun v1.2.5 h1:gSprL5xiBCp+tzcZHgENzJpXnmQwRM/A6s4HnBF85mc= +github.com/uptrace/bun v1.2.5/go.mod h1:vkQMS4NNs4VNZv92y53uBSHXRqYyJp4bGhMHgaNCQpY= +github.com/uptrace/bun/dialect/pgdialect v1.2.5 h1:dWLUxpjTdglzfBks2x+U2WIi+nRVjuh7Z3DLYVFswJk= +github.com/uptrace/bun/dialect/pgdialect v1.2.5/go.mod h1:stwnlE8/6x8cuQ2aXcZqwDK/d+6jxgO3iQewflJT6C4= +github.com/uptrace/bun/extra/bundebug v1.2.3 h1:2QBykz9/u4SkN9dnraImDcbrMk2fUhuq2gL6hkh9qSc= +github.com/uptrace/bun/extra/bundebug v1.2.3/go.mod h1:bihsYJxXxWZXwc1R3qALTHvp+npE0ElgaCvcjzyPPdw= +github.com/uptrace/bun/extra/bunotel v1.2.5 h1:kkuuTbrG9d5leYZuSBKhq2gtq346lIrxf98Mig2y128= +github.com/uptrace/bun/extra/bunotel v1.2.5/go.mod h1:rCHLszRZwppWE9cGDodO2FCI1qCrLwDjONp38KD3bA8= github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 h1:H8wwQwTe5sL6x30z71lUgNiwBdeCHQjrphCfLwqIHGo= github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2/go.mod h1:/kR4beFhlz2g+V5ik8jW+3PMiMQAPt29y6K64NNY53c= +github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 h1:ZjUj9BLYf9PEqBn8W/OapxhPjVRdC6CsXTdULHsyk5c= +github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2/go.mod h1:O8bHQfyinKwTXKkiKNGmLQS7vRsqRxIQTFZpYpHK3IQ= github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 h1:3/aHKUq7qaFMWxyQV0W2ryNgg8x8rVeKVA20KJUkfS0= github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2/go.mod h1:Zit4b8AQXaXvA68+nzmbyDzqiyFRISyw1JiD5JqUBjw= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xo/dburl v0.23.2 h1:Fl88cvayrgE56JA/sqhNMLljCW/b7RmG1mMkKMZUFgA= +github.com/xo/dburl v0.23.2/go.mod h1:uazlaAQxj4gkshhfuuYyvwCBouOmNnG2aDxTCFZpmL4= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zitadel/oidc/v2 v2.12.2 h1:3kpckg4rurgw7w7aLJrq7yvRxb2pkNOtD08RH42vPEs= +github.com/zitadel/oidc/v2 v2.12.2/go.mod h1:vhP26g1g4YVntcTi0amMYW3tJuid70nxqxf+kb6XKgg= +go.opentelemetry.io/contrib/instrumentation/host v0.56.0 h1:bLJ0U2SVly7aCVAv4pSJ62I0yy3GHPMbK+74AXSwC40= +go.opentelemetry.io/contrib/instrumentation/host v0.56.0/go.mod h1:7XvO8DvjdcoYDOQs/1n3AuadI/30eE2R+H/pQQuZVN0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= +go.opentelemetry.io/contrib/instrumentation/runtime v0.56.0 h1:s7wHG+t8bEoH7ibWk1nk682h7EoWLJ5/8j+TSO3bX/o= +go.opentelemetry.io/contrib/instrumentation/runtime v0.56.0/go.mod h1:Q8Hsv3d9DwryfIl+ebj4mY81IYVRSPy4QfxroVZwqLo= +go.opentelemetry.io/contrib/propagators/b3 v1.31.0 h1:PQPXYscmwbCp76QDvO4hMngF2j8Bx/OTV86laEl8uqo= +go.opentelemetry.io/contrib/propagators/b3 v1.31.0/go.mod h1:jbqfV8wDdqSDrAYxVpXQnpM0XFMq2FtDesblJ7blOwQ= go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0 h1:FZ6ei8GFW7kyPYdxJaV2rgI6M+4tvZzhYsQ2wgyVC08= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0/go.mod h1:MdEu/mC6j3D+tTEfvI15b5Ci2Fn7NneJ71YMoiS3tpI= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0 h1:ZsXq73BERAiNuuFXYqP4MR5hBrjXfMGSO+Cx7qoOZiM= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0/go.mod h1:hg1zaDMpyZJuUzjFxFsRYBoccE86tM9Uf4IqNMUxvrY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 h1:K0XaT3DwHAcV4nKLzcQvwAgSyisUghWoY20I7huthMk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0/go.mod h1:B5Ki776z/MBnVha1Nzwp5arlzBbE3+1jk+pGmaP5HME= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 h1:FFeLy03iVTXP6ffeN2iXrxfGsZGCjVx0/4KlizjyBwU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0/go.mod h1:TMu73/k1CP8nBUpDLc71Wj/Kf7ZS9FK5b53VapRsP9o= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 h1:lUsI2TYsQw2r1IASwoROaCnjdj2cvC2+Jbxvk6nHnWU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0/go.mod h1:2HpZxxQurfGxJlJDblybejHB6RX6pmExPNe517hREw4= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.31.0 h1:HZgBIps9wH0RDrwjrmNa3DVbNRW60HEhdzqZFyAp3fI= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.31.0/go.mod h1:RDRhvt6TDG0eIXmonAx5bd9IcwpqCkziwkOClzWKwAQ= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.31.0 h1:UGZ1QwZWY67Z6BmckTU+9Rxn04m2bD3gD6Mk0OIOCPk= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.31.0/go.mod h1:fcwWuDuaObkkChiDlhEpSq9+X1C0omv+s5mBtToAQ64= go.opentelemetry.io/otel/log v0.7.0 h1:d1abJc0b1QQZADKvfe9JqqrfmPYQCz2tUSO+0XZmuV4= go.opentelemetry.io/otel/log v0.7.0/go.mod h1:2jf2z7uVfnzDNknKTO9G+ahcOAyWcp1fJmk/wJjULRo= go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= +go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= +go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw= +go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg= +go.uber.org/fx v1.23.0/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38 h1:2oV8dfuIkM1Ti7DwXc0BJfnwr9csz4TDXI9EmiI+Rbw= +google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38/go.mod h1:vuAjtvlwkDKF6L1GQ0SokiRLCGFfeBUXWr/aFFkHACc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 h1:zciRKQ4kBpFgpfC5QQCVtnnNAcLIqweL7plyZRQHVpI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs= +gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 52ad9941200dd04a7a70878b3c2fa3c887c2aab7 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 14 Nov 2024 00:34:14 +0100 Subject: [PATCH 13/71] feat: add capability to configure the ledger on the generator --- deployments/helm/templates/deployment.yaml | 2 + deployments/helm/values.yaml | 4 +- deployments/pulumi/main.go | 2 + internal/README.md | 10 ++ internal/ledger.go | 11 +- test/rolling-upgrades/Earthfile | 7 +- test/rolling-upgrades/go.mod | 33 +++- test/rolling-upgrades/go.sum | 171 ++++++++++++++++++++- test/rolling-upgrades/main_test.go | 39 +++-- tools/generator/cmd/root.go | 83 ++++++++-- 10 files changed, 316 insertions(+), 46 deletions(-) diff --git a/deployments/helm/templates/deployment.yaml b/deployments/helm/templates/deployment.yaml index 136cca619..727c34a5a 100644 --- a/deployments/helm/templates/deployment.yaml +++ b/deployments/helm/templates/deployment.yaml @@ -43,6 +43,8 @@ spec: value: ":{{ .Values.service.port }}" - name: DEBUG value: "{{ .Values.debug }}" + - name: EXPERIMENTAL_FEATURES + value: "{{ .Values.experimentalFeatures }}" {{- if not (eq .Values.gracePeriod "") }} # https://freecontent.manning.com/handling-client-requests-properly-with-kubernetes/ - name: GRACE_PERIOD diff --git a/deployments/helm/values.yaml b/deployments/helm/values.yaml index f59bab341..71cd80f1c 100644 --- a/deployments/helm/values.yaml +++ b/deployments/helm/values.yaml @@ -102,4 +102,6 @@ postgres: debug: false -gracePeriod: "5s" \ No newline at end of file +gracePeriod: "5s" + +experimentalFeatures: false \ No newline at end of file diff --git a/deployments/pulumi/main.go b/deployments/pulumi/main.go index d615eb2bf..1e062d20d 100644 --- a/deployments/pulumi/main.go +++ b/deployments/pulumi/main.go @@ -40,6 +40,7 @@ func deployLedger(ctx *pulumi.Context) error { imagePullPolicy, _ := conf.Try("image.pullPolicy") replicaCount, _ := conf.TryInt("replicaCount") + experimentalFeatures, _ := conf.TryBool("experimentalFeatures") rel, err := helm.NewRelease(ctx, "ledger", &helm.ReleaseArgs{ Chart: pulumi.String("../helm"), @@ -57,6 +58,7 @@ func deployLedger(ctx *pulumi.Context) error { }, "debug": pulumi.Bool(debug), "replicaCount": pulumi.Int(replicaCount), + "experimentalFeatures": pulumi.Bool(experimentalFeatures), }), }) if err != nil { diff --git a/internal/README.md b/internal/README.md index 626af7ae1..9bd9a8d20 100644 --- a/internal/README.md +++ b/internal/README.md @@ -33,6 +33,7 @@ import "github.com/formancehq/ledger/internal" - [func \(e ErrInvalidLedgerName\) Error\(\) string](<#ErrInvalidLedgerName.Error>) - [func \(e ErrInvalidLedgerName\) Is\(err error\) bool](<#ErrInvalidLedgerName.Is>) - [type FeatureSet](<#FeatureSet>) + - [func \(f FeatureSet\) SortedKeys\(\) \[\]string](<#FeatureSet.SortedKeys>) - [func \(f FeatureSet\) String\(\) string](<#FeatureSet.String>) - [func \(f FeatureSet\) With\(feature, value string\) FeatureSet](<#FeatureSet.With>) - [type Ledger](<#Ledger>) @@ -440,6 +441,15 @@ func (e ErrInvalidLedgerName) Is(err error) bool type FeatureSet map[string]string ``` + +### func \(FeatureSet\) SortedKeys + +```go +func (f FeatureSet) SortedKeys() []string +``` + + + ### func \(FeatureSet\) String diff --git a/internal/ledger.go b/internal/ledger.go index e80b4cc72..8f6adacd7 100644 --- a/internal/ledger.go +++ b/internal/ledger.go @@ -156,15 +156,20 @@ func (f FeatureSet) With(feature, value string) FeatureSet { return ret } +func (f FeatureSet) SortedKeys() []string { + ret := Keys(f) + slices.Sort(ret) + + return ret +} + func (f FeatureSet) String() string { if len(f) == 0 { return "" } - keys := Keys(f) - slices.Sort(keys) ret := "" - for _, key := range keys { + for _, key := range f.SortedKeys() { ret = ret + "," + shortenFeature(key) + "=" + f[key] } diff --git a/test/rolling-upgrades/Earthfile b/test/rolling-upgrades/Earthfile index add728c26..5df23e983 100644 --- a/test/rolling-upgrades/Earthfile +++ b/test/rolling-upgrades/Earthfile @@ -27,7 +27,8 @@ image-current: sources: FROM core+builder-image - WORKDIR /src + COPY ../..+sources/src /src + WORKDIR /src/test/rolling-upgrades COPY go.* *.go . SAVE ARTIFACT /src @@ -75,9 +76,11 @@ run: CACHE --id go-mod-cache /go/pkg/mod CACHE --id go-cache /root/.cache/go-build - WORKDIR /src/test/rolling-upgrades + COPY +sources/src /src COPY ../../deployments/pulumi+sources/src /src/deployments/pulumi COPY ../../deployments/helm+sources/src /src/deployments/helm + + WORKDIR /src/test/rolling-upgrades COPY go.* *.go . ARG NO_CLEANUP=false diff --git a/test/rolling-upgrades/go.mod b/test/rolling-upgrades/go.mod index c9772f8e9..588ed3fda 100644 --- a/test/rolling-upgrades/go.mod +++ b/test/rolling-upgrades/go.mod @@ -1,13 +1,16 @@ module github.com/formancehq/ledger/test/rolling-upgrades -go 1.22.0 +go 1.22.1 toolchain go1.23.2 replace github.com/formancehq/ledger/pkg/client => ../../pkg/client +replace github.com/formancehq/ledger => ../.. + require ( - github.com/formancehq/go-libs/v2 v2.0.0 + github.com/formancehq/go-libs/v2 v2.0.1-0.20241107141636-5509ff77294e + github.com/formancehq/ledger v0.0.0-00010101000000-000000000000 github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.12.0 github.com/pulumi/pulumi/sdk/v3 v3.117.0 github.com/stretchr/testify v1.9.0 @@ -23,7 +26,9 @@ require ( github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/blang/semver v3.5.1+incompatible // indirect + github.com/buger/jsonparser v1.1.1 // indirect github.com/charmbracelet/bubbles v0.16.1 // indirect github.com/charmbracelet/bubbletea v0.24.2 // indirect github.com/charmbracelet/lipgloss v0.7.1 // indirect @@ -34,23 +39,31 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/djherbis/times v1.5.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-git/go-git/v5 v5.12.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/glog v1.2.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/hcl/v2 v2.17.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/invopop/jsonschema v0.12.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/lithammer/shortuuid/v3 v3.0.7 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect @@ -71,6 +84,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 // indirect github.com/pulumi/esc v0.6.2 // indirect + github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect @@ -82,22 +96,33 @@ require ( github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/texttheater/golang-levenshtein v1.0.1 // indirect + github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect github.com/tweekmonster/luser v0.0.0-20161003172636-3fa38070dbd7 // indirect github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect github.com/uber/jaeger-lib v2.4.1+incompatible // indirect + github.com/uptrace/bun v1.2.5 // indirect + github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 // indirect + github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/zclconf/go-cty v1.13.2 // indirect + go.opentelemetry.io/otel v1.31.0 // indirect + go.opentelemetry.io/otel/log v0.7.0 // indirect + go.opentelemetry.io/otel/metric v1.31.0 // indirect + go.opentelemetry.io/otel/trace v1.31.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.28.0 // indirect - golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect + golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect golang.org/x/net v0.30.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.26.0 // indirect golang.org/x/term v0.25.0 // indirect golang.org/x/text v0.19.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect google.golang.org/grpc v1.67.1 // indirect google.golang.org/protobuf v1.35.1 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect diff --git a/test/rolling-upgrades/go.sum b/test/rolling-upgrades/go.sum index f3e3da3ca..6d146abd6 100644 --- a/test/rolling-upgrades/go.sum +++ b/test/rolling-upgrades/go.sum @@ -1,10 +1,16 @@ dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/ThreeDotsLabs/watermill v1.3.7 h1:NV0PSTmuACVEOV4dMxRnmGXrmbz8U83LENOvpHekN7o= @@ -21,11 +27,45 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aws/aws-sdk-go-v2 v1.32.3 h1:T0dRlFBKcdaUPGNtkBSwHZxrtis8CQU17UpNBZYd0wk= +github.com/aws/aws-sdk-go-v2 v1.32.3/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo= +github.com/aws/aws-sdk-go-v2/config v1.28.1 h1:oxIvOUXy8x0U3fR//0eq+RdCKimWI900+SV+10xsCBw= +github.com/aws/aws-sdk-go-v2/config v1.28.1/go.mod h1:bRQcttQJiARbd5JZxw6wG0yIK3eLeSCPdg6uqmmlIiI= +github.com/aws/aws-sdk-go-v2/credentials v1.17.42 h1:sBP0RPjBU4neGpIYyx8mkU2QqLPl5u9cmdTWVzIpHkM= +github.com/aws/aws-sdk-go-v2/credentials v1.17.42/go.mod h1:FwZBfU530dJ26rv9saAbxa9Ej3eF/AK0OAY86k13n4M= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.18 h1:68jFVtt3NulEzojFesM/WVarlFpCaXLKaBxDpzkQ9OQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.18/go.mod h1:Fjnn5jQVIo6VyedMc0/EhPpfNlPl7dHV916O6B+49aE= +github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.22 h1:n89ziXnsp3dyOlodim8OHv0edSu47H7i75UYxDz1YVQ= +github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.22/go.mod h1:7uC80VxwPjAykLSIzkyTgZ+LjFDil+OVndzd8wGMOYY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.22 h1:Jw50LwEkVjuVzE1NzkhNKkBf9cRN7MtE1F/b2cOKTUM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.22/go.mod h1:Y/SmAyPcOTmpeVaWSzSKiILfXTVJwrGmYZhcRbhWuEY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.22 h1:981MHwBaRZM7+9QSR6XamDzF/o7ouUGxFzr+nVSIhrs= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.22/go.mod h1:1RA1+aBEfn+CAB/Mh0MB6LsdCYCnjZm7tKXtnk499ZQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.3 h1:qcxX0JYlgWH3hpPUnd6U0ikcl6LLA9sLkXE2w1fpMvY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.3/go.mod h1:cLSNEmI45soc+Ef8K/L+8sEA3A3pYFEYf5B5UI+6bH4= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.3 h1:UTpsIf0loCIWEbrqdLb+0RxnTXfWh2vhw4nQmFi4nPc= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.3/go.mod h1:FZ9j3PFHHAR+w0BSEjK955w5YD2UwB/l/H0yAK3MJvI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.3 h1:2YCmIXv3tmiItw0LlYf6v7gEHebLY45kBEnPezbUKyU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.3/go.mod h1:u19stRyNPxGhj6dRm+Cdgu6N75qnbW7+QN0q0dsAk58= +github.com/aws/aws-sdk-go-v2/service/sts v1.32.3 h1:wVnQ6tigGsRqSWDEEyH6lSAJ9OyFUsSnbaUWChuSGzs= +github.com/aws/aws-sdk-go-v2/service/sts v1.32.3/go.mod h1:VZa9yTFyj4o10YGsmDO4gbQJUvvhY72fhumT8W4LqsE= +github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= +github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY= @@ -39,6 +79,8 @@ github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vc github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= +github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= @@ -48,15 +90,24 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/djherbis/times v1.5.0 h1:79myA211VwPhFTqUk8xehWrsEO+zcIZj0zT8mXPVARU= github.com/djherbis/times v1.5.0/go.mod h1:5q7FDLvbNg1L/KaBmPcWlVR9NmoKo3+ucqUA3ijQhA0= +github.com/docker/cli v27.3.1+incompatible h1:qEGdFBF3Xu6SCvCYhc7CzaQTlBmqDuzxPDpigSyeKQQ= +github.com/docker/cli v27.3.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= +github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= -github.com/formancehq/go-libs/v2 v2.0.0 h1:lRa90iNlOgV/H44Q8+xDE5HWaaztg++NZMhPdBs6Es4= -github.com/formancehq/go-libs/v2 v2.0.0/go.mod h1:LgxayMN6wgAQbkB3ioBDTHOVMKp1rC6Q55M1CvG44xY= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241107141636-5509ff77294e h1:O7HXIkgcHTY81Ehoex+a2JpmKWP+Co6eYZdoHX0r96w= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241107141636-5509ff77294e/go.mod h1:DTqSp28pYPZa4O1WrOg3kobhgTHdk9geGtxnws9EViM= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= @@ -69,6 +120,17 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -78,6 +140,10 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 h1:c5FlPPgxOn7kJz3VoPLkQYQXGBS3EklQ4Zfi57uOuqQ= +github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -86,14 +152,29 @@ github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/hcl/v2 v2.17.0 h1:z1XvSUyXd1HP10U4lrLg5e0JMVz6CPaJvAgxM0KNZVY= github.com/hashicorp/hcl/v2 v2.17.0/go.mod h1:gJyW2PTShkJqQBKpAmPO3yxMxIuoXkOF2TpqXzrQyx4= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI= +github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= +github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= @@ -110,11 +191,18 @@ github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJV github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= @@ -127,6 +215,10 @@ github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= @@ -139,13 +231,23 @@ github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= +github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/runc v1.1.14 h1:rgSuzbmgz5DUJjeSnw337TxDbRuqjs6iqQck/2weR6w= +github.com/opencontainers/runc v1.1.14/go.mod h1:E4C2z+7BxR7GHXp0hAY53mek+x49X1LjPNeMTfRGvOA= github.com/opentracing/basictracer-go v1.1.0 h1:Oa1fTSBvAl8pa3U+IJYqrKm0NALwH9OsgwOqDv4xJW0= github.com/opentracing/basictracer-go v1.1.0/go.mod h1:V2HZueSJEp879yv285Aap1BS69fQMD+MNP1mRs6mBQc= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA= +github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI= github.com/pgavlin/fx v0.1.6 h1:r9jEg69DhNoCd3Xh0+5mIbdbS3PqWrVWujkY76MFRTU= github.com/pgavlin/fx v0.1.6/go.mod h1:KWZJ6fqBBSh8GxHYqwYCf3rYE7Gp2p0N8tJp8xv9u9M= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= @@ -165,6 +267,8 @@ github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.12.0 h1:vG/22IHpYupt+ZD+KOnRo5PqIr github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.12.0/go.mod h1:WnJK/yelFkTPdsx7jZuUZixRunf+QQlgCwoRi1mVF3A= github.com/pulumi/pulumi/sdk/v3 v3.117.0 h1:ImIsukZ2ZIYQG94uWdSZl9dJjJTosQSTsOQTauTNX7U= github.com/pulumi/pulumi/sdk/v3 v3.117.0/go.mod h1:kNea72+FQk82OjZ3yEP4dl6nbAl2ngE8PDBc0iFAaHg= +github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4= +github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= @@ -198,25 +302,68 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/texttheater/golang-levenshtein v1.0.1 h1:+cRNoVrfiwufQPhoMzB6N0Yf/Mqajr6t1lOv8GyGE2U= github.com/texttheater/golang-levenshtein v1.0.1/go.mod h1:PYAKrbF5sAiq9wd+H82hs7gNaen0CplQ9uvm6+enD/8= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= github.com/tweekmonster/luser v0.0.0-20161003172636-3fa38070dbd7 h1:X9dsIWPuuEJlPX//UmRKophhOKCGXc46RVIGuttks68= github.com/tweekmonster/luser v0.0.0-20161003172636-3fa38070dbd7/go.mod h1:UxoP3EypF8JfGEjAII8jx1q8rQyDnX8qdTCs/UQBVIE= github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= +github.com/uptrace/bun v1.2.5 h1:gSprL5xiBCp+tzcZHgENzJpXnmQwRM/A6s4HnBF85mc= +github.com/uptrace/bun v1.2.5/go.mod h1:vkQMS4NNs4VNZv92y53uBSHXRqYyJp4bGhMHgaNCQpY= +github.com/uptrace/bun/dialect/pgdialect v1.2.5 h1:dWLUxpjTdglzfBks2x+U2WIi+nRVjuh7Z3DLYVFswJk= +github.com/uptrace/bun/dialect/pgdialect v1.2.5/go.mod h1:stwnlE8/6x8cuQ2aXcZqwDK/d+6jxgO3iQewflJT6C4= +github.com/uptrace/bun/extra/bunotel v1.2.5 h1:kkuuTbrG9d5leYZuSBKhq2gtq346lIrxf98Mig2y128= +github.com/uptrace/bun/extra/bunotel v1.2.5/go.mod h1:rCHLszRZwppWE9cGDodO2FCI1qCrLwDjONp38KD3bA8= +github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 h1:H8wwQwTe5sL6x30z71lUgNiwBdeCHQjrphCfLwqIHGo= +github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2/go.mod h1:/kR4beFhlz2g+V5ik8jW+3PMiMQAPt29y6K64NNY53c= +github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 h1:ZjUj9BLYf9PEqBn8W/OapxhPjVRdC6CsXTdULHsyk5c= +github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2/go.mod h1:O8bHQfyinKwTXKkiKNGmLQS7vRsqRxIQTFZpYpHK3IQ= +github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 h1:3/aHKUq7qaFMWxyQV0W2ryNgg8x8rVeKVA20KJUkfS0= +github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2/go.mod h1:Zit4b8AQXaXvA68+nzmbyDzqiyFRISyw1JiD5JqUBjw= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xo/dburl v0.23.2 h1:Fl88cvayrgE56JA/sqhNMLljCW/b7RmG1mMkKMZUFgA= +github.com/xo/dburl v0.23.2/go.mod h1:uazlaAQxj4gkshhfuuYyvwCBouOmNnG2aDxTCFZpmL4= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zclconf/go-cty v1.13.2 h1:4GvrUxe/QUDYuJKAav4EYqdM47/kZa672LwmXFmEKT0= github.com/zclconf/go-cty v1.13.2/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= +go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= +go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= +go.opentelemetry.io/otel/log v0.7.0 h1:d1abJc0b1QQZADKvfe9JqqrfmPYQCz2tUSO+0XZmuV4= +go.opentelemetry.io/otel/log v0.7.0/go.mod h1:2jf2z7uVfnzDNknKTO9G+ahcOAyWcp1fJmk/wJjULRo= +go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= +go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= +go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= +go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= +go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= +go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw= +go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg= +go.uber.org/fx v1.23.0/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -232,8 +379,8 @@ golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2Uz golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -265,6 +412,8 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -272,9 +421,13 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -307,12 +460,14 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 h1:zciRKQ4kBpFgpfC5QQCVtnnNAcLIqweL7plyZRQHVpI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= diff --git a/test/rolling-upgrades/main_test.go b/test/rolling-upgrades/main_test.go index b2b98339e..bfa92f7b9 100644 --- a/test/rolling-upgrades/main_test.go +++ b/test/rolling-upgrades/main_test.go @@ -5,6 +5,7 @@ import ( "flag" "fmt" "github.com/formancehq/go-libs/v2/logging" + ledger "github.com/formancehq/ledger/internal" corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1" "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v3" metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/meta/v1" @@ -64,9 +65,10 @@ func TestK8SRollingUpgrades(t *testing.T) { "postgres.uri": auto.ConfigValue{ Value: "postgres://ledger:ledger@" + pgStackOutputs["service-name"].Value.(string) + ".svc.cluster.local:5432/ledger?sslmode=disable", }, - "debug": auto.ConfigValue{Value: "true"}, - "image.pullPolicy": auto.ConfigValue{Value: "Always"}, - "replicaCount": auto.ConfigValue{Value: "1"}, + "debug": auto.ConfigValue{Value: "true"}, + "image.pullPolicy": auto.ConfigValue{Value: "Always"}, + "replicaCount": auto.ConfigValue{Value: "1"}, + "experimentalFeatures": auto.ConfigValue{Value: "true"}, }, ) require.NoError(t, err, "setting config on ledger stack") @@ -101,6 +103,7 @@ func TestK8SRollingUpgrades(t *testing.T) { require.NoError(t, err, "upping test stack") // Let a moment ensure the test image is actually sending requests + // We could maybe find a dynamic way to do that <-time.After(5 * time.Second) err = ledgerStack.SetConfig(ctx, "version", auto.ConfigValue{ @@ -119,7 +122,12 @@ func TestK8SRollingUpgrades(t *testing.T) { *stackPrefixName+"check", *projectName, func(ctx *pulumi.Context) error { - pod, err := corev1.GetPod(ctx, testStackOutputs["name"].Value.(string), pulumi.ID(testStackOutputs["id"].Value.(string)), nil) + pod, err := corev1.GetPod( + ctx, + testStackOutputs["name"].Value.(string), + pulumi.ID(testStackOutputs["id"].Value.(string)), + nil, + ) if err != nil { return err } @@ -182,6 +190,20 @@ func deployTest(ctx *pulumi.Context) error { image := conf.Require("image") ledgerURL := conf.Require("ledger-url") + generatorArgs := pulumi.StringArray{ + pulumi.String(ledgerURL), + pulumi.String("/examples/example1.js"), + pulumi.String("-p"), + pulumi.String("30"), + } + + for _, key := range ledger.MinimalFeatureSet.SortedKeys() { + generatorArgs = append(generatorArgs, + pulumi.String("--ledger-feature"), + pulumi.String(key+"="+ledger.MinimalFeatureSet[key]), + ) + } + rel, err := corev1.NewPod( ctx, "test", @@ -193,13 +215,8 @@ func deployTest(ctx *pulumi.Context) error { RestartPolicy: pulumi.String("Never"), Containers: corev1.ContainerArray{ corev1.ContainerArgs{ - Name: pulumi.String("test"), - Args: pulumi.StringArray{ - pulumi.String(ledgerURL), - pulumi.String("/examples/example1.js"), - pulumi.String("-p"), - pulumi.String("30"), - }, + Name: pulumi.String("test"), + Args: generatorArgs, Image: pulumi.String(image), ImagePullPolicy: pulumi.String("Always"), }, diff --git a/tools/generator/cmd/root.go b/tools/generator/cmd/root.go index 938896bda..3fc1819ee 100644 --- a/tools/generator/cmd/root.go +++ b/tools/generator/cmd/root.go @@ -17,6 +17,7 @@ import ( "golang.org/x/sync/errgroup" "net/http" "os" + "strings" ) var ( @@ -29,6 +30,9 @@ var ( } parallelFlag = "parallel" ledgerFlag = "ledger" + ledgerMetadataFlag = "ledger-metadata" + ledgerBucketFlag = "ledger-bucket" + ledgerFeatureFlag = "ledger-feature" untilLogIDFlag = "until-log-id" clientIDFlag = "client-id" clientSecretFlag = "client-secret" @@ -36,6 +40,28 @@ var ( insecureSkipVerifyFlag = "insecure-skip-verify" ) +func init() { + rootCmd.Flags().String(clientIDFlag, "", "Client ID") + rootCmd.Flags().String(clientSecretFlag, "", "Client Secret") + rootCmd.Flags().String(authUrlFlag, "", "Auth URL") + rootCmd.Flags().Bool(insecureSkipVerifyFlag, false, "Skip TLS verification") + rootCmd.Flags().IntP(parallelFlag, "p", 1, "Number of parallel users") + rootCmd.Flags().StringP(ledgerFlag, "l", "default", "Ledger to feed") + rootCmd.Flags().Int64P(untilLogIDFlag, "u", 0, "Stop after this transaction ID") + rootCmd.Flags().String(ledgerBucketFlag, "", "Ledger bucket") + rootCmd.Flags().StringSlice(ledgerMetadataFlag, []string{}, "Ledger metadata") + rootCmd.Flags().StringSlice(ledgerFeatureFlag, []string{}, "Ledger features") + + rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} + +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + func run(cmd *cobra.Command, args []string) error { ledgerUrl := args[0] scriptLocation := args[1] @@ -50,11 +76,26 @@ func run(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get vu: %w", err) } - ledger, err := cmd.Flags().GetString(ledgerFlag) + targetedLedger, err := cmd.Flags().GetString(ledgerFlag) if err != nil { return fmt.Errorf("failed to get ledger: %w", err) } + ledgerBucket, err := cmd.Flags().GetString(ledgerBucketFlag) + if err != nil { + return fmt.Errorf("failed to get ledger bucket: %w", err) + } + + ledgerMetadata, err := extractSliceSliceFlag(cmd, ledgerMetadataFlag) + if err != nil { + return fmt.Errorf("failed to get ledger metadata: %w", err) + } + + ledgerFeatures, err := extractSliceSliceFlag(cmd, ledgerFeatureFlag) + if err != nil { + return fmt.Errorf("failed to get ledger features: %w", err) + } + untilLogID, err := cmd.Flags().GetInt64(untilLogIDFlag) if err != nil { return fmt.Errorf("failed to get untilLogID: %w", err) @@ -105,9 +146,14 @@ func run(cmd *cobra.Command, args []string) error { ledgerclient.WithClient(httpClient), ) - logging.FromContext(cmd.Context()).Infof("Creating ledger '%s' if not exists", ledger) + logging.FromContext(cmd.Context()).Infof("Creating ledger '%s' if not exists", targetedLedger) _, err = client.Ledger.V2.CreateLedger(cmd.Context(), operations.V2CreateLedgerRequest{ - Ledger: ledger, + Ledger: targetedLedger, + V2CreateLedgerRequest: &components.V2CreateLedgerRequest{ + Bucket: &ledgerBucket, + Metadata: ledgerMetadata, + Features: ledgerFeatures, + }, }) if err != nil { sdkError := &sdkerrors.V2ErrorResponse{} @@ -142,7 +188,7 @@ func run(cmd *cobra.Command, args []string) error { return fmt.Errorf("iteration %d/%d failed: %w", vu, iteration, err) } - ret, err := action.Apply(ctx, client.Ledger.V2, ledger) + ret, err := action.Apply(ctx, client.Ledger.V2, targetedLedger) if err != nil { if errors.Is(err, context.Canceled) { return nil @@ -160,20 +206,23 @@ func run(cmd *cobra.Command, args []string) error { return errGroup.Wait() } -func Execute() { - err := rootCmd.Execute() +func extractSliceSliceFlag(cmd *cobra.Command, flagName string) (map[string]string, error) { + + inputs, err := cmd.Flags().GetStringSlice(flagName) if err != nil { - os.Exit(1) + return nil, err } -} -func init() { - rootCmd.Flags().String(clientIDFlag, "", "Client ID") - rootCmd.Flags().String(clientSecretFlag, "", "Client Secret") - rootCmd.Flags().String(authUrlFlag, "", "Auth URL") - rootCmd.Flags().Bool(insecureSkipVerifyFlag, false, "Skip TLS verification") - rootCmd.Flags().IntP(parallelFlag, "p", 1, "Number of parallel users") - rootCmd.Flags().StringP(ledgerFlag, "l", "default", "Ledger to feed") - rootCmd.Flags().Int64P(untilLogIDFlag, "u", 0, "Stop after this transaction ID") - rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + ret := make(map[string]string) + + for _, input := range inputs { + parts := strings.SplitN(input, "=", 2) + if len(parts) != 2 { + return ret, fmt.Errorf("invalid metadata: %s", input) + } + + ret[parts[0]] = parts[1] + } + + return ret, nil } From 051666ca92c86f12156e270eee6a8261f0ea6bae Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 14 Nov 2024 09:43:46 +0100 Subject: [PATCH 14/71] feat: add generator global vars --- internal/api/v2/controllers_bulk.go | 1 - pkg/client/.speakeasy/gen.lock | 4 +-- pkg/client/.speakeasy/gen.yaml | 2 +- pkg/client/formance.go | 4 +-- pkg/generate/generator.go | 44 +++++++++++++++++++++++++---- pkg/generate/generator_test.go | 9 ++++-- tools/generator/cmd/root.go | 4 ++- 7 files changed, 54 insertions(+), 14 deletions(-) diff --git a/internal/api/v2/controllers_bulk.go b/internal/api/v2/controllers_bulk.go index ab1a0dbce..7ecbecf32 100644 --- a/internal/api/v2/controllers_bulk.go +++ b/internal/api/v2/controllers_bulk.go @@ -62,7 +62,6 @@ type BulkElement struct { type Result struct { ErrorCode string `json:"errorCode,omitempty"` ErrorDescription string `json:"errorDescription,omitempty"` - ErrorDetails string `json:"errorDetails,omitempty"` Data any `json:"data,omitempty"` ResponseType string `json:"responseType"` // Added for sdk generation (discriminator in oneOf) LogID int `json:"logID"` diff --git a/pkg/client/.speakeasy/gen.lock b/pkg/client/.speakeasy/gen.lock index 984658c2d..19f5d974f 100644 --- a/pkg/client/.speakeasy/gen.lock +++ b/pkg/client/.speakeasy/gen.lock @@ -5,8 +5,8 @@ management: docVersion: v1 speakeasyVersion: 1.351.0 generationVersion: 2.384.1 - releaseVersion: 0.4.28 - configChecksum: 9eb898e4ab7291afdada27c15d20aa5d + releaseVersion: 0.4.30 + configChecksum: b752be0bf02f575b11e046541aa57005 features: go: additionalDependencies: 0.1.0 diff --git a/pkg/client/.speakeasy/gen.yaml b/pkg/client/.speakeasy/gen.yaml index 943f49877..93655e172 100644 --- a/pkg/client/.speakeasy/gen.yaml +++ b/pkg/client/.speakeasy/gen.yaml @@ -15,7 +15,7 @@ generation: auth: oAuth2ClientCredentialsEnabled: true go: - version: 0.4.28 + version: 0.4.30 additionalDependencies: {} allowUnknownFieldsInWeakUnions: false clientServerStatusCodesAsErrors: true diff --git a/pkg/client/formance.go b/pkg/client/formance.go index 27f37ab7a..11a85fa6d 100644 --- a/pkg/client/formance.go +++ b/pkg/client/formance.go @@ -143,9 +143,9 @@ func New(opts ...SDKOption) *Formance { sdkConfiguration: sdkConfiguration{ Language: "go", OpenAPIDocVersion: "v1", - SDKVersion: "0.4.28", + SDKVersion: "0.4.30", GenVersion: "2.384.1", - UserAgent: "speakeasy-sdk/go 0.4.28 2.384.1 v1 github.com/formancehq/ledger/pkg/client", + UserAgent: "speakeasy-sdk/go 0.4.30 2.384.1 v1 github.com/formancehq/ledger/pkg/client", Hooks: hooks.New(), }, } diff --git a/pkg/generate/generator.go b/pkg/generate/generator.go index 0b1bfcd5d..11a718c0c 100644 --- a/pkg/generate/generator.go +++ b/pkg/generate/generator.go @@ -44,7 +44,7 @@ func (r Action) Apply(ctx context.Context, client *client.V2, l string) (*Result var bulkElement components.V2BulkElement switch r.Action { - case v2.ActionCreateTransaction, "": // Handling "" as CREATE_TRANSACTION for backward compatibility + case v2.ActionCreateTransaction: transactionRequest := &ledgercontroller.RunScript{} err := json.Unmarshal(r.Data, transactionRequest) if err != nil { @@ -161,6 +161,15 @@ func (r Action) Apply(ctx context.Context, client *client.V2, l string) (*Result if err != nil { return nil, fmt.Errorf("creating transaction: %w", err) } + if errorResponse := response.V2BulkResponse.Data[0].V2BulkElementResultError; errorResponse != nil { + if errorResponse.ErrorCode != "" { + errorDescription := errorResponse.ErrorDescription + if errorDescription == "" { + errorDescription = "" + } + return nil, fmt.Errorf("[%s] %s", errorResponse.ErrorCode, errorDescription) + } + } return &Result{response.V2BulkResponse.Data[0]}, nil } @@ -173,7 +182,13 @@ func (g *Generator) Next(iteration int) (*Action, error) { return g.next(iteration) } -func NewGenerator(script string) (*Generator, error) { +func NewGenerator(script string, opts ...Option) (*Generator, error) { + + cfg := &config{} + for _, opt := range opts { + opt(cfg) + } + runtime := goja.New() _, err := runtime.RunString(script) @@ -181,6 +196,13 @@ func NewGenerator(script string) (*Generator, error) { return nil, err } + for k, v := range cfg.globals { + err := runtime.Set(k, v) + if err != nil { + return nil, fmt.Errorf("failed to set global variable %s: %w", k, err) + } + } + runtime.SetFieldNameMapper(goja.TagFieldNameMapper("json", true)) err = runtime.Set("uuid", uuid.NewString) @@ -200,9 +222,9 @@ func NewGenerator(script string) (*Generator, error) { var ( action string - ik string - data map[string]any - ok bool + ik string + data map[string]any + ok bool ) rawAction := ret["action"] if rawAction == nil { @@ -246,3 +268,15 @@ func NewGenerator(script string) (*Generator, error) { }, }, nil } + +type config struct { + globals map[string]any +} + +type Option func(*config) + +func WithGlobals(globals map[string]any) Option { + return func(c *config) { + c.globals = globals + } +} diff --git a/pkg/generate/generator_test.go b/pkg/generate/generator_test.go index 8f99dc6ba..dd32406f4 100644 --- a/pkg/generate/generator_test.go +++ b/pkg/generate/generator_test.go @@ -36,7 +36,9 @@ func TestGenerator(t *testing.T) { }) require.NoError(t, err) - generator, err := NewGenerator(script) + generator, err := NewGenerator(script, WithGlobals(map[string]interface{}{ + "globalMetadata": "test", + })) require.NoError(t, err) const ledgerName = "default" @@ -57,7 +59,8 @@ func TestGenerator(t *testing.T) { require.True(t, txs.Data[1].Reverted) require.False(t, txs.Data[0].Reverted) require.Equal(t, map[string]string{ - "foo": "bar", + "foo": "bar", + "globalMetadata": "test", }, txs.Data[1].Metadata) } @@ -73,6 +76,8 @@ send [USD/2 100] ( source = @world destination = @bank ) + +set_tx_meta("globalMetadata", "${globalMetadata}") ` + "`" + ` } } diff --git a/tools/generator/cmd/root.go b/tools/generator/cmd/root.go index 3fc1819ee..be4fae102 100644 --- a/tools/generator/cmd/root.go +++ b/tools/generator/cmd/root.go @@ -171,7 +171,9 @@ func run(cmd *cobra.Command, args []string) error { logging.FromContext(cmd.Context()).Infof("Starting to generate data with %d vus", vus) for vu := 0; vu < vus; vu++ { - generator, err := generate.NewGenerator(string(fileContent)) + generator, err := generate.NewGenerator(string(fileContent), generate.WithGlobals(map[string]any{ + "vu": vu, + })) if err != nil { return fmt.Errorf("failed to create generator: %w", err) } From 655f98bc6f5c609fc1f274dffce314b51470e40f Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 14 Nov 2024 10:14:43 +0100 Subject: [PATCH 15/71] fix: missing hash in log list --- internal/README.md | 2 +- internal/log.go | 2 +- test/e2e/api_logs_list_test.go | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/internal/README.md b/internal/README.md index 9bd9a8d20..99da8b3f7 100644 --- a/internal/README.md +++ b/internal/README.md @@ -546,7 +546,7 @@ type Log struct { // It allows to check if the usage of IdempotencyKey match inputs given on the first idempotency key usage. IdempotencyHash string `json:"idempotencyHash" bun:"idempotency_hash,unique,nullzero"` ID int `json:"id" bun:"id,unique,type:numeric"` - Hash []byte `json:"hash" bun:"hash,type:bytea,scanonly"` + Hash []byte `json:"hash" bun:"hash,type:bytea"` } ``` diff --git a/internal/log.go b/internal/log.go index d611e3a9e..e5efc5980 100644 --- a/internal/log.go +++ b/internal/log.go @@ -91,7 +91,7 @@ type Log struct { // It allows to check if the usage of IdempotencyKey match inputs given on the first idempotency key usage. IdempotencyHash string `json:"idempotencyHash" bun:"idempotency_hash,unique,nullzero"` ID int `json:"id" bun:"id,unique,type:numeric"` - Hash []byte `json:"hash" bun:"hash,type:bytea,scanonly"` + Hash []byte `json:"hash" bun:"hash,type:bytea"` } func (l Log) WithIdempotencyKey(key string) Log { diff --git a/test/e2e/api_logs_list_test.go b/test/e2e/api_logs_list_test.go index 59183f74e..0d2d93a8c 100644 --- a/test/e2e/api_logs_list_test.go +++ b/test/e2e/api_logs_list_test.go @@ -134,6 +134,10 @@ var _ = Context("Ledger logs list API tests", func() { Expect(response.Data).To(HaveLen(3)) + for _, data := range response.Data { + Expect(data.Hash).NotTo(BeEmpty()) + } + // Cannot check the date and the hash since they are changing at // every run Expect(response.Data[0].ID).To(Equal(big.NewInt(3))) From 7f711b9557aaa26f56c05bf733f957257ac8f80c Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 14 Nov 2024 10:55:11 +0100 Subject: [PATCH 16/71] fix: defer http server startup after minimal schema upgrade --- cmd/buckets_upgrade.go | 38 +++++---------- cmd/root.go | 10 +++- internal/storage/bucket/bucket.go | 4 +- internal/storage/bucket/bucket_test.go | 2 +- internal/storage/bucket/migrations.go | 35 ++++++++++++- internal/storage/driver/driver.go | 50 +++++++++++++++---- internal/storage/driver/driver_test.go | 2 +- internal/storage/ledger/legacy/main_test.go | 2 +- internal/storage/module.go | 54 ++++++++++++--------- test/migrations/upgrade_test.go | 2 +- 10 files changed, 133 insertions(+), 66 deletions(-) diff --git a/cmd/buckets_upgrade.go b/cmd/buckets_upgrade.go index 49a7046c3..71ca745d6 100644 --- a/cmd/buckets_upgrade.go +++ b/cmd/buckets_upgrade.go @@ -14,31 +14,22 @@ func NewBucketUpgrade() *cobra.Command { Args: cobra.ExactArgs(1), SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { - connectionOptions, err := bunconnect.ConnectionOptionsFromFlags(cmd) - if err != nil { - return err - } + logger := logging.NewDefaultLogger(cmd.OutOrStdout(), service.IsDebug(cmd), false, false) + cmd.SetContext(logging.ContextWithLogger(cmd.Context(), logger)) - db, err := bunconnect.OpenSQLDB(cmd.Context(), *connectionOptions) + driver, err := getDriver(cmd) if err != nil { return err } defer func() { - _ = db.Close() + _ = driver.GetDB().Close() }() - driver := driver.New(db) - if err := driver.Initialize(cmd.Context()); err != nil { - return err - } - if args[0] == "*" { - return upgradeAll(cmd) + return driver.UpgradeAllBuckets(cmd.Context(), make(chan struct{})) } - logger := logging.NewDefaultLogger(cmd.OutOrStdout(), service.IsDebug(cmd), false, false) - - return driver.UpgradeBucket(logging.ContextWithLogger(cmd.Context(), logger), args[0]) + return driver.UpgradeBucket(cmd.Context(), args[0]) }, } @@ -48,27 +39,22 @@ func NewBucketUpgrade() *cobra.Command { return cmd } -func upgradeAll(cmd *cobra.Command) error { - logger := logging.NewDefaultLogger(cmd.OutOrStdout(), service.IsDebug(cmd), false, false) - ctx := logging.ContextWithLogger(cmd.Context(), logger) +func getDriver(cmd *cobra.Command) (*driver.Driver, error) { connectionOptions, err := bunconnect.ConnectionOptionsFromFlags(cmd) if err != nil { - return err + return nil, err } db, err := bunconnect.OpenSQLDB(cmd.Context(), *connectionOptions) if err != nil { - return err + return nil, err } - defer func() { - _ = db.Close() - }() driver := driver.New(db) - if err := driver.Initialize(ctx); err != nil { - return err + if err := driver.Initialize(cmd.Context()); err != nil { + return nil, err } - return driver.UpgradeAllBuckets(ctx) + return driver, nil } diff --git a/cmd/root.go b/cmd/root.go index b363efb89..960079ee4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -36,7 +36,15 @@ func NewRootCommand() *cobra.Command { root.AddCommand(version) root.AddCommand(bunmigrate.NewDefaultCommand(func(cmd *cobra.Command, _ []string, _ *bun.DB) error { // todo: use provided db ... - return upgradeAll(cmd) + driver, err := getDriver(cmd) + if err != nil { + return err + } + defer func() { + _ = driver.GetDB().Close() + }() + + return driver.UpgradeAllBuckets(cmd.Context(), make(chan struct{})) })) root.AddCommand(NewDocsCommand()) diff --git a/internal/storage/bucket/bucket.go b/internal/storage/bucket/bucket.go index 8db0c7dda..2b53f13da 100644 --- a/internal/storage/bucket/bucket.go +++ b/internal/storage/bucket/bucket.go @@ -20,8 +20,8 @@ type Bucket struct { db *bun.DB } -func (b *Bucket) Migrate(ctx context.Context, tracer trace.Tracer) error { - return migrate(ctx, tracer, b.db, b.name) +func (b *Bucket) Migrate(ctx context.Context, tracer trace.Tracer, minimalVersionReached chan struct{}) error { + return migrate(ctx, tracer, b.db, b.name, minimalVersionReached) } func (b *Bucket) HasMinimalVersion(ctx context.Context) (bool, error) { diff --git a/internal/storage/bucket/bucket_test.go b/internal/storage/bucket/bucket_test.go index 55392b299..0b26c0f49 100644 --- a/internal/storage/bucket/bucket_test.go +++ b/internal/storage/bucket/bucket_test.go @@ -31,5 +31,5 @@ func TestBuckets(t *testing.T) { require.NoError(t, driver.Migrate(ctx, db)) b := bucket.New(db, name) - require.NoError(t, b.Migrate(ctx, noop.Tracer{})) + require.NoError(t, b.Migrate(ctx, noop.Tracer{}, make(chan struct{}))) } diff --git a/internal/storage/bucket/migrations.go b/internal/storage/bucket/migrations.go index cca904cd5..a13197ad0 100644 --- a/internal/storage/bucket/migrations.go +++ b/internal/storage/bucket/migrations.go @@ -3,6 +3,7 @@ package bucket import ( "context" "embed" + "errors" "github.com/formancehq/go-libs/v2/migrations" "github.com/uptrace/bun" "go.opentelemetry.io/otel/trace" @@ -22,9 +23,39 @@ func GetMigrator(db *bun.DB, name string) *migrations.Migrator { return migrator } -func migrate(ctx context.Context, tracer trace.Tracer, db *bun.DB, name string) error { +func migrate(ctx context.Context, tracer trace.Tracer, db *bun.DB, name string, minimalVersionReached chan struct{}) error { ctx, span := tracer.Start(ctx, "Migrate bucket") defer span.End() - return GetMigrator(db, name).Up(ctx) + migrator := GetMigrator(db, name) + version, err := migrator.GetLastVersion(ctx) + if err != nil { + if !errors.Is(err, migrations.ErrMissingVersionTable) { + return err + } + } + + if version >= MinimalSchemaVersion { + close(minimalVersionReached) + } + + for { + err := migrator.UpByOne(ctx) + if err != nil { + if errors.Is(err, migrations.ErrAlreadyUpToDate) { + return nil + } + return err + } + version++ + + if version >= MinimalSchemaVersion { + select { + case <-minimalVersionReached: + // already closed + default: + close(minimalVersionReached) + } + } + } } diff --git a/internal/storage/driver/driver.go b/internal/storage/driver/driver.go index a260fdfdf..16d94b0cf 100644 --- a/internal/storage/driver/driver.go +++ b/internal/storage/driver/driver.go @@ -11,6 +11,7 @@ import ( noopmetrics "go.opentelemetry.io/otel/metric/noop" "go.opentelemetry.io/otel/trace" nooptracer "go.opentelemetry.io/otel/trace/noop" + "golang.org/x/sync/errgroup" ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" @@ -40,7 +41,7 @@ func (d *Driver) CreateLedger(ctx context.Context, l *ledger.Ledger) (*ledgersto } b := bucket.New(d.db, l.Bucket) - if err := b.Migrate(ctx, d.tracer); err != nil { + if err := b.Migrate(ctx, d.tracer, make(chan struct{})); err != nil { return nil, fmt.Errorf("migrating bucket: %w", err) } @@ -188,10 +189,13 @@ func (d *Driver) GetLedger(ctx context.Context, name string) (*ledger.Ledger, er } func (d *Driver) UpgradeBucket(ctx context.Context, name string) error { - return bucket.New(d.db, name).Migrate(ctx, d.tracer) + if err := bucket.New(d.db, name).Migrate(ctx, d.tracer, make(chan struct{})); err != nil { + return fmt.Errorf("migrating bucket '%s': %w", name, err) + } + return nil } -func (d *Driver) UpgradeAllBuckets(ctx context.Context) error { +func (d *Driver) UpgradeAllBuckets(ctx context.Context, minimalVersionReached chan struct{}) error { var buckets []string err := d.db.NewSelect(). @@ -203,17 +207,45 @@ func (d *Driver) UpgradeAllBuckets(ctx context.Context) error { return fmt.Errorf("getting buckets: %w", err) } + sem := make(chan struct{}, len(buckets)) + + grp, ctx := errgroup.WithContext(ctx) for _, bucketName := range buckets { - b := bucket.New(d.db, bucketName) + grp.Go(func() error { + b := bucket.New(d.db, bucketName) + + minimalVersionReached := make(chan struct{}) + + go func() { + select { + case <-ctx.Done(): + return + case <-minimalVersionReached: + sem <- struct{}{} + } + }() + + logging.FromContext(ctx).Infof("Upgrading bucket '%s'", bucketName) + if err := b.Migrate(ctx, d.tracer, minimalVersionReached); err != nil { + return err + } + logging.FromContext(ctx).Infof("Bucket '%s' up to date", bucketName) + + return nil + }) + } - logging.FromContext(ctx).Infof("Upgrading bucket '%s'", bucketName) - if err := b.Migrate(ctx, d.tracer); err != nil { - return err + for i := 0; i < len(buckets); i++ { + select { + case <-ctx.Done(): + return ctx.Err() + case <-sem: } - logging.FromContext(ctx).Infof("Bucket '%s' up to date", bucketName) } - return nil + close(minimalVersionReached) + + return grp.Wait() } func (d *Driver) GetDB() *bun.DB { diff --git a/internal/storage/driver/driver_test.go b/internal/storage/driver/driver_test.go index 62ec6b70c..958c7db8a 100644 --- a/internal/storage/driver/driver_test.go +++ b/internal/storage/driver/driver_test.go @@ -37,7 +37,7 @@ func TestUpgradeAllLedgers(t *testing.T) { require.NoError(t, err) } - require.NoError(t, d.UpgradeAllBuckets(ctx)) + require.NoError(t, d.UpgradeAllBuckets(ctx, make(chan struct{}))) } func TestLedgersCreate(t *testing.T) { diff --git a/internal/storage/ledger/legacy/main_test.go b/internal/storage/ledger/legacy/main_test.go index 266753db8..52d294471 100644 --- a/internal/storage/ledger/legacy/main_test.go +++ b/internal/storage/ledger/legacy/main_test.go @@ -70,7 +70,7 @@ func newLedgerStore(t T) *testStore { l.Bucket = ledgerName b := bucket.New(db, ledgerName) - require.NoError(t, b.Migrate(ctx, noop.Tracer{})) + require.NoError(t, b.Migrate(ctx, noop.Tracer{}, make(chan struct{}))) require.NoError(t, b.AddLedger(ctx, l, db)) return &testStore{ diff --git a/internal/storage/module.go b/internal/storage/module.go index 1897c97e6..b0aa35370 100644 --- a/internal/storage/module.go +++ b/internal/storage/module.go @@ -19,6 +19,7 @@ func NewFXModule(autoUpgrade bool) fx.Option { upgradeContext context.Context cancelContext func() upgradeStopped = make(chan struct{}) + minimalVersionReached = make(chan struct{}) ) lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { @@ -29,29 +30,15 @@ func NewFXModule(autoUpgrade bool) fx.Option { go func() { defer close(upgradeStopped) - for { - select { - case <-ctx.Done(): - return - default: - logging.FromContext(ctx).Infof("Upgrading buckets...") - if err := driver.UpgradeAllBuckets(upgradeContext); err != nil { - // Long migrations can be cancelled (app rescheduled for example) - // before fully terminated, handle this gracefully, don't panic, - // the next start will try again. - if errors.Is(err, context.DeadlineExceeded) || - errors.Is(err, context.Canceled) { - return - } - logging.FromContext(ctx).Errorf("Upgrading buckets: %s", err) - continue - } - return - } - } - + migrate(upgradeContext, driver, minimalVersionReached) }() - return nil + + select { + case <-ctx.Done(): + return ctx.Err() + case <-minimalVersionReached: + return nil + } }, OnStop: func(ctx context.Context) error { cancelContext() @@ -68,3 +55,26 @@ func NewFXModule(autoUpgrade bool) fx.Option { } return fx.Options(ret...) } + +func migrate(ctx context.Context, driver *driver.Driver, minimalVersionReached chan struct{}) { + for { + select { + case <-ctx.Done(): + return + default: + logging.FromContext(ctx).Infof("Upgrading buckets...") + if err := driver.UpgradeAllBuckets(ctx, minimalVersionReached); err != nil { + // Long migrations can be cancelled (app rescheduled for example) + // before fully terminated, handle this gracefully, don't panic, + // the next start will try again. + if errors.Is(err, context.DeadlineExceeded) || + errors.Is(err, context.Canceled) { + return + } + logging.FromContext(ctx).Errorf("Upgrading buckets: %s", err) + continue + } + return + } + } +} \ No newline at end of file diff --git a/test/migrations/upgrade_test.go b/test/migrations/upgrade_test.go index 2abbad014..412fcbed3 100644 --- a/test/migrations/upgrade_test.go +++ b/test/migrations/upgrade_test.go @@ -53,7 +53,7 @@ func TestMigrations(t *testing.T) { // Migrate database driver := driver.New(db) require.NoError(t, driver.Initialize(ctx)) - require.NoError(t, driver.UpgradeAllBuckets(ctx)) + require.NoError(t, driver.UpgradeAllBuckets(ctx, make(chan struct{}))) } func copyDatabase(t *testing.T, dockerPool *docker.Pool, source, destination string) { From d4721e2af7f66a2430e725eddbe87dc2bc549fbd Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 14 Nov 2024 13:49:26 +0100 Subject: [PATCH 17/71] fix: concurrent ledger creation can lead to deadlock --- go.mod | 86 ++-- go.sum | 172 ++++---- internal/README.md | 10 + internal/ledger.go | 10 + internal/storage/bucket/bucket.go | 397 ++++++++++++------- internal/storage/bucket/migrations.go | 9 +- internal/storage/driver/driver.go | 41 +- internal/storage/driver/driver_test.go | 70 +++- internal/storage/driver/migrations.go | 10 +- internal/storage/ledger/accounts.go | 2 + internal/storage/ledger/balances.go | 10 +- internal/storage/ledger/balances_test.go | 4 +- internal/storage/ledger/legacy/main_test.go | 5 +- internal/storage/ledger/main_test.go | 5 +- internal/storage/ledger/transactions.go | 1 + internal/storage/ledger/transactions_test.go | 3 +- internal/storage/ledger/volumes.go | 1 + test/rolling-upgrades/go.mod | 18 +- test/rolling-upgrades/go.sum | 96 ++--- tools/generator/Earthfile | 2 +- tools/generator/go.mod | 113 +----- tools/generator/go.sum | 230 ++++------- 22 files changed, 677 insertions(+), 618 deletions(-) diff --git a/go.mod b/go.mod index 1088b5270..2fca2d73f 100644 --- a/go.mod +++ b/go.mod @@ -9,12 +9,12 @@ replace github.com/formancehq/ledger/pkg/client => ./pkg/client replace google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215 => google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 require ( - github.com/ThreeDotsLabs/watermill v1.3.7 + github.com/ThreeDotsLabs/watermill v1.4.1 github.com/alitto/pond v1.9.2 github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 github.com/bluele/gcache v0.0.2 github.com/dop251/goja v0.0.0-20241009100908-5f46f2705ca3 - github.com/formancehq/go-libs/v2 v2.0.1-0.20241107141636-5509ff77294e + github.com/formancehq/go-libs/v2 v2.0.1-0.20241114125605-4a3e447246a9 github.com/formancehq/ledger/pkg/client v0.0.0-00010101000000-000000000000 github.com/go-chi/chi/v5 v5.1.0 github.com/go-chi/cors v1.2.1 @@ -25,8 +25,8 @@ require ( github.com/jamiealquiza/tachymeter v2.0.0+incompatible github.com/logrusorgru/aurora v2.0.3+incompatible github.com/nats-io/nats.go v1.37.0 - github.com/onsi/ginkgo/v2 v2.20.2 - github.com/onsi/gomega v1.34.2 + github.com/onsi/ginkgo/v2 v2.21.0 + github.com/onsi/gomega v1.35.1 github.com/ory/dockertest/v3 v3.11.0 github.com/pborman/uuid v1.2.1 github.com/pkg/errors v0.9.1 @@ -36,17 +36,17 @@ require ( github.com/stretchr/testify v1.9.0 github.com/uptrace/bun v1.2.5 github.com/uptrace/bun/dialect/pgdialect v1.2.5 - github.com/uptrace/bun/extra/bundebug v1.2.3 + github.com/uptrace/bun/extra/bundebug v1.2.5 github.com/xeipuuv/gojsonschema v1.2.0 github.com/xo/dburl v0.23.2 - go.opentelemetry.io/otel v1.31.0 - go.opentelemetry.io/otel/metric v1.31.0 - go.opentelemetry.io/otel/sdk/metric v1.31.0 - go.opentelemetry.io/otel/trace v1.31.0 + go.opentelemetry.io/otel v1.32.0 + go.opentelemetry.io/otel/metric v1.32.0 + go.opentelemetry.io/otel/sdk/metric v1.32.0 + go.opentelemetry.io/otel/trace v1.32.0 go.uber.org/fx v1.23.0 go.uber.org/mock v0.5.0 golang.org/x/oauth2 v0.23.0 - golang.org/x/sync v0.8.0 + golang.org/x/sync v0.9.0 ) require ( @@ -64,23 +64,23 @@ require ( github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/ThreeDotsLabs/watermill-http/v2 v2.3.1 // indirect github.com/ThreeDotsLabs/watermill-kafka/v3 v3.0.5 // indirect - github.com/ThreeDotsLabs/watermill-nats/v2 v2.1.1 // indirect + github.com/ThreeDotsLabs/watermill-nats/v2 v2.1.2 // indirect github.com/ajg/form v1.5.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/aws/aws-msk-iam-sasl-signer-go v1.0.0 // indirect - github.com/aws/aws-sdk-go-v2 v1.32.3 // indirect - github.com/aws/aws-sdk-go-v2/config v1.28.1 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.17.42 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.18 // indirect - github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.22 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.22 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.22 // indirect + github.com/aws/aws-sdk-go-v2 v1.32.4 // indirect + github.com/aws/aws-sdk-go-v2/config v1.28.3 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.44 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19 // indirect + github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.3 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.24.3 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.3 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.32.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.24.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.32.4 // indirect github.com/aws/smithy-go v1.22.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect @@ -112,12 +112,12 @@ require ( github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect + github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/schema v1.4.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -145,7 +145,7 @@ require ( github.com/muhlemmer/gu v0.3.1 // indirect github.com/muhlemmer/httpforwarded v0.1.0 // indirect github.com/nats-io/jwt/v2 v2.7.0 // indirect - github.com/nats-io/nats-server/v2 v2.10.21 // indirect + github.com/nats-io/nats-server/v2 v2.10.22 // indirect github.com/nats-io/nkeys v0.4.7 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/oklog/ulid v1.3.1 // indirect @@ -159,7 +159,7 @@ require ( github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect github.com/riandyrn/otelchi v0.10.1 // indirect github.com/rs/cors v1.11.1 // indirect - github.com/shirou/gopsutil/v4 v4.24.9 // indirect + github.com/shirou/gopsutil/v4 v4.24.10 // indirect github.com/shomali11/util v0.0.0-20220717175126-f0771b70947f // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/tklauser/go-sysconf v0.3.14 // indirect @@ -179,19 +179,19 @@ require ( github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/zitadel/oidc/v2 v2.12.2 // indirect - go.opentelemetry.io/contrib/instrumentation/host v0.56.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect - go.opentelemetry.io/contrib/instrumentation/runtime v0.56.0 // indirect - go.opentelemetry.io/contrib/propagators/b3 v1.31.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.31.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.31.0 // indirect + go.opentelemetry.io/contrib/instrumentation/host v0.57.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 // indirect + go.opentelemetry.io/contrib/instrumentation/runtime v0.57.0 // indirect + go.opentelemetry.io/contrib/propagators/b3 v1.32.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 // indirect go.opentelemetry.io/otel/log v0.7.0 // indirect - go.opentelemetry.io/otel/sdk v1.31.0 // indirect + go.opentelemetry.io/otel/sdk v1.32.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/dig v1.18.0 // indirect go.uber.org/multierr v1.11.0 // indirect @@ -199,12 +199,12 @@ require ( golang.org/x/crypto v0.28.0 // indirect golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect golang.org/x/net v0.30.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/text v0.19.0 // indirect - golang.org/x/time v0.6.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.20.0 // indirect + golang.org/x/time v0.7.0 // indirect golang.org/x/tools v0.26.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 // indirect google.golang.org/grpc v1.67.1 // indirect google.golang.org/protobuf v1.35.1 // indirect gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect diff --git a/go.sum b/go.sum index f9e76220b..1cb9b579f 100644 --- a/go.sum +++ b/go.sum @@ -12,14 +12,14 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= -github.com/ThreeDotsLabs/watermill v1.3.7 h1:NV0PSTmuACVEOV4dMxRnmGXrmbz8U83LENOvpHekN7o= -github.com/ThreeDotsLabs/watermill v1.3.7/go.mod h1:lBnrLbxOjeMRgcJbv+UiZr8Ylz8RkJ4m6i/VN/Nk+to= +github.com/ThreeDotsLabs/watermill v1.4.1 h1:gjP6yZH+otMPjV0KsV07pl9TeMm9UQV/gqiuiuG5Drs= +github.com/ThreeDotsLabs/watermill v1.4.1/go.mod h1:lBnrLbxOjeMRgcJbv+UiZr8Ylz8RkJ4m6i/VN/Nk+to= github.com/ThreeDotsLabs/watermill-http/v2 v2.3.1 h1:M0iYM5HsGcoxtiQqprRlYZNZnGk3w5LsE9RbC2R8myQ= github.com/ThreeDotsLabs/watermill-http/v2 v2.3.1/go.mod h1:RwGHEzGsEEXC/rQNLWQqR83+WPlABgOgnv2kTB56Y4Y= github.com/ThreeDotsLabs/watermill-kafka/v3 v3.0.5 h1:ud+4txnRgtr3kZXfXZ5+C7kVQEvsLc5HSNUEa0g+X1Q= github.com/ThreeDotsLabs/watermill-kafka/v3 v3.0.5/go.mod h1:t4o+4A6GB+XC8WL3DandhzPwd265zQuyWMQC/I+WIOU= -github.com/ThreeDotsLabs/watermill-nats/v2 v2.1.1 h1:afAkAFzeooBRQvxElR+6xoigXKCukcZXnE9ACxhwlPI= -github.com/ThreeDotsLabs/watermill-nats/v2 v2.1.1/go.mod h1:stjbT+s4u/s5ime5jdIyvPyjBGwGeJewIN7jxH8gp4k= +github.com/ThreeDotsLabs/watermill-nats/v2 v2.1.2 h1:9d7Vb2gepq73Rn/aKaAJWbBiJzS6nDyOm4O353jVsTM= +github.com/ThreeDotsLabs/watermill-nats/v2 v2.1.2/go.mod h1:stjbT+s4u/s5ime5jdIyvPyjBGwGeJewIN7jxH8gp4k= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/alitto/pond v1.9.2 h1:9Qb75z/scEZVCoSU+osVmQ0I0JOeLfdTDafrbcJ8CLs= @@ -30,32 +30,32 @@ github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYW github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/aws/aws-msk-iam-sasl-signer-go v1.0.0 h1:UyjtGmO0Uwl/K+zpzPwLoXzMhcN9xmnR2nrqJoBrg3c= github.com/aws/aws-msk-iam-sasl-signer-go v1.0.0/go.mod h1:TJAXuFs2HcMib3sN5L0gUC+Q01Qvy3DemvA55WuC+iA= -github.com/aws/aws-sdk-go-v2 v1.32.3 h1:T0dRlFBKcdaUPGNtkBSwHZxrtis8CQU17UpNBZYd0wk= -github.com/aws/aws-sdk-go-v2 v1.32.3/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo= -github.com/aws/aws-sdk-go-v2/config v1.28.1 h1:oxIvOUXy8x0U3fR//0eq+RdCKimWI900+SV+10xsCBw= -github.com/aws/aws-sdk-go-v2/config v1.28.1/go.mod h1:bRQcttQJiARbd5JZxw6wG0yIK3eLeSCPdg6uqmmlIiI= -github.com/aws/aws-sdk-go-v2/credentials v1.17.42 h1:sBP0RPjBU4neGpIYyx8mkU2QqLPl5u9cmdTWVzIpHkM= -github.com/aws/aws-sdk-go-v2/credentials v1.17.42/go.mod h1:FwZBfU530dJ26rv9saAbxa9Ej3eF/AK0OAY86k13n4M= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.18 h1:68jFVtt3NulEzojFesM/WVarlFpCaXLKaBxDpzkQ9OQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.18/go.mod h1:Fjnn5jQVIo6VyedMc0/EhPpfNlPl7dHV916O6B+49aE= -github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.22 h1:n89ziXnsp3dyOlodim8OHv0edSu47H7i75UYxDz1YVQ= -github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.22/go.mod h1:7uC80VxwPjAykLSIzkyTgZ+LjFDil+OVndzd8wGMOYY= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.22 h1:Jw50LwEkVjuVzE1NzkhNKkBf9cRN7MtE1F/b2cOKTUM= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.22/go.mod h1:Y/SmAyPcOTmpeVaWSzSKiILfXTVJwrGmYZhcRbhWuEY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.22 h1:981MHwBaRZM7+9QSR6XamDzF/o7ouUGxFzr+nVSIhrs= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.22/go.mod h1:1RA1+aBEfn+CAB/Mh0MB6LsdCYCnjZm7tKXtnk499ZQ= +github.com/aws/aws-sdk-go-v2 v1.32.4 h1:S13INUiTxgrPueTmrm5DZ+MiAo99zYzHEFh1UNkOxNE= +github.com/aws/aws-sdk-go-v2 v1.32.4/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo= +github.com/aws/aws-sdk-go-v2/config v1.28.3 h1:kL5uAptPcPKaJ4q0sDUjUIdueO18Q7JDzl64GpVwdOM= +github.com/aws/aws-sdk-go-v2/config v1.28.3/go.mod h1:SPEn1KA8YbgQnwiJ/OISU4fz7+F6Fe309Jf0QTsRCl4= +github.com/aws/aws-sdk-go-v2/credentials v1.17.44 h1:qqfs5kulLUHUEXlHEZXLJkgGoF3kkUeFUTVA585cFpU= +github.com/aws/aws-sdk-go-v2/credentials v1.17.44/go.mod h1:0Lm2YJ8etJdEdw23s+q/9wTpOeo2HhNE97XcRa7T8MA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19 h1:woXadbf0c7enQ2UGCi8gW/WuKmE0xIzxBF/eD94jMKQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19/go.mod h1:zminj5ucw7w0r65bP6nhyOd3xL6veAUMc3ElGMoLVb4= +github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.23 h1:B2qK61ZXCQu8tkD6eG/gUiIt9Vw9tmWFD7Xo02JPdMY= +github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.23/go.mod h1:02rz9vMZsrOX9IwUcpoGZM4jPprFNPmtD6t9Ume9ECY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23 h1:A2w6m6Tmr+BNXjDsr7M90zkWjsu4JXHwrzPg235STs4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23/go.mod h1:35EVp9wyeANdujZruvHiQUAo9E3vbhnIO1mTCAxMlY0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23 h1:pgYW9FCabt2M25MoHYCfMrVY2ghiiBKYWUVXfwZs+sU= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23/go.mod h1:c48kLgzO19wAu3CPkDWC28JbaJ+hfQlsdl7I2+oqIbk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.3 h1:qcxX0JYlgWH3hpPUnd6U0ikcl6LLA9sLkXE2w1fpMvY= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.3/go.mod h1:cLSNEmI45soc+Ef8K/L+8sEA3A3pYFEYf5B5UI+6bH4= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.3 h1:UTpsIf0loCIWEbrqdLb+0RxnTXfWh2vhw4nQmFi4nPc= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.3/go.mod h1:FZ9j3PFHHAR+w0BSEjK955w5YD2UwB/l/H0yAK3MJvI= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.3 h1:2YCmIXv3tmiItw0LlYf6v7gEHebLY45kBEnPezbUKyU= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.3/go.mod h1:u19stRyNPxGhj6dRm+Cdgu6N75qnbW7+QN0q0dsAk58= -github.com/aws/aws-sdk-go-v2/service/sts v1.32.3 h1:wVnQ6tigGsRqSWDEEyH6lSAJ9OyFUsSnbaUWChuSGzs= -github.com/aws/aws-sdk-go-v2/service/sts v1.32.3/go.mod h1:VZa9yTFyj4o10YGsmDO4gbQJUvvhY72fhumT8W4LqsE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4 h1:tHxQi/XHPK0ctd/wdOw0t7Xrc2OxcRCnVzv8lwWPu0c= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4/go.mod h1:4GQbF1vJzG60poZqWatZlhP31y8PGCCVTvIGPdaaYJ0= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.5 h1:HJwZwRt2Z2Tdec+m+fPjvdmkq2s9Ra+VR0hjF7V2o40= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.5/go.mod h1:wrMCEwjFPms+V86TCQQeOxQF/If4vT44FGIOFiMC2ck= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4 h1:zcx9LiGWZ6i6pjdcoE9oXAB6mUdeyC36Ia/QEiIvYdg= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4/go.mod h1:Tp/ly1cTjRLGBBmNccFumbZ8oqpZlpdhFf80SrRh4is= +github.com/aws/aws-sdk-go-v2/service/sts v1.32.4 h1:yDxvkz3/uOKfxnv8YhzOi9m+2OGIxF+on3KOISbK5IU= +github.com/aws/aws-sdk-go-v2/service/sts v1.32.4/go.mod h1:9XEUty5v5UAsMiFOBJrNibZgwCeOma73jgGwwhgffa8= github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= @@ -104,8 +104,8 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241107141636-5509ff77294e h1:O7HXIkgcHTY81Ehoex+a2JpmKWP+Co6eYZdoHX0r96w= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241107141636-5509ff77294e/go.mod h1:DTqSp28pYPZa4O1WrOg3kobhgTHdk9geGtxnws9EViM= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241114125605-4a3e447246a9 h1:l6jieaR+sn4Ff+puBDMbTYmT2HTYC7Yt7GTxBAwC3eU= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241114125605-4a3e447246a9/go.mod h1:m0uKkey9OC/AeyWMwjMfZqhLzoWrPFBk8vuYdSSYj4Y= github.com/formancehq/numscript v0.0.9-0.20241009144012-1150c14a1417 h1:LOd5hxnXDIBcehFrpW1OnXk+VSs0yJXeu1iAOO+Hji4= github.com/formancehq/numscript v0.0.9-0.20241009144012-1150c14a1417/go.mod h1:btuSv05cYwi9BvLRxVs5zrunU+O1vTgigG1T6UsawcY= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= @@ -150,8 +150,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 h1:c5FlPPgxOn7kJz3VoPLkQYQXGBS3EklQ4Zfi57uOuqQ= -github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -166,8 +166,8 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 h1:ad0vkEBuk23VJzZR9nkLVG0YAoN9coASF1GusYX6AlU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0/go.mod h1:igFoXX2ELCW06bol23DWPB5BEWfZISOzSP5K2sbLea0= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -256,8 +256,8 @@ github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/ github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0= github.com/nats-io/jwt/v2 v2.7.0 h1:J+ZnaaMGQi3xSB8iOhVM5ipiWCDrQvgEoitTwWFyOYw= github.com/nats-io/jwt/v2 v2.7.0/go.mod h1:ZdWS1nZa6WMZfFwwgpEaqBV8EPGVgOTDHN/wTbz0Y5A= -github.com/nats-io/nats-server/v2 v2.10.21 h1:gfG6T06wBdI25XyY2IsauarOc2srWoFxxfsOKjrzoRA= -github.com/nats-io/nats-server/v2 v2.10.21/go.mod h1:I1YxSAEWbXCfy0bthwvNb5X43WwIWMz7gx5ZVPDr5Rc= +github.com/nats-io/nats-server/v2 v2.10.22 h1:Yt63BGu2c3DdMoBZNcR6pjGQwk/asrKU7VX846ibxDA= +github.com/nats-io/nats-server/v2 v2.10.22/go.mod h1:X/m1ye9NYansUXYFrbcDwUi/blHkrgHh2rgCJaakonk= github.com/nats-io/nats.go v1.37.0 h1:07rauXbVnnJvv1gfIyghFEo6lUcYRY0WXc3x7x0vUxE= github.com/nats-io/nats.go v1.37.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= @@ -266,10 +266,10 @@ github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= -github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= -github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= -github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= @@ -300,8 +300,8 @@ github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWN github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shirou/gopsutil/v4 v4.24.9 h1:KIV+/HaHD5ka5f570RZq+2SaeFsb/pq+fp2DGNWYoOI= -github.com/shirou/gopsutil/v4 v4.24.9/go.mod h1:3fkaHNeYsUFCGZ8+9vZVWtbyM1k2eRnlL+bWO8Bxa/Q= +github.com/shirou/gopsutil/v4 v4.24.10 h1:7VOzPtfw/5YDU+jLEoBwXwxJbQetULywoSV4RYY7HkM= +github.com/shirou/gopsutil/v4 v4.24.10/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8= github.com/shomali11/parallelizer v0.0.0-20220717173222-a6776fbf40a9/go.mod h1:QsLM53l8gzX0sQbOjVir85bzOUucuJEF8JgE39wD7w0= github.com/shomali11/util v0.0.0-20180607005212-e0f70fd665ff/go.mod h1:WWE2GJM9B5UpdOiwH2val10w/pvJ2cUUQOOA/4LgOng= github.com/shomali11/util v0.0.0-20220717175126-f0771b70947f h1:OM0LVaVycWC+/j5Qra7USyCg2sc+shg3KwygAA+pYvA= @@ -345,8 +345,8 @@ github.com/uptrace/bun v1.2.5 h1:gSprL5xiBCp+tzcZHgENzJpXnmQwRM/A6s4HnBF85mc= github.com/uptrace/bun v1.2.5/go.mod h1:vkQMS4NNs4VNZv92y53uBSHXRqYyJp4bGhMHgaNCQpY= github.com/uptrace/bun/dialect/pgdialect v1.2.5 h1:dWLUxpjTdglzfBks2x+U2WIi+nRVjuh7Z3DLYVFswJk= github.com/uptrace/bun/dialect/pgdialect v1.2.5/go.mod h1:stwnlE8/6x8cuQ2aXcZqwDK/d+6jxgO3iQewflJT6C4= -github.com/uptrace/bun/extra/bundebug v1.2.3 h1:2QBykz9/u4SkN9dnraImDcbrMk2fUhuq2gL6hkh9qSc= -github.com/uptrace/bun/extra/bundebug v1.2.3/go.mod h1:bihsYJxXxWZXwc1R3qALTHvp+npE0ElgaCvcjzyPPdw= +github.com/uptrace/bun/extra/bundebug v1.2.5 h1:DsI/gl4jvq5tQ84yPqnlRYIQ4U6AouTqxJ8Y5Oijfz4= +github.com/uptrace/bun/extra/bundebug v1.2.5/go.mod h1:JFeYvklf5p92ZILXx4siMe2tEVn5JLelww3IGcJb4yA= github.com/uptrace/bun/extra/bunotel v1.2.5 h1:kkuuTbrG9d5leYZuSBKhq2gtq346lIrxf98Mig2y128= github.com/uptrace/bun/extra/bunotel v1.2.5/go.mod h1:rCHLszRZwppWE9cGDodO2FCI1qCrLwDjONp38KD3bA8= github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 h1:H8wwQwTe5sL6x30z71lUgNiwBdeCHQjrphCfLwqIHGo= @@ -383,40 +383,40 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zitadel/oidc/v2 v2.12.2 h1:3kpckg4rurgw7w7aLJrq7yvRxb2pkNOtD08RH42vPEs= github.com/zitadel/oidc/v2 v2.12.2/go.mod h1:vhP26g1g4YVntcTi0amMYW3tJuid70nxqxf+kb6XKgg= -go.opentelemetry.io/contrib/instrumentation/host v0.56.0 h1:bLJ0U2SVly7aCVAv4pSJ62I0yy3GHPMbK+74AXSwC40= -go.opentelemetry.io/contrib/instrumentation/host v0.56.0/go.mod h1:7XvO8DvjdcoYDOQs/1n3AuadI/30eE2R+H/pQQuZVN0= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= -go.opentelemetry.io/contrib/instrumentation/runtime v0.56.0 h1:s7wHG+t8bEoH7ibWk1nk682h7EoWLJ5/8j+TSO3bX/o= -go.opentelemetry.io/contrib/instrumentation/runtime v0.56.0/go.mod h1:Q8Hsv3d9DwryfIl+ebj4mY81IYVRSPy4QfxroVZwqLo= -go.opentelemetry.io/contrib/propagators/b3 v1.31.0 h1:PQPXYscmwbCp76QDvO4hMngF2j8Bx/OTV86laEl8uqo= -go.opentelemetry.io/contrib/propagators/b3 v1.31.0/go.mod h1:jbqfV8wDdqSDrAYxVpXQnpM0XFMq2FtDesblJ7blOwQ= -go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= -go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0 h1:FZ6ei8GFW7kyPYdxJaV2rgI6M+4tvZzhYsQ2wgyVC08= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0/go.mod h1:MdEu/mC6j3D+tTEfvI15b5Ci2Fn7NneJ71YMoiS3tpI= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0 h1:ZsXq73BERAiNuuFXYqP4MR5hBrjXfMGSO+Cx7qoOZiM= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0/go.mod h1:hg1zaDMpyZJuUzjFxFsRYBoccE86tM9Uf4IqNMUxvrY= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 h1:K0XaT3DwHAcV4nKLzcQvwAgSyisUghWoY20I7huthMk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0/go.mod h1:B5Ki776z/MBnVha1Nzwp5arlzBbE3+1jk+pGmaP5HME= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 h1:FFeLy03iVTXP6ffeN2iXrxfGsZGCjVx0/4KlizjyBwU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0/go.mod h1:TMu73/k1CP8nBUpDLc71Wj/Kf7ZS9FK5b53VapRsP9o= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 h1:lUsI2TYsQw2r1IASwoROaCnjdj2cvC2+Jbxvk6nHnWU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0/go.mod h1:2HpZxxQurfGxJlJDblybejHB6RX6pmExPNe517hREw4= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.31.0 h1:HZgBIps9wH0RDrwjrmNa3DVbNRW60HEhdzqZFyAp3fI= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.31.0/go.mod h1:RDRhvt6TDG0eIXmonAx5bd9IcwpqCkziwkOClzWKwAQ= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.31.0 h1:UGZ1QwZWY67Z6BmckTU+9Rxn04m2bD3gD6Mk0OIOCPk= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.31.0/go.mod h1:fcwWuDuaObkkChiDlhEpSq9+X1C0omv+s5mBtToAQ64= +go.opentelemetry.io/contrib/instrumentation/host v0.57.0 h1:1gfzOyXEuCrrwCXF81LO3DQ4rll6YBKfAQHPl+03mik= +go.opentelemetry.io/contrib/instrumentation/host v0.57.0/go.mod h1:pHBt+1Rhz99VBX7AQVgwcKPf611zgD6pQy7VwBNMFmE= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94= +go.opentelemetry.io/contrib/instrumentation/runtime v0.57.0 h1:kJB5wMVorwre8QzEodzTAbzm9FOOah0zvG+V4abNlEE= +go.opentelemetry.io/contrib/instrumentation/runtime v0.57.0/go.mod h1:Nup4TgnOyEJWmVq9sf/ASH3ZJiAXwWHd5xZCHG7Sg9M= +go.opentelemetry.io/contrib/propagators/b3 v1.32.0 h1:MazJBz2Zf6HTN/nK/s3Ru1qme+VhWU5hm83QxEP+dvw= +go.opentelemetry.io/contrib/propagators/b3 v1.32.0/go.mod h1:B0s70QHYPrJwPOwD1o3V/R8vETNOG9N3qZf4LDYvA30= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 h1:t/Qur3vKSkUCcDVaSumWF2PKHt85pc7fRvFuoVT8qFU= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0/go.mod h1:Rl61tySSdcOJWoEgYZVtmnKdA0GeKrSqkHC1t+91CH8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 h1:9kV11HXBHZAvuPUZxmMWrH8hZn/6UnHX4K0mu36vNsU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0/go.mod h1:JyA0FHXe22E1NeNiHmVp7kFHglnexDQ7uRWDiiJ1hKQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 h1:SZmDnHcgp3zwlPBS2JX2urGYe/jBKEIT6ZedHRUyCz8= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0/go.mod h1:fdWW0HtZJ7+jNpTKUR0GpMEDP69nR8YBJQxNiVCE3jk= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s= go.opentelemetry.io/otel/log v0.7.0 h1:d1abJc0b1QQZADKvfe9JqqrfmPYQCz2tUSO+0XZmuV4= go.opentelemetry.io/otel/log v0.7.0/go.mod h1:2jf2z7uVfnzDNknKTO9G+ahcOAyWcp1fJmk/wJjULRo= -go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= -go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= -go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= -go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= -go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= -go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= -go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= -go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= +go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= +go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= +go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= +go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw= @@ -460,8 +460,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -483,8 +483,8 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -493,10 +493,10 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= -golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -508,10 +508,10 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38 h1:2oV8dfuIkM1Ti7DwXc0BJfnwr9csz4TDXI9EmiI+Rbw= -google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38/go.mod h1:vuAjtvlwkDKF6L1GQ0SokiRLCGFfeBUXWr/aFFkHACc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 h1:zciRKQ4kBpFgpfC5QQCVtnnNAcLIqweL7plyZRQHVpI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 h1:M0KvPgPmDZHPlbRbaNU1APr28TvwvvdUPlSv7PUvy8g= +google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:dguCy7UOdZhTvLzDyt15+rOrawrpM4q7DD9dQ1P11P4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 h1:XVhgTWWV3kGQlwJHR3upFWZeTsei6Oks1apkZSeonIE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= diff --git a/internal/README.md b/internal/README.md index 99da8b3f7..d71e81cf6 100644 --- a/internal/README.md +++ b/internal/README.md @@ -33,6 +33,7 @@ import "github.com/formancehq/ledger/internal" - [func \(e ErrInvalidLedgerName\) Error\(\) string](<#ErrInvalidLedgerName.Error>) - [func \(e ErrInvalidLedgerName\) Is\(err error\) bool](<#ErrInvalidLedgerName.Is>) - [type FeatureSet](<#FeatureSet>) + - [func \(f FeatureSet\) Match\(features FeatureSet\) bool](<#FeatureSet.Match>) - [func \(f FeatureSet\) SortedKeys\(\) \[\]string](<#FeatureSet.SortedKeys>) - [func \(f FeatureSet\) String\(\) string](<#FeatureSet.String>) - [func \(f FeatureSet\) With\(feature, value string\) FeatureSet](<#FeatureSet.With>) @@ -441,6 +442,15 @@ func (e ErrInvalidLedgerName) Is(err error) bool type FeatureSet map[string]string ``` + +### func \(FeatureSet\) Match + +```go +func (f FeatureSet) Match(features FeatureSet) bool +``` + + + ### func \(FeatureSet\) SortedKeys diff --git a/internal/ledger.go b/internal/ledger.go index 8f6adacd7..a66f5e996 100644 --- a/internal/ledger.go +++ b/internal/ledger.go @@ -176,6 +176,16 @@ func (f FeatureSet) String() string { return ret[1:] } +func (f FeatureSet) Match(features FeatureSet) bool { + for key, value := range features { + if f[key] != value { + return false + } + } + + return true +} + func shortenFeature(feature string) string { return strings.Join(Map(strings.Split(feature, "_"), func(from string) string { return from[:1] diff --git a/internal/storage/bucket/bucket.go b/internal/storage/bucket/bucket.go index 2b53f13da..fd6e5422a 100644 --- a/internal/storage/bucket/bucket.go +++ b/internal/storage/bucket/bucket.go @@ -20,8 +20,8 @@ type Bucket struct { db *bun.DB } -func (b *Bucket) Migrate(ctx context.Context, tracer trace.Tracer, minimalVersionReached chan struct{}) error { - return migrate(ctx, tracer, b.db, b.name, minimalVersionReached) +func (b *Bucket) Migrate(ctx context.Context, tracer trace.Tracer, minimalVersionReached chan struct{}, options ...migrations.Option) error { + return migrate(ctx, tracer, b.db, b.name, minimalVersionReached, options...) } func (b *Bucket) HasMinimalVersion(ctx context.Context) (bool, error) { @@ -40,15 +40,19 @@ func (b *Bucket) GetMigrationsInfo(ctx context.Context) ([]migrations.Info, erro func (b *Bucket) AddLedger(ctx context.Context, l ledger.Ledger, db bun.IDB) error { - tpl := template.Must(template.New("sql").Parse(addLedgerTpl)) - buf := bytes.NewBuffer(nil) - if err := tpl.Execute(buf, l); err != nil { - return fmt.Errorf("executing template: %w", err) - } + for _, setup := range ledgerSetups { + if l.Features.Match(setup.requireFeatures) { + tpl := template.Must(template.New("sql").Parse(setup.script)) + buf := bytes.NewBuffer(nil) + if err := tpl.Execute(buf, l); err != nil { + return fmt.Errorf("executing template: %w", err) + } - _, err := db.ExecContext(ctx, buf.String()) - if err != nil { - return fmt.Errorf("executing sql: %w", err) + _, err := db.ExecContext(ctx, buf.String()) + if err != nil { + return fmt.Errorf("executing sql: %w", err) + } + } } return nil @@ -61,140 +65,241 @@ func New(db *bun.DB, name string) *Bucket { } } -const addLedgerTpl = ` --- create a sequence for transactions by ledger instead of a sequence of the table as we want to have contiguous ids --- notes: we can still have "holes" on ids since a sql transaction can be reverted after a usage of the sequence -create sequence "{{.Bucket}}"."transaction_id_{{.ID}}" owned by "{{.Bucket}}".transactions.id; -select setval('"{{.Bucket}}"."transaction_id_{{.ID}}"', coalesce(( - select max(id) + 1 - from "{{.Bucket}}".transactions - where ledger = '{{ .Name }}' -), 1)::bigint, false); - --- create a sequence for logs by ledger instead of a sequence of the table as we want to have contiguous ids --- notes: we can still have "holes" on id since a sql transaction can be reverted after a usage of the sequence -create sequence "{{.Bucket}}"."log_id_{{.ID}}" owned by "{{.Bucket}}".logs.id; -select setval('"{{.Bucket}}"."log_id_{{.ID}}"', coalesce(( - select max(id) + 1 - from "{{.Bucket}}".logs - where ledger = '{{ .Name }}' -), 1)::bigint, false); - --- enable post commit effective volumes synchronously - -{{ if .HasFeature "MOVES_HISTORY_POST_COMMIT_EFFECTIVE_VOLUMES" "SYNC" }} -create index "pcev_{{.ID}}" on "{{.Bucket}}".moves (accounts_address, asset, effective_date desc) where ledger = '{{.Name}}'; - -create trigger "set_effective_volumes_{{.ID}}" -before insert -on "{{.Bucket}}"."moves" -for each row -when ( - new.ledger = '{{.Name}}' -) -execute procedure "{{.Bucket}}".set_effective_volumes(); - -create trigger "update_effective_volumes_{{.ID}}" -after insert -on "{{.Bucket}}"."moves" -for each row -when ( - new.ledger = '{{.Name}}' -) -execute procedure "{{.Bucket}}".update_effective_volumes(); -{{ end }} - --- logs hash - -{{ if .HasFeature "HASH_LOGS" "SYNC" }} -create trigger "set_log_hash_{{.ID}}" -before insert -on "{{.Bucket}}"."logs" -for each row -when ( - new.ledger = '{{.Name}}' -) -execute procedure "{{.Bucket}}".set_log_hash(); -{{ end }} - -{{ if .HasFeature "ACCOUNT_METADATA_HISTORY" "SYNC" }} -create trigger "update_account_metadata_history_{{.ID}}" -after update -on "{{.Bucket}}"."accounts" -for each row -when ( - new.ledger = '{{.Name}}' -) -execute procedure "{{.Bucket}}".update_account_metadata_history(); - -create trigger "insert_account_metadata_history_{{.ID}}" -after insert -on "{{.Bucket}}"."accounts" -for each row -when ( - new.ledger = '{{.Name}}' -) -execute procedure "{{.Bucket}}".insert_account_metadata_history(); -{{ end }} - -{{ if .HasFeature "TRANSACTION_METADATA_HISTORY" "SYNC" }} -create trigger "update_transaction_metadata_history_{{.ID}}" -after update -on "{{.Bucket}}"."transactions" -for each row -when ( - new.ledger = '{{.Name}}' -) -execute procedure "{{.Bucket}}".update_transaction_metadata_history(); - -create trigger "insert_transaction_metadata_history_{{.ID}}" -after insert -on "{{.Bucket}}"."transactions" -for each row -when ( - new.ledger = '{{.Name}}' -) -execute procedure "{{.Bucket}}".insert_transaction_metadata_history(); -{{ end }} - -{{ if .HasFeature "INDEX_TRANSACTION_ACCOUNTS" "ON" }} -create index "transactions_sources_{{.ID}}" on "{{.Bucket}}".transactions using gin (sources jsonb_path_ops) where ledger = '{{.Name}}'; -create index "transactions_destinations_{{.ID}}" on "{{.Bucket}}".transactions using gin (destinations jsonb_path_ops) where ledger = '{{.Name}}'; -create trigger "transaction_set_addresses_{{.ID}}" - before insert - on "{{.Bucket}}"."transactions" - for each row - when ( - new.ledger = '{{.Name}}' - ) -execute procedure "{{.Bucket}}".set_transaction_addresses(); -{{ end }} - -{{ if .HasFeature "INDEX_ADDRESS_SEGMENTS" "ON" }} -create index "accounts_address_array_{{.ID}}" on "{{.Bucket}}".accounts using gin (address_array jsonb_ops) where ledger = '{{.Name}}'; -create index "accounts_address_array_length_{{.ID}}" on "{{.Bucket}}".accounts (jsonb_array_length(address_array)) where ledger = '{{.Name}}'; - -create trigger "accounts_set_address_array_{{.ID}}" - before insert - on "{{.Bucket}}"."accounts" - for each row - when ( - new.ledger = '{{.Name}}' - ) -execute procedure "{{.Bucket}}".set_address_array_for_account(); - -{{ if .HasFeature "INDEX_TRANSACTION_ACCOUNTS" "ON" }} -create index "transactions_sources_arrays_{{.ID}}" on "{{.Bucket}}".transactions using gin (sources_arrays jsonb_path_ops) where ledger = '{{.Name}}'; -create index "transactions_destinations_arrays_{{.ID}}" on "{{.Bucket}}".transactions using gin (destinations_arrays jsonb_path_ops) where ledger = '{{.Name}}'; +type ledgerSetup struct { + requireFeatures ledger.FeatureSet + script string +} -create trigger "transaction_set_addresses_segments_{{.ID}}" - before insert - on "{{.Bucket}}"."transactions" - for each row - when ( - new.ledger = '{{.Name}}' - ) -execute procedure "{{.Bucket}}".set_transaction_addresses_segments(); -{{ end }} -{{ end }} -` +var ledgerSetups = []ledgerSetup{ + { + script: ` + -- create a sequence for transactions by ledger instead of a sequence of the table as we want to have contiguous ids + -- notes: we can still have "holes" on ids since a sql transaction can be reverted after a usage of the sequence + create sequence "{{.Bucket}}"."transaction_id_{{.ID}}" owned by "{{.Bucket}}".transactions.id; + select setval('"{{.Bucket}}"."transaction_id_{{.ID}}"', coalesce(( + select max(id) + 1 + from "{{.Bucket}}".transactions + where ledger = '{{ .Name }}' + ), 1)::bigint, false); + `, + }, + { + script: ` + -- create a sequence for logs by ledger instead of a sequence of the table as we want to have contiguous ids + -- notes: we can still have "holes" on id since a sql transaction can be reverted after a usage of the sequence + create sequence "{{.Bucket}}"."log_id_{{.ID}}" owned by "{{.Bucket}}".logs.id; + select setval('"{{.Bucket}}"."log_id_{{.ID}}"', coalesce(( + select max(id) + 1 + from "{{.Bucket}}".logs + where ledger = '{{ .Name }}' + ), 1)::bigint, false); + `, + }, + { + requireFeatures: ledger.FeatureSet{ + ledger.FeatureMovesHistoryPostCommitEffectiveVolumes: "SYNC", + }, + script: `create index "pcev_{{.ID}}" on "{{.Bucket}}".moves (accounts_address, asset, effective_date desc) where ledger = '{{.Name}}';`, + }, + { + requireFeatures: ledger.FeatureSet{ + ledger.FeatureMovesHistoryPostCommitEffectiveVolumes: "SYNC", + }, + script: ` + create trigger "set_effective_volumes_{{.ID}}" + before insert + on "{{.Bucket}}"."moves" + for each row + when ( + new.ledger = '{{.Name}}' + ) + execute procedure "{{.Bucket}}".set_effective_volumes(); + `, + }, + { + requireFeatures: ledger.FeatureSet{ + ledger.FeatureMovesHistoryPostCommitEffectiveVolumes: "SYNC", + }, + script: ` + create trigger "update_effective_volumes_{{.ID}}" + after insert + on "{{.Bucket}}"."moves" + for each row + when ( + new.ledger = '{{.Name}}' + ) + execute procedure "{{.Bucket}}".update_effective_volumes(); + `, + }, + { + requireFeatures: ledger.FeatureSet{ + ledger.FeatureHashLogs: "SYNC", + }, + script: ` + create trigger "set_log_hash_{{.ID}}" + before insert + on "{{.Bucket}}"."logs" + for each row + when ( + new.ledger = '{{.Name}}' + ) + execute procedure "{{.Bucket}}".set_log_hash(); + `, + }, + { + requireFeatures: ledger.FeatureSet{ + ledger.FeatureAccountMetadataHistory: "SYNC", + }, + script: ` + create trigger "update_account_metadata_history_{{.ID}}" + after update + on "{{.Bucket}}"."accounts" + for each row + when ( + new.ledger = '{{.Name}}' + ) + execute procedure "{{.Bucket}}".update_account_metadata_history(); + `, + }, + { + requireFeatures: ledger.FeatureSet{ + ledger.FeatureAccountMetadataHistory: "SYNC", + }, + script: ` + create trigger "insert_account_metadata_history_{{.ID}}" + after insert + on "{{.Bucket}}"."accounts" + for each row + when ( + new.ledger = '{{.Name}}' + ) + execute procedure "{{.Bucket}}".insert_account_metadata_history(); + `, + }, + { + requireFeatures: ledger.FeatureSet{ + ledger.FeatureTransactionMetadataHistory: "SYNC", + }, + script: ` + create trigger "update_transaction_metadata_history_{{.ID}}" + after update + on "{{.Bucket}}"."transactions" + for each row + when ( + new.ledger = '{{.Name}}' + ) + execute procedure "{{.Bucket}}".update_transaction_metadata_history(); + `, + }, + { + requireFeatures: ledger.FeatureSet{ + ledger.FeatureTransactionMetadataHistory: "SYNC", + }, + script: ` + create trigger "insert_transaction_metadata_history_{{.ID}}" + after insert + on "{{.Bucket}}"."transactions" + for each row + when ( + new.ledger = '{{.Name}}' + ) + execute procedure "{{.Bucket}}".insert_transaction_metadata_history(); + `, + }, + { + requireFeatures: ledger.FeatureSet{ + ledger.FeatureIndexTransactionAccounts: "SYNC", + }, + script: ` + create index "transactions_sources_{{.ID}}" on "{{.Bucket}}".transactions using gin (sources jsonb_path_ops) where ledger = '{{.Name}}'; + `, + }, + { + requireFeatures: ledger.FeatureSet{ + ledger.FeatureIndexTransactionAccounts: "ON", + }, + script: ` + create index "transactions_destinations_{{.ID}}" on "{{.Bucket}}".transactions using gin (destinations jsonb_path_ops) where ledger = '{{.Name}}'; + `, + }, + { + requireFeatures: ledger.FeatureSet{ + ledger.FeatureIndexTransactionAccounts: "ON", + }, + script: ` + create trigger "transaction_set_addresses_{{.ID}}" + before insert + on "{{.Bucket}}"."transactions" + for each row + when ( + new.ledger = '{{.Name}}' + ) + execute procedure "{{.Bucket}}".set_transaction_addresses(); + `, + }, + { + requireFeatures: ledger.FeatureSet{ + ledger.FeatureIndexAddressSegments: "ON", + }, + script: ` + create index "accounts_address_array_{{.ID}}" on "{{.Bucket}}".accounts using gin (address_array jsonb_ops) where ledger = '{{.Name}}'; + `, + }, + { + requireFeatures: ledger.FeatureSet{ + ledger.FeatureIndexAddressSegments: "ON", + }, + script: ` + create index "accounts_address_array_length_{{.ID}}" on "{{.Bucket}}".accounts (jsonb_array_length(address_array)) where ledger = '{{.Name}}'; + `, + }, + { + requireFeatures: ledger.FeatureSet{ + ledger.FeatureIndexAddressSegments: "ON", + }, + script: ` + create trigger "accounts_set_address_array_{{.ID}}" + before insert + on "{{.Bucket}}"."accounts" + for each row + when ( + new.ledger = '{{.Name}}' + ) + execute procedure "{{.Bucket}}".set_address_array_for_account(); + `, + }, + { + requireFeatures: ledger.FeatureSet{ + ledger.FeatureIndexAddressSegments: "ON", + ledger.FeatureIndexTransactionAccounts: "ON", + }, + script: ` + create index "transactions_sources_arrays_{{.ID}}" on "{{.Bucket}}".transactions using gin (sources_arrays jsonb_path_ops) where ledger = '{{.Name}}'; + `, + }, + { + requireFeatures: ledger.FeatureSet{ + ledger.FeatureIndexAddressSegments: "ON", + ledger.FeatureIndexTransactionAccounts: "ON", + }, + script: ` + create index "transactions_destinations_arrays_{{.ID}}" on "{{.Bucket}}".transactions using gin (destinations_arrays jsonb_path_ops) where ledger = '{{.Name}}'; + `, + }, + { + requireFeatures: ledger.FeatureSet{ + ledger.FeatureIndexAddressSegments: "ON", + ledger.FeatureIndexTransactionAccounts: "ON", + }, + script: ` + create trigger "transaction_set_addresses_segments_{{.ID}}" + before insert + on "{{.Bucket}}"."transactions" + for each row + when ( + new.ledger = '{{.Name}}' + ) + execute procedure "{{.Bucket}}".set_transaction_addresses_segments(); + `, + }, +} \ No newline at end of file diff --git a/internal/storage/bucket/migrations.go b/internal/storage/bucket/migrations.go index a13197ad0..32e02802a 100644 --- a/internal/storage/bucket/migrations.go +++ b/internal/storage/bucket/migrations.go @@ -12,8 +12,9 @@ import ( //go:embed migrations var MigrationsFS embed.FS -func GetMigrator(db *bun.DB, name string) *migrations.Migrator { - migrator := migrations.NewMigrator(db, migrations.WithSchema(name)) +func GetMigrator(db *bun.DB, name string, options ...migrations.Option) *migrations.Migrator { + options = append(options, migrations.WithSchema(name)) + migrator := migrations.NewMigrator(db, options...) migrations, err := migrations.CollectMigrations(MigrationsFS, name) if err != nil { panic(err) @@ -23,11 +24,11 @@ func GetMigrator(db *bun.DB, name string) *migrations.Migrator { return migrator } -func migrate(ctx context.Context, tracer trace.Tracer, db *bun.DB, name string, minimalVersionReached chan struct{}) error { +func migrate(ctx context.Context, tracer trace.Tracer, db *bun.DB, name string, minimalVersionReached chan struct{}, options ...migrations.Option) error { ctx, span := tracer.Start(ctx, "Migrate bucket") defer span.End() - migrator := GetMigrator(db, name) + migrator := GetMigrator(db, name, options...) version, err := migrator.GetLastVersion(ctx) if err != nil { if !errors.Is(err, migrations.ErrMissingVersionTable) { diff --git a/internal/storage/driver/driver.go b/internal/storage/driver/driver.go index 16d94b0cf..1e719ce63 100644 --- a/internal/storage/driver/driver.go +++ b/internal/storage/driver/driver.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "github.com/formancehq/go-libs/v2/metadata" + "github.com/formancehq/go-libs/v2/migrations" "github.com/formancehq/go-libs/v2/platform/postgres" systemcontroller "github.com/formancehq/ledger/internal/controller/system" "go.opentelemetry.io/otel/metric" @@ -12,6 +13,7 @@ import ( "go.opentelemetry.io/otel/trace" nooptracer "go.opentelemetry.io/otel/trace/noop" "golang.org/x/sync/errgroup" + "time" ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" @@ -29,9 +31,10 @@ const ( ) type Driver struct { - db *bun.DB - tracer trace.Tracer - meter metric.Meter + db *bun.DB + tracer trace.Tracer + meter metric.Meter + migratorLockRetryInterval time.Duration } func (d *Driver) CreateLedger(ctx context.Context, l *ledger.Ledger) (*ledgerstore.Store, error) { @@ -41,7 +44,12 @@ func (d *Driver) CreateLedger(ctx context.Context, l *ledger.Ledger) (*ledgersto } b := bucket.New(d.db, l.Bucket) - if err := b.Migrate(ctx, d.tracer, make(chan struct{})); err != nil { + if err := b.Migrate( + ctx, + d.tracer, + make(chan struct{}), + migrations.WithLockRetryInterval(d.migratorLockRetryInterval), + ); err != nil { return nil, fmt.Errorf("migrating bucket: %w", err) } @@ -95,7 +103,7 @@ func (d *Driver) Initialize(ctx context.Context) error { return fmt.Errorf("detecting rollbacks: %w", err) } - err = Migrate(ctx, d.db) + err = Migrate(ctx, d.db, migrations.WithLockRetryInterval(d.migratorLockRetryInterval)) if err != nil { constraintsFailed := postgres.ErrConstraintsFailed{} if errors.As(err, &constraintsFailed) && @@ -189,10 +197,12 @@ func (d *Driver) GetLedger(ctx context.Context, name string) (*ledger.Ledger, er } func (d *Driver) UpgradeBucket(ctx context.Context, name string) error { - if err := bucket.New(d.db, name).Migrate(ctx, d.tracer, make(chan struct{})); err != nil { - return fmt.Errorf("migrating bucket '%s': %w", name, err) - } - return nil + return bucket.New(d.db, name).Migrate( + ctx, + d.tracer, + make(chan struct{}), + migrations.WithLockRetryInterval(d.migratorLockRetryInterval), + ) } func (d *Driver) UpgradeAllBuckets(ctx context.Context, minimalVersionReached chan struct{}) error { @@ -226,7 +236,12 @@ func (d *Driver) UpgradeAllBuckets(ctx context.Context, minimalVersionReached ch }() logging.FromContext(ctx).Infof("Upgrading bucket '%s'", bucketName) - if err := b.Migrate(ctx, d.tracer, minimalVersionReached); err != nil { + if err := b.Migrate( + ctx, + d.tracer, + minimalVersionReached, + migrations.WithLockRetryInterval(d.migratorLockRetryInterval), + ); err != nil { return err } logging.FromContext(ctx).Infof("Bucket '%s' up to date", bucketName) @@ -276,6 +291,12 @@ func WithTracer(tracer trace.Tracer) Option { } } +func WithMigratorLockRetryInterval(interval time.Duration) Option { + return func(d *Driver) { + d.migratorLockRetryInterval = interval + } +} + var defaultOptions = []Option{ WithMeter(noopmetrics.Meter{}), WithTracer(nooptracer.Tracer{}), diff --git a/internal/storage/driver/driver_test.go b/internal/storage/driver/driver_test.go index 958c7db8a..cc45c6425 100644 --- a/internal/storage/driver/driver_test.go +++ b/internal/storage/driver/driver_test.go @@ -3,21 +3,24 @@ package driver_test import ( + "context" "fmt" + "github.com/formancehq/go-libs/v2/bun/bunconnect" + "github.com/formancehq/go-libs/v2/bun/bundebug" "github.com/formancehq/go-libs/v2/bun/bunpaginate" "github.com/formancehq/go-libs/v2/metadata" "github.com/formancehq/go-libs/v2/pointer" + "github.com/formancehq/go-libs/v2/testing/docker" ledger "github.com/formancehq/ledger/internal" ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" "github.com/formancehq/ledger/internal/storage/driver" "github.com/google/uuid" + "github.com/uptrace/bun" + "golang.org/x/sync/errgroup" "os" + "slices" "testing" - - "github.com/formancehq/go-libs/v2/bun/bunconnect" - "github.com/formancehq/go-libs/v2/bun/bundebug" - "github.com/formancehq/go-libs/v2/testing/docker" - "github.com/uptrace/bun" + "time" "github.com/formancehq/go-libs/v2/logging" "github.com/stretchr/testify/require" @@ -41,17 +44,54 @@ func TestUpgradeAllLedgers(t *testing.T) { } func TestLedgersCreate(t *testing.T) { + t.Parallel() + ctx := logging.TestingContext() - driver := newStorageDriver(t) + driver := newStorageDriver(t, driver.WithMigratorLockRetryInterval(100*time.Millisecond)) - l := ledger.MustNewWithDefault("foo") - _, err := driver.CreateLedger(ctx, &l) - require.NoError(t, err) - require.Equal(t, 1, l.ID) - require.NotEmpty(t, l.AddedAt) + const count = 30 + grp, ctx := errgroup.WithContext(ctx) + createdLedgersChan := make(chan ledger.Ledger, count) + + for i := range count { + grp.Go(func() error { + l := ledger.MustNewWithDefault(fmt.Sprintf("ledger%d", i)) + + ctx, cancel := context.WithDeadline(ctx, time.Now().Add(40*time.Second)) + defer cancel() + + _, err := driver.CreateLedger(ctx, &l) + if err != nil { + return err + } + createdLedgersChan <- l + + return nil + }) + } + + require.NoError(t, grp.Wait()) + + close(createdLedgersChan) + + createdLedgers := make([]ledger.Ledger, 0) + for createdLedger := range createdLedgersChan { + createdLedgers = append(createdLedgers, createdLedger) + } + + slices.SortStableFunc(createdLedgers, func(a, b ledger.Ledger) int { + return a.ID - b.ID + }) + + for i, createdLedger := range createdLedgers { + require.Equal(t, i+1, createdLedger.ID) + require.NotEmpty(t, createdLedger.AddedAt) + } } func TestLedgersList(t *testing.T) { + t.Parallel() + ctx := logging.TestingContext() driver := newStorageDriver(t) @@ -87,6 +127,8 @@ func TestLedgersList(t *testing.T) { } func TestLedgerUpdateMetadata(t *testing.T) { + t.Parallel() + ctx := logging.TestingContext() storageDriver := newStorageDriver(t) @@ -106,6 +148,8 @@ func TestLedgerUpdateMetadata(t *testing.T) { } func TestLedgerDeleteMetadata(t *testing.T) { + t.Parallel() + ctx := logging.TestingContext() driver := newStorageDriver(t) @@ -124,7 +168,7 @@ func TestLedgerDeleteMetadata(t *testing.T) { require.Equal(t, metadata.Metadata{}, ledgerFromDB.Metadata) } -func newStorageDriver(t docker.T) *driver.Driver { +func newStorageDriver(t docker.T, driverOptions ...driver.Option) *driver.Driver { t.Helper() ctx := logging.TestingContext() @@ -137,7 +181,7 @@ func newStorageDriver(t docker.T) *driver.Driver { db, err := bunconnect.OpenSQLDB(ctx, pgDatabase.ConnectionOptions(), hooks...) require.NoError(t, err) - d := driver.New(db) + d := driver.New(db, driverOptions...) require.NoError(t, d.Initialize(logging.TestingContext())) diff --git a/internal/storage/driver/migrations.go b/internal/storage/driver/migrations.go index 6d36c5693..890f8ee8d 100644 --- a/internal/storage/driver/migrations.go +++ b/internal/storage/driver/migrations.go @@ -13,7 +13,7 @@ import ( "github.com/uptrace/bun" ) -func GetMigrator(db *bun.DB) *migrations.Migrator { +func GetMigrator(db *bun.DB, options ...migrations.Option) *migrations.Migrator { // configuration table has been removed, we keep the model to keep migrations consistent but the table is not used anymore. type configuration struct { @@ -24,7 +24,9 @@ func GetMigrator(db *bun.DB) *migrations.Migrator { AddedAt time.Time `bun:"addedAt,type:timestamp"` } - migrator := migrations.NewMigrator(db, migrations.WithSchema(SchemaSystem)) + options = append(options, migrations.WithSchema(SchemaSystem)) + + migrator := migrations.NewMigrator(db, options...) migrator.RegisterMigrations( migrations.Migration{ Name: "Init schema", @@ -207,8 +209,8 @@ func GetMigrator(db *bun.DB) *migrations.Migrator { return migrator } -func Migrate(ctx context.Context, db *bun.DB) error { - return GetMigrator(db).Up(ctx) +func Migrate(ctx context.Context, db *bun.DB, options ...migrations.Option) error { + return GetMigrator(db, options...).Up(ctx) } func detectDowngrades(migrator *migrations.Migrator, ctx context.Context) error { diff --git a/internal/storage/ledger/accounts.go b/internal/storage/ledger/accounts.go index 16f30ab66..f95e1837d 100644 --- a/internal/storage/ledger/accounts.go +++ b/internal/storage/ledger/accounts.go @@ -50,11 +50,13 @@ func (s *Store) selectBalance(date *time.Time) *bun.SelectQuery { return s.db.NewSelect(). ModelTableExpr("(?) moves", sortedMoves). + Where("ledger = ?", s.ledger.Name). ColumnExpr("accounts_address, asset, balance") } return s.db.NewSelect(). ModelTableExpr(s.GetPrefixedRelationName("accounts_volumes")). + Where("ledger = ?", s.ledger.Name). ColumnExpr("input - output as balance") } diff --git a/internal/storage/ledger/balances.go b/internal/storage/ledger/balances.go index 566e45d76..cd9f97a4e 100644 --- a/internal/storage/ledger/balances.go +++ b/internal/storage/ledger/balances.go @@ -102,7 +102,9 @@ func (s *Store) selectAccountWithAssetAndVolumes(date *time.Time, useInsertionDa selectAccountsWithVolumes = selectAccountsWithVolumes. Join( `join (?) accounts on accounts.address = accounts_volumes.accounts_address`, - s.db.NewSelect().ModelTableExpr(s.GetPrefixedRelationName("accounts")), + s.db.NewSelect(). + ModelTableExpr(s.GetPrefixedRelationName("accounts")). + Where("ledger = ?", s.ledger.Name), ) } } @@ -112,7 +114,7 @@ func (s *Store) selectAccountWithAssetAndVolumes(date *time.Time, useInsertionDa TableExpr( "(?) accounts", selectAccountsWithVolumes. - Join("join "+s.GetPrefixedRelationName("accounts")+" accounts on accounts.address = accounts_volumes.accounts_address"), + Join("join "+s.GetPrefixedRelationName("accounts")+" accounts on accounts.address = accounts_volumes.accounts_address and ledger = ?", s.ledger.Name), ). ColumnExpr("address, asset, volumes, metadata"). ColumnExpr("accounts.address_array as accounts_address_array") @@ -198,8 +200,8 @@ func (s *Store) GetBalances(ctx context.Context, query ledgercontroller.BalanceQ args := make([]any, 0) for account, assets := range query { for _, asset := range assets { - conditions = append(conditions, "accounts_address = ? and asset = ?") - args = append(args, account, asset) + conditions = append(conditions, "ledger = ? and accounts_address = ? and asset = ?") + args = append(args, s.ledger.Name, account, asset) } } diff --git a/internal/storage/ledger/balances_test.go b/internal/storage/ledger/balances_test.go index 3096e8952..24a6c9bbc 100644 --- a/internal/storage/ledger/balances_test.go +++ b/internal/storage/ledger/balances_test.go @@ -106,6 +106,7 @@ func TestBalancesGet(t *testing.T) { count, err := store.GetDB().NewSelect(). ModelTableExpr(store.GetPrefixedRelationName("accounts_volumes")). + Where("ledger = ?", store.GetLedger().Name). Count(ctx) require.NoError(t, err) require.Equal(t, 1, count) @@ -124,6 +125,7 @@ func TestBalancesGet(t *testing.T) { count, err = store.GetDB().NewSelect(). ModelTableExpr(store.GetPrefixedRelationName("accounts_volumes")). + Where("ledger = ?", store.GetLedger().Name). Count(ctx) require.NoError(t, err) require.Equal(t, 2, count) @@ -171,7 +173,7 @@ func TestBalancesGet(t *testing.T) { volumes := &ledger.AccountsVolumes{} err = store.GetDB().NewSelect(). ModelTableExpr(store.GetPrefixedRelationName("accounts_volumes")). - Where("accounts_address = ?", "bank"). + Where("accounts_address = ? and ledger = ?", "bank", store.GetLedger().Name). Scan(ctx, volumes) require.NoError(t, err) diff --git a/internal/storage/ledger/legacy/main_test.go b/internal/storage/ledger/legacy/main_test.go index 52d294471..4b614743e 100644 --- a/internal/storage/ledger/legacy/main_test.go +++ b/internal/storage/ledger/legacy/main_test.go @@ -67,14 +67,13 @@ func newLedgerStore(t T) *testStore { require.NoError(t, systemstore.Migrate(ctx, db)) l := ledger.MustNewWithDefault(ledgerName) - l.Bucket = ledgerName - b := bucket.New(db, ledgerName) + b := bucket.New(db, ledger.DefaultBucket) require.NoError(t, b.Migrate(ctx, noop.Tracer{}, make(chan struct{}))) require.NoError(t, b.AddLedger(ctx, l, db)) return &testStore{ - Store: legacy.New(db, l.Name, l.Name), + Store: legacy.New(db, ledger.DefaultBucket, l.Name), newStore: ledgerstore.New(db, b, l), } } diff --git a/internal/storage/ledger/main_test.go b/internal/storage/ledger/main_test.go index 21b923a44..8b72798d2 100644 --- a/internal/storage/ledger/main_test.go +++ b/internal/storage/ledger/main_test.go @@ -28,8 +28,8 @@ import ( ) var ( - srv = NewDeferred[*pgtesting.PostgresServer]() - bunDB = NewDeferred[*bun.DB]() + srv = NewDeferred[*pgtesting.PostgresServer]() + bunDB = NewDeferred[*bun.DB]() ) func TestMain(m *testing.M) { @@ -94,7 +94,6 @@ func newLedgerStore(t T) *ledgerstore.Store { ctx := logging.TestingContext() l := ledger.MustNewWithDefault(ledgerName) - l.Bucket = ledgerName store, err := driver.CreateLedger(ctx, &l) require.NoError(t, err) diff --git a/internal/storage/ledger/transactions.go b/internal/storage/ledger/transactions.go index a8b522e7d..03aefb05c 100644 --- a/internal/storage/ledger/transactions.go +++ b/internal/storage/ledger/transactions.go @@ -141,6 +141,7 @@ func (s *Store) selectTransactions(date *time.Time, expandVolumes, expandEffecti DistinctOn("transactions_id, accounts_address, asset"). ModelTableExpr(s.GetPrefixedRelationName("moves")). Column("transactions_id", "accounts_address", "asset"). + Where("ledger = ?", s.ledger.Name). ColumnExpr(`first_value(moves.post_commit_effective_volumes) over (partition by (transactions_id, accounts_address, asset) order by seq desc) as post_commit_effective_volumes`), ). Column("transactions_id"). diff --git a/internal/storage/ledger/transactions_test.go b/internal/storage/ledger/transactions_test.go index 6bb0312e3..9b22eeb24 100644 --- a/internal/storage/ledger/transactions_test.go +++ b/internal/storage/ledger/transactions_test.go @@ -711,7 +711,8 @@ func TestTransactionsInsert(t *testing.T) { err = store.GetDB(). NewSelect(). Model(&m). - ModelTableExpr(store.GetPrefixedRelationName("transactions") + " as model"). + ModelTableExpr(store.GetPrefixedRelationName("transactions")+" as model"). + Where("ledger = ?", store.GetLedger().Name). Scan(ctx) require.NoError(t, err) require.Equal(t, Model{ diff --git a/internal/storage/ledger/volumes.go b/internal/storage/ledger/volumes.go index 53db4e14d..a7a259aa0 100644 --- a/internal/storage/ledger/volumes.go +++ b/internal/storage/ledger/volumes.go @@ -113,6 +113,7 @@ func (s *Store) selectVolumes(oot, pit *time.Time, useInsertionDate bool, groupL ColumnExpr("sum(case when is_source then amount else 0 end) as output"). ColumnExpr("sum(case when not is_source then amount else -amount end) as balance"). ModelTableExpr(s.GetPrefixedRelationName("moves")). + Where("ledger = ?", s.ledger.Name). GroupExpr("accounts_address, asset") dateFilterColumn := "effective_date" diff --git a/test/rolling-upgrades/go.mod b/test/rolling-upgrades/go.mod index 588ed3fda..d2c0d9237 100644 --- a/test/rolling-upgrades/go.mod +++ b/test/rolling-upgrades/go.mod @@ -9,7 +9,7 @@ replace github.com/formancehq/ledger/pkg/client => ../../pkg/client replace github.com/formancehq/ledger => ../.. require ( - github.com/formancehq/go-libs/v2 v2.0.1-0.20241107141636-5509ff77294e + github.com/formancehq/go-libs/v2 v2.0.1-0.20241114125605-4a3e447246a9 github.com/formancehq/ledger v0.0.0-00010101000000-000000000000 github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.12.0 github.com/pulumi/pulumi/sdk/v3 v3.117.0 @@ -20,7 +20,7 @@ require ( dario.cat/mergo v1.0.1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.0.0 // indirect - github.com/ThreeDotsLabs/watermill v1.3.7 // indirect + github.com/ThreeDotsLabs/watermill v1.4.1 // indirect github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect github.com/agext/levenshtein v1.2.3 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect @@ -108,21 +108,21 @@ require ( github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/zclconf/go-cty v1.13.2 // indirect - go.opentelemetry.io/otel v1.31.0 // indirect + go.opentelemetry.io/otel v1.32.0 // indirect go.opentelemetry.io/otel/log v0.7.0 // indirect - go.opentelemetry.io/otel/metric v1.31.0 // indirect - go.opentelemetry.io/otel/trace v1.31.0 // indirect + go.opentelemetry.io/otel/metric v1.32.0 // indirect + go.opentelemetry.io/otel/trace v1.32.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.28.0 // indirect golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect golang.org/x/net v0.30.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.26.0 // indirect + golang.org/x/sync v0.9.0 // indirect + golang.org/x/sys v0.27.0 // indirect golang.org/x/term v0.25.0 // indirect - golang.org/x/text v0.19.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect + golang.org/x/text v0.20.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 // indirect google.golang.org/grpc v1.67.1 // indirect google.golang.org/protobuf v1.35.1 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect diff --git a/test/rolling-upgrades/go.sum b/test/rolling-upgrades/go.sum index 6d146abd6..23a56cbfa 100644 --- a/test/rolling-upgrades/go.sum +++ b/test/rolling-upgrades/go.sum @@ -13,8 +13,8 @@ github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEV github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= -github.com/ThreeDotsLabs/watermill v1.3.7 h1:NV0PSTmuACVEOV4dMxRnmGXrmbz8U83LENOvpHekN7o= -github.com/ThreeDotsLabs/watermill v1.3.7/go.mod h1:lBnrLbxOjeMRgcJbv+UiZr8Ylz8RkJ4m6i/VN/Nk+to= +github.com/ThreeDotsLabs/watermill v1.4.1 h1:gjP6yZH+otMPjV0KsV07pl9TeMm9UQV/gqiuiuG5Drs= +github.com/ThreeDotsLabs/watermill v1.4.1/go.mod h1:lBnrLbxOjeMRgcJbv+UiZr8Ylz8RkJ4m6i/VN/Nk+to= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= @@ -27,32 +27,32 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aws/aws-sdk-go-v2 v1.32.3 h1:T0dRlFBKcdaUPGNtkBSwHZxrtis8CQU17UpNBZYd0wk= -github.com/aws/aws-sdk-go-v2 v1.32.3/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo= -github.com/aws/aws-sdk-go-v2/config v1.28.1 h1:oxIvOUXy8x0U3fR//0eq+RdCKimWI900+SV+10xsCBw= -github.com/aws/aws-sdk-go-v2/config v1.28.1/go.mod h1:bRQcttQJiARbd5JZxw6wG0yIK3eLeSCPdg6uqmmlIiI= -github.com/aws/aws-sdk-go-v2/credentials v1.17.42 h1:sBP0RPjBU4neGpIYyx8mkU2QqLPl5u9cmdTWVzIpHkM= -github.com/aws/aws-sdk-go-v2/credentials v1.17.42/go.mod h1:FwZBfU530dJ26rv9saAbxa9Ej3eF/AK0OAY86k13n4M= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.18 h1:68jFVtt3NulEzojFesM/WVarlFpCaXLKaBxDpzkQ9OQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.18/go.mod h1:Fjnn5jQVIo6VyedMc0/EhPpfNlPl7dHV916O6B+49aE= -github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.22 h1:n89ziXnsp3dyOlodim8OHv0edSu47H7i75UYxDz1YVQ= -github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.22/go.mod h1:7uC80VxwPjAykLSIzkyTgZ+LjFDil+OVndzd8wGMOYY= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.22 h1:Jw50LwEkVjuVzE1NzkhNKkBf9cRN7MtE1F/b2cOKTUM= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.22/go.mod h1:Y/SmAyPcOTmpeVaWSzSKiILfXTVJwrGmYZhcRbhWuEY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.22 h1:981MHwBaRZM7+9QSR6XamDzF/o7ouUGxFzr+nVSIhrs= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.22/go.mod h1:1RA1+aBEfn+CAB/Mh0MB6LsdCYCnjZm7tKXtnk499ZQ= +github.com/aws/aws-sdk-go-v2 v1.32.4 h1:S13INUiTxgrPueTmrm5DZ+MiAo99zYzHEFh1UNkOxNE= +github.com/aws/aws-sdk-go-v2 v1.32.4/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo= +github.com/aws/aws-sdk-go-v2/config v1.28.3 h1:kL5uAptPcPKaJ4q0sDUjUIdueO18Q7JDzl64GpVwdOM= +github.com/aws/aws-sdk-go-v2/config v1.28.3/go.mod h1:SPEn1KA8YbgQnwiJ/OISU4fz7+F6Fe309Jf0QTsRCl4= +github.com/aws/aws-sdk-go-v2/credentials v1.17.44 h1:qqfs5kulLUHUEXlHEZXLJkgGoF3kkUeFUTVA585cFpU= +github.com/aws/aws-sdk-go-v2/credentials v1.17.44/go.mod h1:0Lm2YJ8etJdEdw23s+q/9wTpOeo2HhNE97XcRa7T8MA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19 h1:woXadbf0c7enQ2UGCi8gW/WuKmE0xIzxBF/eD94jMKQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19/go.mod h1:zminj5ucw7w0r65bP6nhyOd3xL6veAUMc3ElGMoLVb4= +github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.23 h1:B2qK61ZXCQu8tkD6eG/gUiIt9Vw9tmWFD7Xo02JPdMY= +github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.23/go.mod h1:02rz9vMZsrOX9IwUcpoGZM4jPprFNPmtD6t9Ume9ECY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23 h1:A2w6m6Tmr+BNXjDsr7M90zkWjsu4JXHwrzPg235STs4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23/go.mod h1:35EVp9wyeANdujZruvHiQUAo9E3vbhnIO1mTCAxMlY0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23 h1:pgYW9FCabt2M25MoHYCfMrVY2ghiiBKYWUVXfwZs+sU= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23/go.mod h1:c48kLgzO19wAu3CPkDWC28JbaJ+hfQlsdl7I2+oqIbk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.3 h1:qcxX0JYlgWH3hpPUnd6U0ikcl6LLA9sLkXE2w1fpMvY= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.3/go.mod h1:cLSNEmI45soc+Ef8K/L+8sEA3A3pYFEYf5B5UI+6bH4= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.3 h1:UTpsIf0loCIWEbrqdLb+0RxnTXfWh2vhw4nQmFi4nPc= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.3/go.mod h1:FZ9j3PFHHAR+w0BSEjK955w5YD2UwB/l/H0yAK3MJvI= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.3 h1:2YCmIXv3tmiItw0LlYf6v7gEHebLY45kBEnPezbUKyU= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.3/go.mod h1:u19stRyNPxGhj6dRm+Cdgu6N75qnbW7+QN0q0dsAk58= -github.com/aws/aws-sdk-go-v2/service/sts v1.32.3 h1:wVnQ6tigGsRqSWDEEyH6lSAJ9OyFUsSnbaUWChuSGzs= -github.com/aws/aws-sdk-go-v2/service/sts v1.32.3/go.mod h1:VZa9yTFyj4o10YGsmDO4gbQJUvvhY72fhumT8W4LqsE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4 h1:tHxQi/XHPK0ctd/wdOw0t7Xrc2OxcRCnVzv8lwWPu0c= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4/go.mod h1:4GQbF1vJzG60poZqWatZlhP31y8PGCCVTvIGPdaaYJ0= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.5 h1:HJwZwRt2Z2Tdec+m+fPjvdmkq2s9Ra+VR0hjF7V2o40= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.5/go.mod h1:wrMCEwjFPms+V86TCQQeOxQF/If4vT44FGIOFiMC2ck= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4 h1:zcx9LiGWZ6i6pjdcoE9oXAB6mUdeyC36Ia/QEiIvYdg= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4/go.mod h1:Tp/ly1cTjRLGBBmNccFumbZ8oqpZlpdhFf80SrRh4is= +github.com/aws/aws-sdk-go-v2/service/sts v1.32.4 h1:yDxvkz3/uOKfxnv8YhzOi9m+2OGIxF+on3KOISbK5IU= +github.com/aws/aws-sdk-go-v2/service/sts v1.32.4/go.mod h1:9XEUty5v5UAsMiFOBJrNibZgwCeOma73jgGwwhgffa8= github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= @@ -106,8 +106,8 @@ github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241107141636-5509ff77294e h1:O7HXIkgcHTY81Ehoex+a2JpmKWP+Co6eYZdoHX0r96w= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241107141636-5509ff77294e/go.mod h1:DTqSp28pYPZa4O1WrOg3kobhgTHdk9geGtxnws9EViM= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241114125605-4a3e447246a9 h1:l6jieaR+sn4Ff+puBDMbTYmT2HTYC7Yt7GTxBAwC3eU= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241114125605-4a3e447246a9/go.mod h1:m0uKkey9OC/AeyWMwjMfZqhLzoWrPFBk8vuYdSSYj4Y= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= @@ -140,8 +140,8 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 h1:c5FlPPgxOn7kJz3VoPLkQYQXGBS3EklQ4Zfi57uOuqQ= -github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -231,10 +231,10 @@ github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= -github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= -github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= -github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= @@ -348,16 +348,16 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zclconf/go-cty v1.13.2 h1:4GvrUxe/QUDYuJKAav4EYqdM47/kZa672LwmXFmEKT0= github.com/zclconf/go-cty v1.13.2/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= -go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= -go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= go.opentelemetry.io/otel/log v0.7.0 h1:d1abJc0b1QQZADKvfe9JqqrfmPYQCz2tUSO+0XZmuV4= go.opentelemetry.io/otel/log v0.7.0/go.mod h1:2jf2z7uVfnzDNknKTO9G+ahcOAyWcp1fJmk/wJjULRo= -go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= -go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= -go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= -go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= -go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= -go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= +go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= +go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw= @@ -405,8 +405,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -434,8 +434,8 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -450,8 +450,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -466,8 +466,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 h1:zciRKQ4kBpFgpfC5QQCVtnnNAcLIqweL7plyZRQHVpI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 h1:XVhgTWWV3kGQlwJHR3upFWZeTsei6Oks1apkZSeonIE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= diff --git a/tools/generator/Earthfile b/tools/generator/Earthfile index 2111309b5..6690189e5 100644 --- a/tools/generator/Earthfile +++ b/tools/generator/Earthfile @@ -19,7 +19,7 @@ sources: COPY ../..+tidy/go.sum /src/ WORKDIR /src/tools/generator - COPY --dir cmd . + COPY --dir cmd examples . COPY go.* *.go . SAVE ARTIFACT /src diff --git a/tools/generator/go.mod b/tools/generator/go.mod index 0e08ee16d..0adff009d 100644 --- a/tools/generator/go.mod +++ b/tools/generator/go.mod @@ -9,81 +9,44 @@ replace github.com/formancehq/ledger => ../.. replace github.com/formancehq/ledger/pkg/client => ../../pkg/client require ( - github.com/formancehq/go-libs/v2 v2.0.1-0.20241107141636-5509ff77294e + github.com/formancehq/go-libs/v2 v2.0.1-0.20241114125605-4a3e447246a9 github.com/formancehq/ledger v0.0.0-00010101000000-000000000000 github.com/formancehq/ledger/pkg/client v0.0.0-00010101000000-000000000000 github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.9.0 golang.org/x/oauth2 v0.23.0 - golang.org/x/sync v0.8.0 + golang.org/x/sync v0.9.0 ) require ( dario.cat/mergo v1.0.1 // indirect - filippo.io/edwards25519 v1.1.0 // indirect - github.com/IBM/sarama v1.43.3 // indirect - github.com/ThreeDotsLabs/watermill v1.3.7 // indirect - github.com/ThreeDotsLabs/watermill-http/v2 v2.3.1 // indirect - github.com/ThreeDotsLabs/watermill-kafka/v3 v3.0.5 // indirect - github.com/ThreeDotsLabs/watermill-nats/v2 v2.1.1 // indirect - github.com/ajg/form v1.5.1 // indirect + github.com/ThreeDotsLabs/watermill v1.4.1 // indirect github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect - github.com/aws/aws-msk-iam-sasl-signer-go v1.0.0 // indirect - github.com/aws/aws-sdk-go-v2 v1.32.3 // indirect - github.com/aws/aws-sdk-go-v2/config v1.28.1 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.17.42 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.18 // indirect - github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.22 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.22 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.22 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.3 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.24.3 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.3 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.32.3 // indirect - github.com/aws/smithy-go v1.22.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/bluele/gcache v0.0.2 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dlclark/regexp2 v1.11.4 // indirect - github.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 // indirect github.com/dop251/goja v0.0.0-20241009100908-5f46f2705ca3 // indirect - github.com/eapache/go-resiliency v1.7.0 // indirect - github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect - github.com/eapache/queue v1.1.0 // indirect - github.com/ebitengine/purego v0.8.1 // indirect github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731 // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/formancehq/numscript v0.0.9-0.20241009144012-1150c14a1417 // indirect - github.com/go-chi/chi v4.1.2+incompatible // indirect github.com/go-chi/chi/v5 v5.1.0 // indirect - github.com/go-chi/cors v1.2.1 // indirect - github.com/go-chi/render v1.0.3 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect - github.com/go-sql-driver/mysql v1.8.1 // indirect - github.com/go-task/slim-sprig/v3 v3.0.0 // indirect - github.com/golang/snappy v0.0.4 // indirect - github.com/google/go-cmp v0.6.0 // indirect - github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect + github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/schema v1.4.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect - github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/jsonschema v0.12.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect @@ -91,78 +54,41 @@ require ( github.com/jackc/pgx/v5 v5.7.1 // indirect github.com/jackc/pgxlisten v0.0.0-20241005155529-9d952acd6a6c // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/jcmturner/aescts/v2 v2.0.0 // indirect - github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect - github.com/jcmturner/gofork v1.7.6 // indirect - github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect - github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/jinzhu/inflection v1.0.0 // indirect - github.com/klauspost/compress v1.17.11 // indirect github.com/lithammer/shortuuid/v3 v3.0.7 // indirect github.com/logrusorgru/aurora v2.0.3+incompatible // indirect - github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/muhlemmer/gu v0.3.1 // indirect github.com/muhlemmer/httpforwarded v0.1.0 // indirect - github.com/nats-io/nats.go v1.37.0 // indirect - github.com/nats-io/nkeys v0.4.7 // indirect - github.com/nats-io/nuid v1.0.1 // indirect github.com/oklog/ulid v1.3.1 // indirect - github.com/onsi/ginkgo/v2 v2.20.2 // indirect - github.com/onsi/gomega v1.34.2 // indirect - github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect - github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect github.com/riandyrn/otelchi v0.10.1 // indirect github.com/rs/cors v1.11.1 // indirect - github.com/shirou/gopsutil/v4 v4.24.9 // indirect - github.com/shomali11/util v0.0.0-20220717175126-f0771b70947f // indirect - github.com/shomali11/xsql v0.0.0-20190608141458-bf76292144df // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/tklauser/go-sysconf v0.3.14 // indirect - github.com/tklauser/numcpus v0.9.0 // indirect github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect github.com/uptrace/bun v1.2.5 // indirect - github.com/uptrace/bun/dialect/pgdialect v1.2.5 // indirect - github.com/uptrace/bun/extra/bunotel v1.2.5 // indirect github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 // indirect - github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect - github.com/xdg-go/pbkdf2 v1.0.0 // indirect - github.com/xdg-go/scram v1.1.2 // indirect - github.com/xdg-go/stringprep v1.0.4 // indirect - github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect - github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - github.com/xeipuuv/gojsonschema v1.2.0 // indirect - github.com/xo/dburl v0.23.2 // indirect - github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/zitadel/oidc/v2 v2.12.2 // indirect - go.opentelemetry.io/contrib/instrumentation/host v0.56.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect - go.opentelemetry.io/contrib/instrumentation/runtime v0.56.0 // indirect - go.opentelemetry.io/contrib/propagators/b3 v1.31.0 // indirect - go.opentelemetry.io/otel v1.31.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.31.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.31.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 // indirect + go.opentelemetry.io/contrib/propagators/b3 v1.32.0 // indirect + go.opentelemetry.io/otel v1.32.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 // indirect go.opentelemetry.io/otel/log v0.7.0 // indirect - go.opentelemetry.io/otel/metric v1.31.0 // indirect - go.opentelemetry.io/otel/sdk v1.31.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.31.0 // indirect - go.opentelemetry.io/otel/trace v1.31.0 // indirect + go.opentelemetry.io/otel/metric v1.32.0 // indirect + go.opentelemetry.io/otel/sdk v1.32.0 // indirect + go.opentelemetry.io/otel/trace v1.32.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/dig v1.18.0 // indirect go.uber.org/fx v1.23.0 // indirect @@ -172,11 +98,10 @@ require ( golang.org/x/crypto v0.28.0 // indirect golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect golang.org/x/net v0.30.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/text v0.19.0 // indirect - golang.org/x/tools v0.26.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.20.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 // indirect google.golang.org/grpc v1.67.1 // indirect google.golang.org/protobuf v1.35.1 // indirect gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect diff --git a/tools/generator/go.sum b/tools/generator/go.sum index 6b801ebf8..44dc83bc1 100644 --- a/tools/generator/go.sum +++ b/tools/generator/go.sum @@ -12,50 +12,48 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= -github.com/ThreeDotsLabs/watermill v1.3.7 h1:NV0PSTmuACVEOV4dMxRnmGXrmbz8U83LENOvpHekN7o= -github.com/ThreeDotsLabs/watermill v1.3.7/go.mod h1:lBnrLbxOjeMRgcJbv+UiZr8Ylz8RkJ4m6i/VN/Nk+to= +github.com/ThreeDotsLabs/watermill v1.4.1 h1:gjP6yZH+otMPjV0KsV07pl9TeMm9UQV/gqiuiuG5Drs= +github.com/ThreeDotsLabs/watermill v1.4.1/go.mod h1:lBnrLbxOjeMRgcJbv+UiZr8Ylz8RkJ4m6i/VN/Nk+to= github.com/ThreeDotsLabs/watermill-http/v2 v2.3.1 h1:M0iYM5HsGcoxtiQqprRlYZNZnGk3w5LsE9RbC2R8myQ= github.com/ThreeDotsLabs/watermill-http/v2 v2.3.1/go.mod h1:RwGHEzGsEEXC/rQNLWQqR83+WPlABgOgnv2kTB56Y4Y= github.com/ThreeDotsLabs/watermill-kafka/v3 v3.0.5 h1:ud+4txnRgtr3kZXfXZ5+C7kVQEvsLc5HSNUEa0g+X1Q= github.com/ThreeDotsLabs/watermill-kafka/v3 v3.0.5/go.mod h1:t4o+4A6GB+XC8WL3DandhzPwd265zQuyWMQC/I+WIOU= -github.com/ThreeDotsLabs/watermill-nats/v2 v2.1.1 h1:afAkAFzeooBRQvxElR+6xoigXKCukcZXnE9ACxhwlPI= -github.com/ThreeDotsLabs/watermill-nats/v2 v2.1.1/go.mod h1:stjbT+s4u/s5ime5jdIyvPyjBGwGeJewIN7jxH8gp4k= +github.com/ThreeDotsLabs/watermill-nats/v2 v2.1.2 h1:9d7Vb2gepq73Rn/aKaAJWbBiJzS6nDyOm4O353jVsTM= +github.com/ThreeDotsLabs/watermill-nats/v2 v2.1.2/go.mod h1:stjbT+s4u/s5ime5jdIyvPyjBGwGeJewIN7jxH8gp4k= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= -github.com/alitto/pond v1.9.2 h1:9Qb75z/scEZVCoSU+osVmQ0I0JOeLfdTDafrbcJ8CLs= -github.com/alitto/pond v1.9.2/go.mod h1:xQn3P/sHTYcU/1BR3i86IGIrilcrGC2LiS+E2+CJWsI= github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 h1:yL7+Jz0jTC6yykIK/Wh74gnTJnrGr5AyrNMXuA0gves= github.com/antlr/antlr4/runtime/Go/antlr v1.4.10/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/aws/aws-msk-iam-sasl-signer-go v1.0.0 h1:UyjtGmO0Uwl/K+zpzPwLoXzMhcN9xmnR2nrqJoBrg3c= github.com/aws/aws-msk-iam-sasl-signer-go v1.0.0/go.mod h1:TJAXuFs2HcMib3sN5L0gUC+Q01Qvy3DemvA55WuC+iA= -github.com/aws/aws-sdk-go-v2 v1.32.3 h1:T0dRlFBKcdaUPGNtkBSwHZxrtis8CQU17UpNBZYd0wk= -github.com/aws/aws-sdk-go-v2 v1.32.3/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo= -github.com/aws/aws-sdk-go-v2/config v1.28.1 h1:oxIvOUXy8x0U3fR//0eq+RdCKimWI900+SV+10xsCBw= -github.com/aws/aws-sdk-go-v2/config v1.28.1/go.mod h1:bRQcttQJiARbd5JZxw6wG0yIK3eLeSCPdg6uqmmlIiI= -github.com/aws/aws-sdk-go-v2/credentials v1.17.42 h1:sBP0RPjBU4neGpIYyx8mkU2QqLPl5u9cmdTWVzIpHkM= -github.com/aws/aws-sdk-go-v2/credentials v1.17.42/go.mod h1:FwZBfU530dJ26rv9saAbxa9Ej3eF/AK0OAY86k13n4M= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.18 h1:68jFVtt3NulEzojFesM/WVarlFpCaXLKaBxDpzkQ9OQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.18/go.mod h1:Fjnn5jQVIo6VyedMc0/EhPpfNlPl7dHV916O6B+49aE= -github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.22 h1:n89ziXnsp3dyOlodim8OHv0edSu47H7i75UYxDz1YVQ= -github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.22/go.mod h1:7uC80VxwPjAykLSIzkyTgZ+LjFDil+OVndzd8wGMOYY= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.22 h1:Jw50LwEkVjuVzE1NzkhNKkBf9cRN7MtE1F/b2cOKTUM= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.22/go.mod h1:Y/SmAyPcOTmpeVaWSzSKiILfXTVJwrGmYZhcRbhWuEY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.22 h1:981MHwBaRZM7+9QSR6XamDzF/o7ouUGxFzr+nVSIhrs= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.22/go.mod h1:1RA1+aBEfn+CAB/Mh0MB6LsdCYCnjZm7tKXtnk499ZQ= +github.com/aws/aws-sdk-go-v2 v1.32.4 h1:S13INUiTxgrPueTmrm5DZ+MiAo99zYzHEFh1UNkOxNE= +github.com/aws/aws-sdk-go-v2 v1.32.4/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo= +github.com/aws/aws-sdk-go-v2/config v1.28.3 h1:kL5uAptPcPKaJ4q0sDUjUIdueO18Q7JDzl64GpVwdOM= +github.com/aws/aws-sdk-go-v2/config v1.28.3/go.mod h1:SPEn1KA8YbgQnwiJ/OISU4fz7+F6Fe309Jf0QTsRCl4= +github.com/aws/aws-sdk-go-v2/credentials v1.17.44 h1:qqfs5kulLUHUEXlHEZXLJkgGoF3kkUeFUTVA585cFpU= +github.com/aws/aws-sdk-go-v2/credentials v1.17.44/go.mod h1:0Lm2YJ8etJdEdw23s+q/9wTpOeo2HhNE97XcRa7T8MA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19 h1:woXadbf0c7enQ2UGCi8gW/WuKmE0xIzxBF/eD94jMKQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19/go.mod h1:zminj5ucw7w0r65bP6nhyOd3xL6veAUMc3ElGMoLVb4= +github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.23 h1:B2qK61ZXCQu8tkD6eG/gUiIt9Vw9tmWFD7Xo02JPdMY= +github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.23/go.mod h1:02rz9vMZsrOX9IwUcpoGZM4jPprFNPmtD6t9Ume9ECY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23 h1:A2w6m6Tmr+BNXjDsr7M90zkWjsu4JXHwrzPg235STs4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23/go.mod h1:35EVp9wyeANdujZruvHiQUAo9E3vbhnIO1mTCAxMlY0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23 h1:pgYW9FCabt2M25MoHYCfMrVY2ghiiBKYWUVXfwZs+sU= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23/go.mod h1:c48kLgzO19wAu3CPkDWC28JbaJ+hfQlsdl7I2+oqIbk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.3 h1:qcxX0JYlgWH3hpPUnd6U0ikcl6LLA9sLkXE2w1fpMvY= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.3/go.mod h1:cLSNEmI45soc+Ef8K/L+8sEA3A3pYFEYf5B5UI+6bH4= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.3 h1:UTpsIf0loCIWEbrqdLb+0RxnTXfWh2vhw4nQmFi4nPc= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.3/go.mod h1:FZ9j3PFHHAR+w0BSEjK955w5YD2UwB/l/H0yAK3MJvI= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.3 h1:2YCmIXv3tmiItw0LlYf6v7gEHebLY45kBEnPezbUKyU= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.3/go.mod h1:u19stRyNPxGhj6dRm+Cdgu6N75qnbW7+QN0q0dsAk58= -github.com/aws/aws-sdk-go-v2/service/sts v1.32.3 h1:wVnQ6tigGsRqSWDEEyH6lSAJ9OyFUsSnbaUWChuSGzs= -github.com/aws/aws-sdk-go-v2/service/sts v1.32.3/go.mod h1:VZa9yTFyj4o10YGsmDO4gbQJUvvhY72fhumT8W4LqsE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4 h1:tHxQi/XHPK0ctd/wdOw0t7Xrc2OxcRCnVzv8lwWPu0c= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4/go.mod h1:4GQbF1vJzG60poZqWatZlhP31y8PGCCVTvIGPdaaYJ0= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.5 h1:HJwZwRt2Z2Tdec+m+fPjvdmkq2s9Ra+VR0hjF7V2o40= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.5/go.mod h1:wrMCEwjFPms+V86TCQQeOxQF/If4vT44FGIOFiMC2ck= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4 h1:zcx9LiGWZ6i6pjdcoE9oXAB6mUdeyC36Ia/QEiIvYdg= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4/go.mod h1:Tp/ly1cTjRLGBBmNccFumbZ8oqpZlpdhFf80SrRh4is= +github.com/aws/aws-sdk-go-v2/service/sts v1.32.4 h1:yDxvkz3/uOKfxnv8YhzOi9m+2OGIxF+on3KOISbK5IU= +github.com/aws/aws-sdk-go-v2/service/sts v1.32.4/go.mod h1:9XEUty5v5UAsMiFOBJrNibZgwCeOma73jgGwwhgffa8= github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= @@ -102,12 +100,10 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241107141636-5509ff77294e h1:O7HXIkgcHTY81Ehoex+a2JpmKWP+Co6eYZdoHX0r96w= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241107141636-5509ff77294e/go.mod h1:DTqSp28pYPZa4O1WrOg3kobhgTHdk9geGtxnws9EViM= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241114125605-4a3e447246a9 h1:l6jieaR+sn4Ff+puBDMbTYmT2HTYC7Yt7GTxBAwC3eU= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241114125605-4a3e447246a9/go.mod h1:m0uKkey9OC/AeyWMwjMfZqhLzoWrPFBk8vuYdSSYj4Y= github.com/formancehq/numscript v0.0.9-0.20241009144012-1150c14a1417 h1:LOd5hxnXDIBcehFrpW1OnXk+VSs0yJXeu1iAOO+Hji4= github.com/formancehq/numscript v0.0.9-0.20241009144012-1150c14a1417/go.mod h1:btuSv05cYwi9BvLRxVs5zrunU+O1vTgigG1T6UsawcY= -github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= -github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/gkampitakis/ciinfo v0.3.0 h1:gWZlOC2+RYYttL0hBqcoQhM7h1qNkVqvRCV1fOvpAv8= github.com/gkampitakis/ciinfo v0.3.0/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= @@ -127,7 +123,6 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= @@ -148,8 +143,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 h1:c5FlPPgxOn7kJz3VoPLkQYQXGBS3EklQ4Zfi57uOuqQ= -github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -159,13 +154,10 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= -github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= -github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 h1:ad0vkEBuk23VJzZR9nkLVG0YAoN9coASF1GusYX6AlU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0/go.mod h1:igFoXX2ELCW06bol23DWPB5BEWfZISOzSP5K2sbLea0= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= @@ -176,7 +168,6 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= -github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -199,8 +190,6 @@ github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8 github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= -github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= -github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= @@ -235,8 +224,6 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q= -github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= @@ -245,10 +232,6 @@ github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY= github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0= -github.com/nats-io/jwt/v2 v2.7.0 h1:J+ZnaaMGQi3xSB8iOhVM5ipiWCDrQvgEoitTwWFyOYw= -github.com/nats-io/jwt/v2 v2.7.0/go.mod h1:ZdWS1nZa6WMZfFwwgpEaqBV8EPGVgOTDHN/wTbz0Y5A= -github.com/nats-io/nats-server/v2 v2.10.21 h1:gfG6T06wBdI25XyY2IsauarOc2srWoFxxfsOKjrzoRA= -github.com/nats-io/nats-server/v2 v2.10.21/go.mod h1:I1YxSAEWbXCfy0bthwvNb5X43WwIWMz7gx5ZVPDr5Rc= github.com/nats-io/nats.go v1.37.0 h1:07rauXbVnnJvv1gfIyghFEo6lUcYRY0WXc3x7x0vUxE= github.com/nats-io/nats.go v1.37.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= @@ -257,10 +240,10 @@ github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= -github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= -github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= -github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= @@ -269,8 +252,6 @@ github.com/opencontainers/runc v1.1.14 h1:rgSuzbmgz5DUJjeSnw337TxDbRuqjs6iqQck/2 github.com/opencontainers/runc v1.1.14/go.mod h1:E4C2z+7BxR7GHXp0hAY53mek+x49X1LjPNeMTfRGvOA= github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA= github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI= -github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= -github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -291,10 +272,8 @@ github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWN github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shirou/gopsutil/v4 v4.24.9 h1:KIV+/HaHD5ka5f570RZq+2SaeFsb/pq+fp2DGNWYoOI= -github.com/shirou/gopsutil/v4 v4.24.9/go.mod h1:3fkaHNeYsUFCGZ8+9vZVWtbyM1k2eRnlL+bWO8Bxa/Q= -github.com/shomali11/parallelizer v0.0.0-20220717173222-a6776fbf40a9/go.mod h1:QsLM53l8gzX0sQbOjVir85bzOUucuJEF8JgE39wD7w0= -github.com/shomali11/util v0.0.0-20180607005212-e0f70fd665ff/go.mod h1:WWE2GJM9B5UpdOiwH2val10w/pvJ2cUUQOOA/4LgOng= +github.com/shirou/gopsutil/v4 v4.24.10 h1:7VOzPtfw/5YDU+jLEoBwXwxJbQetULywoSV4RYY7HkM= +github.com/shirou/gopsutil/v4 v4.24.10/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8= github.com/shomali11/util v0.0.0-20220717175126-f0771b70947f h1:OM0LVaVycWC+/j5Qra7USyCg2sc+shg3KwygAA+pYvA= github.com/shomali11/util v0.0.0-20220717175126-f0771b70947f/go.mod h1:9POpw/crb6YrseaYBOwraL0lAYy0aOW79eU3bvMxgbM= github.com/shomali11/xsql v0.0.0-20190608141458-bf76292144df h1:SVCDTuzM3KEk8WBwSSw7RTPLw9ajzBaXDg39Bo6xIeU= @@ -306,16 +285,9 @@ github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3k github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= @@ -336,8 +308,8 @@ github.com/uptrace/bun v1.2.5 h1:gSprL5xiBCp+tzcZHgENzJpXnmQwRM/A6s4HnBF85mc= github.com/uptrace/bun v1.2.5/go.mod h1:vkQMS4NNs4VNZv92y53uBSHXRqYyJp4bGhMHgaNCQpY= github.com/uptrace/bun/dialect/pgdialect v1.2.5 h1:dWLUxpjTdglzfBks2x+U2WIi+nRVjuh7Z3DLYVFswJk= github.com/uptrace/bun/dialect/pgdialect v1.2.5/go.mod h1:stwnlE8/6x8cuQ2aXcZqwDK/d+6jxgO3iQewflJT6C4= -github.com/uptrace/bun/extra/bundebug v1.2.3 h1:2QBykz9/u4SkN9dnraImDcbrMk2fUhuq2gL6hkh9qSc= -github.com/uptrace/bun/extra/bundebug v1.2.3/go.mod h1:bihsYJxXxWZXwc1R3qALTHvp+npE0ElgaCvcjzyPPdw= +github.com/uptrace/bun/extra/bundebug v1.2.5 h1:DsI/gl4jvq5tQ84yPqnlRYIQ4U6AouTqxJ8Y5Oijfz4= +github.com/uptrace/bun/extra/bundebug v1.2.5/go.mod h1:JFeYvklf5p92ZILXx4siMe2tEVn5JLelww3IGcJb4yA= github.com/uptrace/bun/extra/bunotel v1.2.5 h1:kkuuTbrG9d5leYZuSBKhq2gtq346lIrxf98Mig2y128= github.com/uptrace/bun/extra/bunotel v1.2.5/go.mod h1:rCHLszRZwppWE9cGDodO2FCI1qCrLwDjONp38KD3bA8= github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 h1:H8wwQwTe5sL6x30z71lUgNiwBdeCHQjrphCfLwqIHGo= @@ -358,7 +330,6 @@ github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= @@ -367,45 +338,44 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xo/dburl v0.23.2 h1:Fl88cvayrgE56JA/sqhNMLljCW/b7RmG1mMkKMZUFgA= github.com/xo/dburl v0.23.2/go.mod h1:uazlaAQxj4gkshhfuuYyvwCBouOmNnG2aDxTCFZpmL4= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zitadel/oidc/v2 v2.12.2 h1:3kpckg4rurgw7w7aLJrq7yvRxb2pkNOtD08RH42vPEs= github.com/zitadel/oidc/v2 v2.12.2/go.mod h1:vhP26g1g4YVntcTi0amMYW3tJuid70nxqxf+kb6XKgg= -go.opentelemetry.io/contrib/instrumentation/host v0.56.0 h1:bLJ0U2SVly7aCVAv4pSJ62I0yy3GHPMbK+74AXSwC40= -go.opentelemetry.io/contrib/instrumentation/host v0.56.0/go.mod h1:7XvO8DvjdcoYDOQs/1n3AuadI/30eE2R+H/pQQuZVN0= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= -go.opentelemetry.io/contrib/instrumentation/runtime v0.56.0 h1:s7wHG+t8bEoH7ibWk1nk682h7EoWLJ5/8j+TSO3bX/o= -go.opentelemetry.io/contrib/instrumentation/runtime v0.56.0/go.mod h1:Q8Hsv3d9DwryfIl+ebj4mY81IYVRSPy4QfxroVZwqLo= -go.opentelemetry.io/contrib/propagators/b3 v1.31.0 h1:PQPXYscmwbCp76QDvO4hMngF2j8Bx/OTV86laEl8uqo= -go.opentelemetry.io/contrib/propagators/b3 v1.31.0/go.mod h1:jbqfV8wDdqSDrAYxVpXQnpM0XFMq2FtDesblJ7blOwQ= -go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= -go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0 h1:FZ6ei8GFW7kyPYdxJaV2rgI6M+4tvZzhYsQ2wgyVC08= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0/go.mod h1:MdEu/mC6j3D+tTEfvI15b5Ci2Fn7NneJ71YMoiS3tpI= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0 h1:ZsXq73BERAiNuuFXYqP4MR5hBrjXfMGSO+Cx7qoOZiM= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0/go.mod h1:hg1zaDMpyZJuUzjFxFsRYBoccE86tM9Uf4IqNMUxvrY= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 h1:K0XaT3DwHAcV4nKLzcQvwAgSyisUghWoY20I7huthMk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0/go.mod h1:B5Ki776z/MBnVha1Nzwp5arlzBbE3+1jk+pGmaP5HME= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 h1:FFeLy03iVTXP6ffeN2iXrxfGsZGCjVx0/4KlizjyBwU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0/go.mod h1:TMu73/k1CP8nBUpDLc71Wj/Kf7ZS9FK5b53VapRsP9o= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 h1:lUsI2TYsQw2r1IASwoROaCnjdj2cvC2+Jbxvk6nHnWU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0/go.mod h1:2HpZxxQurfGxJlJDblybejHB6RX6pmExPNe517hREw4= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.31.0 h1:HZgBIps9wH0RDrwjrmNa3DVbNRW60HEhdzqZFyAp3fI= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.31.0/go.mod h1:RDRhvt6TDG0eIXmonAx5bd9IcwpqCkziwkOClzWKwAQ= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.31.0 h1:UGZ1QwZWY67Z6BmckTU+9Rxn04m2bD3gD6Mk0OIOCPk= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.31.0/go.mod h1:fcwWuDuaObkkChiDlhEpSq9+X1C0omv+s5mBtToAQ64= +go.opentelemetry.io/contrib/instrumentation/host v0.57.0 h1:1gfzOyXEuCrrwCXF81LO3DQ4rll6YBKfAQHPl+03mik= +go.opentelemetry.io/contrib/instrumentation/host v0.57.0/go.mod h1:pHBt+1Rhz99VBX7AQVgwcKPf611zgD6pQy7VwBNMFmE= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94= +go.opentelemetry.io/contrib/instrumentation/runtime v0.57.0 h1:kJB5wMVorwre8QzEodzTAbzm9FOOah0zvG+V4abNlEE= +go.opentelemetry.io/contrib/instrumentation/runtime v0.57.0/go.mod h1:Nup4TgnOyEJWmVq9sf/ASH3ZJiAXwWHd5xZCHG7Sg9M= +go.opentelemetry.io/contrib/propagators/b3 v1.32.0 h1:MazJBz2Zf6HTN/nK/s3Ru1qme+VhWU5hm83QxEP+dvw= +go.opentelemetry.io/contrib/propagators/b3 v1.32.0/go.mod h1:B0s70QHYPrJwPOwD1o3V/R8vETNOG9N3qZf4LDYvA30= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 h1:t/Qur3vKSkUCcDVaSumWF2PKHt85pc7fRvFuoVT8qFU= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0/go.mod h1:Rl61tySSdcOJWoEgYZVtmnKdA0GeKrSqkHC1t+91CH8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 h1:9kV11HXBHZAvuPUZxmMWrH8hZn/6UnHX4K0mu36vNsU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0/go.mod h1:JyA0FHXe22E1NeNiHmVp7kFHglnexDQ7uRWDiiJ1hKQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 h1:SZmDnHcgp3zwlPBS2JX2urGYe/jBKEIT6ZedHRUyCz8= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0/go.mod h1:fdWW0HtZJ7+jNpTKUR0GpMEDP69nR8YBJQxNiVCE3jk= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s= go.opentelemetry.io/otel/log v0.7.0 h1:d1abJc0b1QQZADKvfe9JqqrfmPYQCz2tUSO+0XZmuV4= go.opentelemetry.io/otel/log v0.7.0/go.mod h1:2jf2z7uVfnzDNknKTO9G+ahcOAyWcp1fJmk/wJjULRo= -go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= -go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= -go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= -go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= -go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= -go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= -go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= -go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= +go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= +go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= +go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= +go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw= @@ -420,69 +390,34 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= -golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38 h1:2oV8dfuIkM1Ti7DwXc0BJfnwr9csz4TDXI9EmiI+Rbw= -google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38/go.mod h1:vuAjtvlwkDKF6L1GQ0SokiRLCGFfeBUXWr/aFFkHACc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 h1:zciRKQ4kBpFgpfC5QQCVtnnNAcLIqweL7plyZRQHVpI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 h1:M0KvPgPmDZHPlbRbaNU1APr28TvwvvdUPlSv7PUvy8g= +google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:dguCy7UOdZhTvLzDyt15+rOrawrpM4q7DD9dQ1P11P4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 h1:XVhgTWWV3kGQlwJHR3upFWZeTsei6Oks1apkZSeonIE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= @@ -492,7 +427,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs= gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From e79871adfb7bb852434534f4182e4a822a571589 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 14 Nov 2024 18:01:39 +0100 Subject: [PATCH 18/71] fix: flaky test (account ordering not specified) --- internal/storage/ledger/volumes.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/storage/ledger/volumes.go b/internal/storage/ledger/volumes.go index a7a259aa0..aa8806108 100644 --- a/internal/storage/ledger/volumes.go +++ b/internal/storage/ledger/volumes.go @@ -114,7 +114,8 @@ func (s *Store) selectVolumes(oot, pit *time.Time, useInsertionDate bool, groupL ColumnExpr("sum(case when not is_source then amount else -amount end) as balance"). ModelTableExpr(s.GetPrefixedRelationName("moves")). Where("ledger = ?", s.ledger.Name). - GroupExpr("accounts_address, asset") + GroupExpr("accounts_address, asset"). + Order("accounts_address", "asset") dateFilterColumn := "effective_date" if useInsertionDate { From 18d7e1c4f7ad99941ae0eec6db1c15b3a71e5591 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Tue, 19 Nov 2024 14:51:07 +0100 Subject: [PATCH 19/71] feat: add generator set --- docs/api/README.md | 46 +++++ internal/README.md | 120 +----------- internal/api/common/mocks.go | 2 +- internal/api/v1/mocks.go | 2 +- internal/api/v2/common.go | 2 +- internal/api/v2/errors.go | 16 +- internal/api/v2/mocks.go | 2 +- .../controller/ledger/controller_default.go | 3 +- internal/controller/system/controller.go | 3 +- internal/controller/system/errors.go | 4 +- internal/errors.go | 2 +- internal/ledger.go | 128 +------------ internal/ledger_test.go | 5 +- internal/storage/bucket/bucket.go | 81 ++++---- internal/storage/ledger/accounts.go | 11 +- internal/storage/ledger/balances.go | 15 +- internal/storage/ledger/logs.go | 3 +- internal/storage/ledger/moves.go | 5 +- internal/storage/ledger/store.go | 5 +- internal/storage/ledger/transactions.go | 13 +- internal/storage/ledger/volumes.go | 5 +- internal/storage/module.go | 8 +- openapi.yaml | 24 +++ openapi/v2.yaml | 24 +++ pkg/client/.speakeasy/gen.lock | 8 +- pkg/client/.speakeasy/gen.yaml | 2 +- pkg/client/README.md | 1 + .../models/operations/getmetricsresponse.md | 9 + pkg/client/docs/sdks/ledger/README.md | 52 +++++ pkg/client/formance.go | 4 +- pkg/client/ledger.go | 179 ++++++++++++++++++ pkg/client/models/operations/getmetrics.go | 27 +++ pkg/features/features.go | 119 ++++++++++++ pkg/generate/generator.go | 10 +- pkg/generate/set.go | 73 +++++++ pkg/testserver/server.go | 2 +- test/e2e/api_ledgers_create_test.go | 12 +- test/e2e/api_ledgers_import_test.go | 6 +- test/performance/benchmark_test.go | 13 +- test/performance/features_test.go | 14 +- test/rolling-upgrades/main_test.go | 6 +- test/stress/stress_test.go | 4 +- tools/generator/cmd/root.go | 45 +---- 43 files changed, 712 insertions(+), 403 deletions(-) create mode 100644 pkg/client/docs/models/operations/getmetricsresponse.md create mode 100644 pkg/client/models/operations/getmetrics.go create mode 100644 pkg/features/features.go create mode 100644 pkg/generate/set.go diff --git a/docs/api/README.md b/docs/api/README.md index 616d7e693..613b21baa 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -60,6 +60,52 @@ To perform this operation, you must be authenticated by means of one of the foll Authorization ( Scopes: ledger:read ) +## Read in memory metrics + + + +> Code samples + +```http +GET http://localhost:8080/_/metrics HTTP/1.1 +Host: localhost:8080 +Accept: application/json + +``` + +`GET /_/metrics` + +> Example responses + +> 200 Response + +```json +{ + "property1": null, + "property2": null +} +``` + +

Responses

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

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|» **additionalProperties**|any|false|none|none| + + +

ledger.v2

## List ledgers diff --git a/internal/README.md b/internal/README.md index d71e81cf6..6a8490675 100644 --- a/internal/README.md +++ b/internal/README.md @@ -32,11 +32,6 @@ import "github.com/formancehq/ledger/internal" - [type ErrInvalidLedgerName](<#ErrInvalidLedgerName>) - [func \(e ErrInvalidLedgerName\) Error\(\) string](<#ErrInvalidLedgerName.Error>) - [func \(e ErrInvalidLedgerName\) Is\(err error\) bool](<#ErrInvalidLedgerName.Is>) -- [type FeatureSet](<#FeatureSet>) - - [func \(f FeatureSet\) Match\(features FeatureSet\) bool](<#FeatureSet.Match>) - - [func \(f FeatureSet\) SortedKeys\(\) \[\]string](<#FeatureSet.SortedKeys>) - - [func \(f FeatureSet\) String\(\) string](<#FeatureSet.String>) - - [func \(f FeatureSet\) With\(feature, value string\) FeatureSet](<#FeatureSet.With>) - [type Ledger](<#Ledger>) - [func MustNewWithDefault\(name string\) Ledger](<#MustNewWithDefault>) - [func New\(name string, configuration Configuration\) \(\*Ledger, error\)](<#New>) @@ -116,40 +111,20 @@ import "github.com/formancehq/ledger/internal" ## Constants - + ```go const ( - // FeatureMovesHistory is used to define if the ledger has to save funds movements history. - // Value is either ON or OFF - FeatureMovesHistory = "MOVES_HISTORY" - // FeatureMovesHistoryPostCommitEffectiveVolumes is used to define if the pvce property of funds movements history - // has to be updated with back dated transaction. - // Value is either SYNC or DISABLED. - // todo: depends on FeatureMovesHistory (dependency should be checked) - FeatureMovesHistoryPostCommitEffectiveVolumes = "MOVES_HISTORY_POST_COMMIT_EFFECTIVE_VOLUMES" - // FeatureHashLogs is used to defined it the logs has to be hashed. - FeatureHashLogs = "HASH_LOGS" - // FeatureAccountMetadataHistory is used to defined it the account metadata must be historized. - FeatureAccountMetadataHistory = "ACCOUNT_METADATA_HISTORY" - // FeatureTransactionMetadataHistory is used to defined it the transaction metadata must be historized. - FeatureTransactionMetadataHistory = "TRANSACTION_METADATA_HISTORY" - // FeatureIndexAddressSegments is used to defined it we want to index segments of accounts address. - // Without this feature, the ledger will not allow filtering on partial account address. - FeatureIndexAddressSegments = "INDEX_ADDRESS_SEGMENTS" - // FeatureIndexTransactionAccounts is used to defined it we want to index accounts used in a transaction. - FeatureIndexTransactionAccounts = "INDEX_TRANSACTION_ACCOUNTS" - - DefaultBucket = "_default" + MetaTargetTypeAccount = "ACCOUNT" + MetaTargetTypeTransaction = "TRANSACTION" ) ``` - + ```go const ( - MetaTargetTypeAccount = "ACCOUNT" - MetaTargetTypeTransaction = "TRANSACTION" + DefaultBucket = "_default" ) ``` @@ -163,40 +138,6 @@ const ( ## Variables - - -```go -var ( - DefaultFeatures = FeatureSet{ - FeatureMovesHistory: "ON", - FeatureMovesHistoryPostCommitEffectiveVolumes: "SYNC", - FeatureHashLogs: "SYNC", - FeatureAccountMetadataHistory: "SYNC", - FeatureTransactionMetadataHistory: "SYNC", - FeatureIndexAddressSegments: "ON", - FeatureIndexTransactionAccounts: "ON", - } - MinimalFeatureSet = FeatureSet{ - FeatureMovesHistory: "OFF", - FeatureMovesHistoryPostCommitEffectiveVolumes: "DISABLED", - FeatureHashLogs: "DISABLED", - FeatureAccountMetadataHistory: "DISABLED", - FeatureTransactionMetadataHistory: "DISABLED", - FeatureIndexAddressSegments: "OFF", - FeatureIndexTransactionAccounts: "OFF", - } - FeatureConfigurations = map[string][]string{ - FeatureMovesHistory: {"ON", "OFF"}, - FeatureMovesHistoryPostCommitEffectiveVolumes: {"SYNC", "DISABLED"}, - FeatureHashLogs: {"SYNC", "DISABLED"}, - FeatureAccountMetadataHistory: {"SYNC", "DISABLED"}, - FeatureTransactionMetadataHistory: {"SYNC", "DISABLED"}, - FeatureIndexAddressSegments: {"ON", "OFF"}, - FeatureIndexTransactionAccounts: {"ON", "OFF"}, - } -) -``` - ```go @@ -281,9 +222,9 @@ type BalancesByAssetsByAccounts map[string]BalancesByAssets ```go type Configuration struct { - Bucket string `json:"bucket" bun:"bucket,type:varchar(255)"` - Metadata metadata.Metadata `json:"metadata" bun:"metadata,type:jsonb"` - Features FeatureSet `json:"features" bun:"features,type:jsonb"` + Bucket string `json:"bucket" bun:"bucket,type:varchar(255)"` + Metadata metadata.Metadata `json:"metadata" bun:"metadata,type:jsonb"` + Features features.FeatureSet `json:"features" bun:"features,type:jsonb"` } ``` @@ -433,51 +374,6 @@ func (e ErrInvalidLedgerName) Is(err error) bool - -## type FeatureSet - - - -```go -type FeatureSet map[string]string -``` - - -### func \(FeatureSet\) Match - -```go -func (f FeatureSet) Match(features FeatureSet) bool -``` - - - - -### func \(FeatureSet\) SortedKeys - -```go -func (f FeatureSet) SortedKeys() []string -``` - - - - -### func \(FeatureSet\) String - -```go -func (f FeatureSet) String() string -``` - - - - -### func \(FeatureSet\) With - -```go -func (f FeatureSet) With(feature, value string) FeatureSet -``` - - - ## type Ledger diff --git a/internal/api/common/mocks.go b/internal/api/common/mocks.go index ffc0edce4..349e3fa56 100644 --- a/internal/api/common/mocks.go +++ b/internal/api/common/mocks.go @@ -1,3 +1,3 @@ //go:generate mockgen -write_source_comment=false -write_package_comment=false -source ../../controller/system/controller.go -destination mocks_system_controller_test.go -package common --mock_names Controller=SystemController . Controller //go:generate mockgen -write_source_comment=false -write_package_comment=false -source ../../controller/ledger/controller.go -destination mocks_ledger_controller_test.go -package common --mock_names Controller=LedgerController . Controller -package common \ No newline at end of file +package common diff --git a/internal/api/v1/mocks.go b/internal/api/v1/mocks.go index f10db2ce4..f2523b685 100644 --- a/internal/api/v1/mocks.go +++ b/internal/api/v1/mocks.go @@ -1,3 +1,3 @@ //go:generate mockgen -write_source_comment=false -write_package_comment=false -source ../../controller/system/controller.go -destination mocks_system_controller_test.go -package v1 --mock_names Controller=SystemController . Controller //go:generate mockgen -write_source_comment=false -write_package_comment=false -source ../../controller/ledger/controller.go -destination mocks_ledger_controller_test.go -package v1 --mock_names Controller=LedgerController . Controller -package v1 \ No newline at end of file +package v1 diff --git a/internal/api/v2/common.go b/internal/api/v2/common.go index 82fbc486e..b223f1fee 100644 --- a/internal/api/v2/common.go +++ b/internal/api/v2/common.go @@ -176,4 +176,4 @@ func getPaginatedQueryOptionsOfFiltersForVolumes(r *http.Request) (*ledgercontro return pointer.For(ledgercontroller.NewPaginatedQueryOptions(*filtersForVolumes). WithPageSize(pageSize). WithQueryBuilder(qb)), nil -} \ No newline at end of file +} diff --git a/internal/api/v2/errors.go b/internal/api/v2/errors.go index 438a1e9ad..cdcc2b254 100644 --- a/internal/api/v2/errors.go +++ b/internal/api/v2/errors.go @@ -1,14 +1,14 @@ package v2 const ( - ErrConflict = "CONFLICT" - ErrInsufficientFund = "INSUFFICIENT_FUND" - ErrValidation = "VALIDATION" - ErrAlreadyRevert = "ALREADY_REVERT" - ErrNoPostings = "NO_POSTINGS" - ErrCompilationFailed = "COMPILATION_FAILED" - ErrMetadataOverride = "METADATA_OVERRIDE" - ErrBulkSizeExceeded = "BULK_SIZE_EXCEEDED" + ErrConflict = "CONFLICT" + ErrInsufficientFund = "INSUFFICIENT_FUND" + ErrValidation = "VALIDATION" + ErrAlreadyRevert = "ALREADY_REVERT" + ErrNoPostings = "NO_POSTINGS" + ErrCompilationFailed = "COMPILATION_FAILED" + ErrMetadataOverride = "METADATA_OVERRIDE" + ErrBulkSizeExceeded = "BULK_SIZE_EXCEEDED" ErrLedgerAlreadyExists = "LEDGER_ALREADY_EXISTS" ErrInterpreterParse = "INTERPRETER_PARSE" diff --git a/internal/api/v2/mocks.go b/internal/api/v2/mocks.go index c082bd6a6..2cfde480e 100644 --- a/internal/api/v2/mocks.go +++ b/internal/api/v2/mocks.go @@ -1,3 +1,3 @@ //go:generate mockgen -write_source_comment=false -write_package_comment=false -source ../../controller/system/controller.go -destination mocks_system_controller_test.go -package v2 --mock_names Controller=SystemController . Controller //go:generate mockgen -write_source_comment=false -write_package_comment=false -source ../../controller/ledger/controller.go -destination mocks_ledger_controller_test.go -package v2 --mock_names Controller=LedgerController . Controller -package v2 \ No newline at end of file +package v2 diff --git a/internal/controller/ledger/controller_default.go b/internal/controller/ledger/controller_default.go index f8dcc70ef..b55d29e78 100644 --- a/internal/controller/ledger/controller_default.go +++ b/internal/controller/ledger/controller_default.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "github.com/formancehq/ledger/pkg/features" "math/big" "reflect" @@ -210,7 +211,7 @@ func (ctrl *DefaultController) importLog(ctx context.Context, sqlTx TX, log ledg return fmt.Errorf("failed to insert log: %w", err) } - if ctrl.ledger.HasFeature(ledger.FeatureHashLogs, "SYNC") { + if ctrl.ledger.HasFeature(features.FeatureHashLogs, "SYNC") { if !reflect.DeepEqual(log.Hash, logCopy.Hash) { return newErrInvalidHash(log.ID, logCopy.Hash, log.Hash) } diff --git a/internal/controller/system/controller.go b/internal/controller/system/controller.go index 86b6b63d7..44265079f 100644 --- a/internal/controller/system/controller.go +++ b/internal/controller/system/controller.go @@ -2,6 +2,7 @@ package system import ( "context" + "github.com/formancehq/ledger/pkg/features" "reflect" "time" @@ -89,7 +90,7 @@ func (ctrl *DefaultController) CreateLedger(ctx context.Context, name string, co configuration.SetDefaults() if !ctrl.enableFeatures { - if !reflect.DeepEqual(configuration.Features, ledger.DefaultFeatures) { + if !reflect.DeepEqual(configuration.Features, features.DefaultFeatures) { return ErrExperimentalFeaturesDisabled } } diff --git a/internal/controller/system/errors.go b/internal/controller/system/errors.go index d5b5e1da7..18d5eca64 100644 --- a/internal/controller/system/errors.go +++ b/internal/controller/system/errors.go @@ -6,7 +6,7 @@ import ( ) var ( - ErrLedgerAlreadyExists = errors.New("ledger already exists") + ErrLedgerAlreadyExists = errors.New("ledger already exists") ErrExperimentalFeaturesDisabled = errors.New("experimental features are disabled") ) @@ -27,4 +27,4 @@ func newErrInvalidLedgerConfiguration(err error) ErrInvalidLedgerConfiguration { return ErrInvalidLedgerConfiguration{ err: err, } -} \ No newline at end of file +} diff --git a/internal/errors.go b/internal/errors.go index f51e711c4..aa6d9e777 100644 --- a/internal/errors.go +++ b/internal/errors.go @@ -36,4 +36,4 @@ func (e ErrInvalidBucketName) Is(err error) bool { func newErrInvalidBucketName(bucket string, err error) ErrInvalidBucketName { return ErrInvalidBucketName{err: err, bucket: bucket} -} \ No newline at end of file +} diff --git a/internal/ledger.go b/internal/ledger.go index a66f5e996..f50708fd2 100644 --- a/internal/ledger.go +++ b/internal/ledger.go @@ -2,14 +2,12 @@ package ledger import ( "fmt" - . "github.com/formancehq/go-libs/v2/collectionutils" + "github.com/formancehq/go-libs/v2/metadata" "github.com/formancehq/go-libs/v2/time" + "github.com/formancehq/ledger/pkg/features" "github.com/uptrace/bun" "regexp" "slices" - "strings" - - "github.com/formancehq/go-libs/v2/metadata" ) type Ledger struct { @@ -22,7 +20,7 @@ type Ledger struct { } func (l Ledger) HasFeature(feature, value string) bool { - if err := validateFeatureWithValue(feature, value); err != nil { + if err := features.ValidateFeatureWithValue(feature, value); err != nil { panic(err) } @@ -69,58 +67,10 @@ func MustNewWithDefault(name string) Ledger { } const ( - // FeatureMovesHistory is used to define if the ledger has to save funds movements history. - // Value is either ON or OFF - FeatureMovesHistory = "MOVES_HISTORY" - // FeatureMovesHistoryPostCommitEffectiveVolumes is used to define if the pvce property of funds movements history - // has to be updated with back dated transaction. - // Value is either SYNC or DISABLED. - // todo: depends on FeatureMovesHistory (dependency should be checked) - FeatureMovesHistoryPostCommitEffectiveVolumes = "MOVES_HISTORY_POST_COMMIT_EFFECTIVE_VOLUMES" - // FeatureHashLogs is used to defined it the logs has to be hashed. - FeatureHashLogs = "HASH_LOGS" - // FeatureAccountMetadataHistory is used to defined it the account metadata must be historized. - FeatureAccountMetadataHistory = "ACCOUNT_METADATA_HISTORY" - // FeatureTransactionMetadataHistory is used to defined it the transaction metadata must be historized. - FeatureTransactionMetadataHistory = "TRANSACTION_METADATA_HISTORY" - // FeatureIndexAddressSegments is used to defined it we want to index segments of accounts address. - // Without this feature, the ledger will not allow filtering on partial account address. - FeatureIndexAddressSegments = "INDEX_ADDRESS_SEGMENTS" - // FeatureIndexTransactionAccounts is used to defined it we want to index accounts used in a transaction. - FeatureIndexTransactionAccounts = "INDEX_TRANSACTION_ACCOUNTS" - DefaultBucket = "_default" ) var ( - DefaultFeatures = FeatureSet{ - FeatureMovesHistory: "ON", - FeatureMovesHistoryPostCommitEffectiveVolumes: "SYNC", - FeatureHashLogs: "SYNC", - FeatureAccountMetadataHistory: "SYNC", - FeatureTransactionMetadataHistory: "SYNC", - FeatureIndexAddressSegments: "ON", - FeatureIndexTransactionAccounts: "ON", - } - MinimalFeatureSet = FeatureSet{ - FeatureMovesHistory: "OFF", - FeatureMovesHistoryPostCommitEffectiveVolumes: "DISABLED", - FeatureHashLogs: "DISABLED", - FeatureAccountMetadataHistory: "DISABLED", - FeatureTransactionMetadataHistory: "DISABLED", - FeatureIndexAddressSegments: "OFF", - FeatureIndexTransactionAccounts: "OFF", - } - FeatureConfigurations = map[string][]string{ - FeatureMovesHistory: {"ON", "OFF"}, - FeatureMovesHistoryPostCommitEffectiveVolumes: {"SYNC", "DISABLED"}, - FeatureHashLogs: {"SYNC", "DISABLED"}, - FeatureAccountMetadataHistory: {"SYNC", "DISABLED"}, - FeatureTransactionMetadataHistory: {"SYNC", "DISABLED"}, - FeatureIndexAddressSegments: {"ON", "OFF"}, - FeatureIndexTransactionAccounts: {"ON", "OFF"}, - } - ledgerNameFormat = regexp.MustCompile("^[0-9a-zA-Z_-]{1,63}$") bucketNameFormat = regexp.MustCompile("^[0-9a-zA-Z_-]{1,63}$") @@ -132,70 +82,10 @@ var ( } ) -func validateFeatureWithValue(feature, value string) error { - possibleConfigurations, ok := FeatureConfigurations[feature] - if !ok { - return fmt.Errorf("feature %q not exists", feature) - } - if !slices.Contains(possibleConfigurations, value) { - return fmt.Errorf("configuration %s it not possible for feature %s", value, feature) - } - - return nil -} - -type FeatureSet map[string]string - -func (f FeatureSet) With(feature, value string) FeatureSet { - ret := FeatureSet{} - for k, v := range f { - ret[k] = v - } - ret[feature] = value - - return ret -} - -func (f FeatureSet) SortedKeys() []string { - ret := Keys(f) - slices.Sort(ret) - - return ret -} - -func (f FeatureSet) String() string { - if len(f) == 0 { - return "" - } - - ret := "" - for _, key := range f.SortedKeys() { - ret = ret + "," + shortenFeature(key) + "=" + f[key] - } - - return ret[1:] -} - -func (f FeatureSet) Match(features FeatureSet) bool { - for key, value := range features { - if f[key] != value { - return false - } - } - - return true -} - -func shortenFeature(feature string) string { - return strings.Join(Map(strings.Split(feature, "_"), func(from string) string { - return from[:1] - }), "") -} - type Configuration struct { - Bucket string `json:"bucket" bun:"bucket,type:varchar(255)"` - Metadata metadata.Metadata `json:"metadata" bun:"metadata,type:jsonb"` - Features FeatureSet `json:"features" bun:"features,type:jsonb"` + Bucket string `json:"bucket" bun:"bucket,type:varchar(255)"` + Metadata metadata.Metadata `json:"metadata" bun:"metadata,type:jsonb"` + Features features.FeatureSet `json:"features" bun:"features,type:jsonb"` } func (c *Configuration) SetDefaults() { @@ -206,7 +96,7 @@ func (c *Configuration) SetDefaults() { c.Features = map[string]string{} } - for key, value := range DefaultFeatures { + for key, value := range features.DefaultFeatures { if _, ok := c.Features[key]; !ok { c.Features[key] = value } @@ -215,7 +105,7 @@ func (c *Configuration) SetDefaults() { func (c *Configuration) Validate() error { for feature, value := range c.Features { - if err := validateFeatureWithValue(feature, value); err != nil { + if err := features.ValidateFeatureWithValue(feature, value); err != nil { return err } } @@ -227,6 +117,6 @@ func NewDefaultConfiguration() Configuration { return Configuration{ Bucket: DefaultBucket, Metadata: metadata.Metadata{}, - Features: DefaultFeatures, + Features: features.DefaultFeatures, } } diff --git a/internal/ledger_test.go b/internal/ledger_test.go index 91c6534a8..588e6821d 100644 --- a/internal/ledger_test.go +++ b/internal/ledger_test.go @@ -1,12 +1,13 @@ package ledger import ( + "github.com/formancehq/ledger/pkg/features" "github.com/stretchr/testify/require" "testing" ) func TestFeatures(t *testing.T) { - f := MinimalFeatureSet.With(FeatureMovesHistory, "DISABLED") - require.Equal(t, "DISABLED", f[FeatureMovesHistory]) + f := features.MinimalFeatureSet.With(features.FeatureMovesHistory, "DISABLED") + require.Equal(t, "DISABLED", f[features.FeatureMovesHistory]) require.Equal(t, "AMH=DISABLED,HL=DISABLED,IAS=OFF,ITA=OFF,MH=DISABLED,MHPCEV=DISABLED,TMH=DISABLED", f.String()) } diff --git a/internal/storage/bucket/bucket.go b/internal/storage/bucket/bucket.go index fd6e5422a..2570750e2 100644 --- a/internal/storage/bucket/bucket.go +++ b/internal/storage/bucket/bucket.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/formancehq/go-libs/v2/migrations" ledger "github.com/formancehq/ledger/internal" + "github.com/formancehq/ledger/pkg/features" "github.com/uptrace/bun" "go.opentelemetry.io/otel/trace" "text/template" @@ -66,8 +67,8 @@ func New(db *bun.DB, name string) *Bucket { } type ledgerSetup struct { - requireFeatures ledger.FeatureSet - script string + requireFeatures features.FeatureSet + script string } var ledgerSetups = []ledgerSetup{ @@ -96,14 +97,14 @@ var ledgerSetups = []ledgerSetup{ `, }, { - requireFeatures: ledger.FeatureSet{ - ledger.FeatureMovesHistoryPostCommitEffectiveVolumes: "SYNC", + requireFeatures: features.FeatureSet{ + features.FeatureMovesHistoryPostCommitEffectiveVolumes: "SYNC", }, script: `create index "pcev_{{.ID}}" on "{{.Bucket}}".moves (accounts_address, asset, effective_date desc) where ledger = '{{.Name}}';`, }, { - requireFeatures: ledger.FeatureSet{ - ledger.FeatureMovesHistoryPostCommitEffectiveVolumes: "SYNC", + requireFeatures: features.FeatureSet{ + features.FeatureMovesHistoryPostCommitEffectiveVolumes: "SYNC", }, script: ` create trigger "set_effective_volumes_{{.ID}}" @@ -117,8 +118,8 @@ var ledgerSetups = []ledgerSetup{ `, }, { - requireFeatures: ledger.FeatureSet{ - ledger.FeatureMovesHistoryPostCommitEffectiveVolumes: "SYNC", + requireFeatures: features.FeatureSet{ + features.FeatureMovesHistoryPostCommitEffectiveVolumes: "SYNC", }, script: ` create trigger "update_effective_volumes_{{.ID}}" @@ -132,8 +133,8 @@ var ledgerSetups = []ledgerSetup{ `, }, { - requireFeatures: ledger.FeatureSet{ - ledger.FeatureHashLogs: "SYNC", + requireFeatures: features.FeatureSet{ + features.FeatureHashLogs: "SYNC", }, script: ` create trigger "set_log_hash_{{.ID}}" @@ -147,8 +148,8 @@ var ledgerSetups = []ledgerSetup{ `, }, { - requireFeatures: ledger.FeatureSet{ - ledger.FeatureAccountMetadataHistory: "SYNC", + requireFeatures: features.FeatureSet{ + features.FeatureAccountMetadataHistory: "SYNC", }, script: ` create trigger "update_account_metadata_history_{{.ID}}" @@ -162,8 +163,8 @@ var ledgerSetups = []ledgerSetup{ `, }, { - requireFeatures: ledger.FeatureSet{ - ledger.FeatureAccountMetadataHistory: "SYNC", + requireFeatures: features.FeatureSet{ + features.FeatureAccountMetadataHistory: "SYNC", }, script: ` create trigger "insert_account_metadata_history_{{.ID}}" @@ -177,8 +178,8 @@ var ledgerSetups = []ledgerSetup{ `, }, { - requireFeatures: ledger.FeatureSet{ - ledger.FeatureTransactionMetadataHistory: "SYNC", + requireFeatures: features.FeatureSet{ + features.FeatureTransactionMetadataHistory: "SYNC", }, script: ` create trigger "update_transaction_metadata_history_{{.ID}}" @@ -192,8 +193,8 @@ var ledgerSetups = []ledgerSetup{ `, }, { - requireFeatures: ledger.FeatureSet{ - ledger.FeatureTransactionMetadataHistory: "SYNC", + requireFeatures: features.FeatureSet{ + features.FeatureTransactionMetadataHistory: "SYNC", }, script: ` create trigger "insert_transaction_metadata_history_{{.ID}}" @@ -207,24 +208,24 @@ var ledgerSetups = []ledgerSetup{ `, }, { - requireFeatures: ledger.FeatureSet{ - ledger.FeatureIndexTransactionAccounts: "SYNC", + requireFeatures: features.FeatureSet{ + features.FeatureIndexTransactionAccounts: "SYNC", }, script: ` create index "transactions_sources_{{.ID}}" on "{{.Bucket}}".transactions using gin (sources jsonb_path_ops) where ledger = '{{.Name}}'; `, }, { - requireFeatures: ledger.FeatureSet{ - ledger.FeatureIndexTransactionAccounts: "ON", + requireFeatures: features.FeatureSet{ + features.FeatureIndexTransactionAccounts: "ON", }, script: ` create index "transactions_destinations_{{.ID}}" on "{{.Bucket}}".transactions using gin (destinations jsonb_path_ops) where ledger = '{{.Name}}'; `, }, { - requireFeatures: ledger.FeatureSet{ - ledger.FeatureIndexTransactionAccounts: "ON", + requireFeatures: features.FeatureSet{ + features.FeatureIndexTransactionAccounts: "ON", }, script: ` create trigger "transaction_set_addresses_{{.ID}}" @@ -238,24 +239,24 @@ var ledgerSetups = []ledgerSetup{ `, }, { - requireFeatures: ledger.FeatureSet{ - ledger.FeatureIndexAddressSegments: "ON", + requireFeatures: features.FeatureSet{ + features.FeatureIndexAddressSegments: "ON", }, script: ` create index "accounts_address_array_{{.ID}}" on "{{.Bucket}}".accounts using gin (address_array jsonb_ops) where ledger = '{{.Name}}'; `, }, { - requireFeatures: ledger.FeatureSet{ - ledger.FeatureIndexAddressSegments: "ON", + requireFeatures: features.FeatureSet{ + features.FeatureIndexAddressSegments: "ON", }, script: ` create index "accounts_address_array_length_{{.ID}}" on "{{.Bucket}}".accounts (jsonb_array_length(address_array)) where ledger = '{{.Name}}'; `, }, { - requireFeatures: ledger.FeatureSet{ - ledger.FeatureIndexAddressSegments: "ON", + requireFeatures: features.FeatureSet{ + features.FeatureIndexAddressSegments: "ON", }, script: ` create trigger "accounts_set_address_array_{{.ID}}" @@ -269,27 +270,27 @@ var ledgerSetups = []ledgerSetup{ `, }, { - requireFeatures: ledger.FeatureSet{ - ledger.FeatureIndexAddressSegments: "ON", - ledger.FeatureIndexTransactionAccounts: "ON", + requireFeatures: features.FeatureSet{ + features.FeatureIndexAddressSegments: "ON", + features.FeatureIndexTransactionAccounts: "ON", }, script: ` create index "transactions_sources_arrays_{{.ID}}" on "{{.Bucket}}".transactions using gin (sources_arrays jsonb_path_ops) where ledger = '{{.Name}}'; `, }, { - requireFeatures: ledger.FeatureSet{ - ledger.FeatureIndexAddressSegments: "ON", - ledger.FeatureIndexTransactionAccounts: "ON", + requireFeatures: features.FeatureSet{ + features.FeatureIndexAddressSegments: "ON", + features.FeatureIndexTransactionAccounts: "ON", }, script: ` create index "transactions_destinations_arrays_{{.ID}}" on "{{.Bucket}}".transactions using gin (destinations_arrays jsonb_path_ops) where ledger = '{{.Name}}'; `, }, { - requireFeatures: ledger.FeatureSet{ - ledger.FeatureIndexAddressSegments: "ON", - ledger.FeatureIndexTransactionAccounts: "ON", + requireFeatures: features.FeatureSet{ + features.FeatureIndexAddressSegments: "ON", + features.FeatureIndexTransactionAccounts: "ON", }, script: ` create trigger "transaction_set_addresses_segments_{{.ID}}" @@ -302,4 +303,4 @@ var ledgerSetups = []ledgerSetup{ execute procedure "{{.Bucket}}".set_transaction_addresses_segments(); `, }, -} \ No newline at end of file +} diff --git a/internal/storage/ledger/accounts.go b/internal/storage/ledger/accounts.go index f95e1837d..1e34ca469 100644 --- a/internal/storage/ledger/accounts.go +++ b/internal/storage/ledger/accounts.go @@ -5,6 +5,7 @@ import ( "database/sql" "fmt" . "github.com/formancehq/go-libs/v2/bun/bunpaginate" + "github.com/formancehq/ledger/pkg/features" "regexp" "github.com/formancehq/ledger/internal/tracing" @@ -119,7 +120,7 @@ func (s *Store) selectAccounts(date *time.Time, expandVolumes, expandEffectiveVo ret = ret.Where("accounts.first_usage <= ?", date) } - if s.ledger.HasFeature(ledger.FeatureAccountMetadataHistory, "SYNC") && date != nil && !date.IsZero() { + if s.ledger.HasFeature(features.FeatureAccountMetadataHistory, "SYNC") && date != nil && !date.IsZero() { ret = ret. Join( `left join (?) accounts_metadata on accounts_metadata.accounts_address = accounts.address`, @@ -130,14 +131,14 @@ func (s *Store) selectAccounts(date *time.Time, expandVolumes, expandEffectiveVo ret = ret.ColumnExpr("accounts.metadata") } - if s.ledger.HasFeature(ledger.FeatureMovesHistory, "ON") && needVolumes { + if s.ledger.HasFeature(features.FeatureMovesHistory, "ON") && needVolumes { ret = ret.Join( `left join (?) volumes on volumes.accounts_address = accounts.address`, s.selectAccountWithAggregatedVolumes(date, true, "volumes"), ).Column("volumes.*") } - if s.ledger.HasFeature(ledger.FeatureMovesHistoryPostCommitEffectiveVolumes, "SYNC") && expandEffectiveVolumes { + if s.ledger.HasFeature(features.FeatureMovesHistoryPostCommitEffectiveVolumes, "SYNC") && expandEffectiveVolumes { ret = ret.Join( `left join (?) effective_volumes on effective_volumes.accounts_address = accounts.address`, s.selectAccountWithAggregatedVolumes(date, false, "effective_volumes"), @@ -176,7 +177,7 @@ func (s *Store) selectAccounts(date *time.Time, expandVolumes, expandEffectiveVo String(), nil, nil case key == "metadata": - if s.ledger.HasFeature(ledger.FeatureAccountMetadataHistory, "SYNC") && date != nil && !date.IsZero() { + if s.ledger.HasFeature(features.FeatureAccountMetadataHistory, "SYNC") && date != nil && !date.IsZero() { key = "accounts_metadata.metadata" } @@ -184,7 +185,7 @@ func (s *Store) selectAccounts(date *time.Time, expandVolumes, expandEffectiveVo case metadataRegex.Match([]byte(key)): match := metadataRegex.FindAllStringSubmatch(key, 3) - if s.ledger.HasFeature(ledger.FeatureAccountMetadataHistory, "SYNC") && date != nil && !date.IsZero() { + if s.ledger.HasFeature(features.FeatureAccountMetadataHistory, "SYNC") && date != nil && !date.IsZero() { key = "accounts_metadata.metadata" } else { key = "metadata" diff --git a/internal/storage/ledger/balances.go b/internal/storage/ledger/balances.go index cd9f97a4e..442c984dc 100644 --- a/internal/storage/ledger/balances.go +++ b/internal/storage/ledger/balances.go @@ -3,6 +3,7 @@ package ledger import ( "context" "fmt" + "github.com/formancehq/ledger/pkg/features" "math/big" "strings" @@ -56,23 +57,23 @@ func (s *Store) selectAccountWithAssetAndVolumes(date *time.Time, useInsertionDa } } - if needAddressSegment && !s.ledger.HasFeature(ledger.FeatureIndexAddressSegments, "ON") { - return ret.Err(ledgercontroller.NewErrMissingFeature(ledger.FeatureIndexAddressSegments)) + if needAddressSegment && !s.ledger.HasFeature(features.FeatureIndexAddressSegments, "ON") { + return ret.Err(ledgercontroller.NewErrMissingFeature(features.FeatureIndexAddressSegments)) } var selectAccountsWithVolumes *bun.SelectQuery if date != nil && !date.IsZero() { if useInsertionDate { - if !s.ledger.HasFeature(ledger.FeatureMovesHistory, "ON") { - return ret.Err(ledgercontroller.NewErrMissingFeature(ledger.FeatureMovesHistory)) + if !s.ledger.HasFeature(features.FeatureMovesHistory, "ON") { + return ret.Err(ledgercontroller.NewErrMissingFeature(features.FeatureMovesHistory)) } selectAccountsWithVolumes = s.db.NewSelect(). TableExpr("(?) moves", s.SelectDistinctMovesBySeq(date)). Column("asset", "accounts_address"). ColumnExpr("post_commit_volumes as volumes") } else { - if !s.ledger.HasFeature(ledger.FeatureMovesHistoryPostCommitEffectiveVolumes, "SYNC") { - return ret.Err(ledgercontroller.NewErrMissingFeature(ledger.FeatureMovesHistoryPostCommitEffectiveVolumes)) + if !s.ledger.HasFeature(features.FeatureMovesHistoryPostCommitEffectiveVolumes, "SYNC") { + return ret.Err(ledgercontroller.NewErrMissingFeature(features.FeatureMovesHistoryPostCommitEffectiveVolumes)) } selectAccountsWithVolumes = s.db.NewSelect(). TableExpr("(?) moves", s.SelectDistinctMovesByEffectiveDate(date)). @@ -92,7 +93,7 @@ func (s *Store) selectAccountWithAssetAndVolumes(date *time.Time, useInsertionDa TableExpr("(?) accounts_volumes", selectAccountsWithVolumes) if needMetadata { - if s.ledger.HasFeature(ledger.FeatureAccountMetadataHistory, "SYNC") && date != nil && !date.IsZero() { + if s.ledger.HasFeature(features.FeatureAccountMetadataHistory, "SYNC") && date != nil && !date.IsZero() { selectAccountsWithVolumes = selectAccountsWithVolumes. Join( `left join (?) accounts_metadata on accounts_metadata.accounts_address = accounts_volumes.accounts_address`, diff --git a/internal/storage/ledger/logs.go b/internal/storage/ledger/logs.go index 132ae8bfc..9d18352aa 100644 --- a/internal/storage/ledger/logs.go +++ b/internal/storage/ledger/logs.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "github.com/formancehq/ledger/internal/tracing" + "github.com/formancehq/ledger/pkg/features" "errors" "github.com/formancehq/go-libs/v2/bun/bunpaginate" @@ -55,7 +56,7 @@ func (s *Store) InsertLog(ctx context.Context, log *ledger.Log) error { tracing.NoResult(func(ctx context.Context) error { // We lock logs table as we need than the last log does not change until the transaction commit - if s.ledger.HasFeature(ledger.FeatureHashLogs, "SYNC") { + if s.ledger.HasFeature(features.FeatureHashLogs, "SYNC") { _, err := s.db.NewRaw(`select pg_advisory_xact_lock(?)`, s.ledger.ID).Exec(ctx) if err != nil { return postgres.ResolveError(err) diff --git a/internal/storage/ledger/moves.go b/internal/storage/ledger/moves.go index 7198cd9b5..2c32228c2 100644 --- a/internal/storage/ledger/moves.go +++ b/internal/storage/ledger/moves.go @@ -3,6 +3,7 @@ package ledger import ( "context" ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" + "github.com/formancehq/ledger/pkg/features" "github.com/formancehq/go-libs/v2/platform/postgres" "github.com/formancehq/go-libs/v2/time" @@ -14,8 +15,8 @@ import ( func (s *Store) SortMovesBySeq(date *time.Time) *bun.SelectQuery { ret := s.db.NewSelect() - if !s.ledger.HasFeature(ledger.FeatureMovesHistory, "ON") { - return ret.Err(ledgercontroller.NewErrMissingFeature(ledger.FeatureMovesHistory)) + if !s.ledger.HasFeature(features.FeatureMovesHistory, "ON") { + return ret.Err(ledgercontroller.NewErrMissingFeature(features.FeatureMovesHistory)) } ret = ret. diff --git a/internal/storage/ledger/store.go b/internal/storage/ledger/store.go index 26d2f6e05..ed9bfb6d3 100644 --- a/internal/storage/ledger/store.go +++ b/internal/storage/ledger/store.go @@ -6,6 +6,7 @@ import ( "github.com/formancehq/go-libs/v2/migrations" "github.com/formancehq/go-libs/v2/platform/postgres" "github.com/formancehq/ledger/internal/storage/bucket" + "github.com/formancehq/ledger/pkg/features" "go.opentelemetry.io/otel/metric" noopmetrics "go.opentelemetry.io/otel/metric/noop" "go.opentelemetry.io/otel/trace" @@ -71,8 +72,8 @@ func (s *Store) validateAddressFilter(operator string, value any) error { } if value, ok := value.(string); !ok { return fmt.Errorf("invalid 'address' filter") - } else if isSegmentedAddress(value) && !s.ledger.HasFeature(ledger.FeatureIndexAddressSegments, "ON") { - return fmt.Errorf("feature %s must be 'ON' to use segments address", ledger.FeatureIndexAddressSegments) + } else if isSegmentedAddress(value) && !s.ledger.HasFeature(features.FeatureIndexAddressSegments, "ON") { + return fmt.Errorf("feature %s must be 'ON' to use segments address", features.FeatureIndexAddressSegments) } return nil diff --git a/internal/storage/ledger/transactions.go b/internal/storage/ledger/transactions.go index 03aefb05c..6b49413b4 100644 --- a/internal/storage/ledger/transactions.go +++ b/internal/storage/ledger/transactions.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/formancehq/ledger/pkg/features" "math/big" "regexp" "slices" @@ -51,8 +52,8 @@ func (s *Store) selectDistinctTransactionMetadataHistories(date *time.Time) *bun func (s *Store) selectTransactions(date *time.Time, expandVolumes, expandEffectiveVolumes bool, q query.Builder) *bun.SelectQuery { ret := s.db.NewSelect() - if expandEffectiveVolumes && !s.ledger.HasFeature(ledger.FeatureMovesHistoryPostCommitEffectiveVolumes, "SYNC") { - return ret.Err(ledgercontroller.NewErrMissingFeature(ledger.FeatureMovesHistoryPostCommitEffectiveVolumes)) + if expandEffectiveVolumes && !s.ledger.HasFeature(features.FeatureMovesHistoryPostCommitEffectiveVolumes, "SYNC") { + return ret.Err(ledgercontroller.NewErrMissingFeature(features.FeatureMovesHistoryPostCommitEffectiveVolumes)) } if q != nil { @@ -116,7 +117,7 @@ func (s *Store) selectTransactions(date *time.Time, expandVolumes, expandEffecti ret = ret.Where("timestamp <= ?", date) } - if s.ledger.HasFeature(ledger.FeatureAccountMetadataHistory, "SYNC") && date != nil && !date.IsZero() { + if s.ledger.HasFeature(features.FeatureAccountMetadataHistory, "SYNC") && date != nil && !date.IsZero() { ret = ret. Join( `left join (?) transactions_metadata on transactions_metadata.transactions_id = transactions.id`, @@ -127,7 +128,7 @@ func (s *Store) selectTransactions(date *time.Time, expandVolumes, expandEffecti ret = ret.ColumnExpr("metadata") } - if s.ledger.HasFeature(ledger.FeatureMovesHistoryPostCommitEffectiveVolumes, "SYNC") && expandEffectiveVolumes { + if s.ledger.HasFeature(features.FeatureMovesHistoryPostCommitEffectiveVolumes, "SYNC") && expandEffectiveVolumes { ret = ret. Join( `join (?) pcev on pcev.transactions_id = transactions.id`, @@ -264,7 +265,7 @@ func (s *Store) CommitTransaction(ctx context.Context, tx *ledger.Transaction) e } } - if s.ledger.HasFeature(ledger.FeatureMovesHistory, "ON") { + if s.ledger.HasFeature(features.FeatureMovesHistory, "ON") { moves := ledger.Moves{} postings := tx.Postings slices.Reverse(postings) @@ -300,7 +301,7 @@ func (s *Store) CommitTransaction(ctx context.Context, tx *ledger.Transaction) e return fmt.Errorf("failed to insert moves: %w", err) } - if s.ledger.HasFeature(ledger.FeatureMovesHistoryPostCommitEffectiveVolumes, "SYNC") { + if s.ledger.HasFeature(features.FeatureMovesHistoryPostCommitEffectiveVolumes, "SYNC") { // todo: tx is inserted earlier! tx.PostCommitEffectiveVolumes = moves.ComputePostCommitEffectiveVolumes() } diff --git a/internal/storage/ledger/volumes.go b/internal/storage/ledger/volumes.go index aa8806108..c0a8644cf 100644 --- a/internal/storage/ledger/volumes.go +++ b/internal/storage/ledger/volumes.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/formancehq/go-libs/v2/collectionutils" "github.com/formancehq/go-libs/v2/platform/postgres" + "github.com/formancehq/ledger/pkg/features" "github.com/formancehq/ledger/internal/tracing" @@ -67,8 +68,8 @@ func (s *Store) UpdateVolumes(ctx context.Context, accountVolumes ...ledger.Acco func (s *Store) selectVolumes(oot, pit *time.Time, useInsertionDate bool, groupLevel int, q lquery.Builder) *bun.SelectQuery { ret := s.db.NewSelect() - if !s.ledger.HasFeature(ledger.FeatureMovesHistory, "ON") { - return ret.Err(ledgercontroller.NewErrMissingFeature(ledger.FeatureMovesHistory)) + if !s.ledger.HasFeature(features.FeatureMovesHistory, "ON") { + return ret.Err(ledgercontroller.NewErrMissingFeature(features.FeatureMovesHistory)) } var ( diff --git a/internal/storage/module.go b/internal/storage/module.go index b0aa35370..fd524c8f0 100644 --- a/internal/storage/module.go +++ b/internal/storage/module.go @@ -16,9 +16,9 @@ func NewFXModule(autoUpgrade bool) fx.Option { ret = append(ret, fx.Invoke(func(lc fx.Lifecycle, driver *driver.Driver) { var ( - upgradeContext context.Context - cancelContext func() - upgradeStopped = make(chan struct{}) + upgradeContext context.Context + cancelContext func() + upgradeStopped = make(chan struct{}) minimalVersionReached = make(chan struct{}) ) lc.Append(fx.Hook{ @@ -77,4 +77,4 @@ func migrate(ctx context.Context, driver *driver.Driver, minimalVersionReached c return } } -} \ No newline at end of file +} diff --git a/openapi.yaml b/openapi.yaml index 81042b8ec..181026535 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1208,6 +1208,30 @@ paths: security: - Authorization: - ledger:read + /_/metrics: + get: + tags: + - ledger + summary: Read in memory metrics + operationId: getMetrics + x-speakeasy-name-override: GetMetrics + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + additionalProperties: {} + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/V2ErrorResponse' + security: + - Authorization: + - ledger:read /v2: get: summary: List ledgers diff --git a/openapi/v2.yaml b/openapi/v2.yaml index 137eb09c8..194e2c331 100644 --- a/openapi/v2.yaml +++ b/openapi/v2.yaml @@ -35,6 +35,30 @@ paths: security: - Authorization: - ledger:read + /_/metrics: + get: + tags: + - ledger + summary: Read in memory metrics + operationId: getMetrics + x-speakeasy-name-override: GetMetrics + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + additionalProperties: {} + default: + description: Error + content: + application/json: + schema: + $ref: "#/components/schemas/V2ErrorResponse" + security: + - Authorization: + - ledger:read /v2: get: summary: List ledgers diff --git a/pkg/client/.speakeasy/gen.lock b/pkg/client/.speakeasy/gen.lock index 19f5d974f..960ab7ad2 100644 --- a/pkg/client/.speakeasy/gen.lock +++ b/pkg/client/.speakeasy/gen.lock @@ -1,12 +1,12 @@ lockVersion: 2.0.0 id: a9ac79e1-e429-4ee3-96c4-ec973f19bec3 management: - docChecksum: 5e24fc96851e508606f6b6668ed3ffb3 + docChecksum: 743c41071ff98ac5e7d00e58f65650e3 docVersion: v1 speakeasyVersion: 1.351.0 generationVersion: 2.384.1 - releaseVersion: 0.4.30 - configChecksum: b752be0bf02f575b11e046541aa57005 + releaseVersion: 0.4.31 + configChecksum: 006527a5b70ea037906b0ff29a01a2ef features: go: additionalDependencies: 0.1.0 @@ -55,6 +55,7 @@ generatedFiles: - internal/utils/security.go - internal/utils/utils.go - /models/operations/v2getinfo.go + - /models/operations/getmetrics.go - /models/operations/getinfo.go - /models/operations/getledgerinfo.go - /models/operations/countaccounts.go @@ -173,6 +174,7 @@ generatedFiles: - /models/components/v2log.go - /models/components/security.go - docs/models/operations/v2getinforesponse.md + - docs/models/operations/getmetricsresponse.md - docs/models/operations/getinforesponse.md - docs/models/operations/getledgerinforequest.md - docs/models/operations/getledgerinforesponse.md diff --git a/pkg/client/.speakeasy/gen.yaml b/pkg/client/.speakeasy/gen.yaml index 93655e172..965163ad3 100644 --- a/pkg/client/.speakeasy/gen.yaml +++ b/pkg/client/.speakeasy/gen.yaml @@ -15,7 +15,7 @@ generation: auth: oAuth2ClientCredentialsEnabled: true go: - version: 0.4.30 + version: 0.4.31 additionalDependencies: {} allowUnknownFieldsInWeakUnions: false clientServerStatusCodesAsErrors: true diff --git a/pkg/client/README.md b/pkg/client/README.md index 4d6bc2c4c..613553c69 100644 --- a/pkg/client/README.md +++ b/pkg/client/README.md @@ -87,6 +87,7 @@ func main() { ### [Ledger](docs/sdks/ledger/README.md) * [GetInfo](docs/sdks/ledger/README.md#getinfo) - Show server information +* [GetMetrics](docs/sdks/ledger/README.md#getmetrics) - Read in memory metrics ### [Ledger.V1](docs/sdks/v1/README.md) diff --git a/pkg/client/docs/models/operations/getmetricsresponse.md b/pkg/client/docs/models/operations/getmetricsresponse.md new file mode 100644 index 000000000..721394c72 --- /dev/null +++ b/pkg/client/docs/models/operations/getmetricsresponse.md @@ -0,0 +1,9 @@ +# GetMetricsResponse + + +## Fields + +| Field | Type | Required | Description | +| ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | +| `HTTPMeta` | [components.HTTPMetadata](../../models/components/httpmetadata.md) | :heavy_check_mark: | N/A | +| `Object` | map[string]*any* | :heavy_minus_sign: | OK | \ No newline at end of file diff --git a/pkg/client/docs/sdks/ledger/README.md b/pkg/client/docs/sdks/ledger/README.md index a0d4fc64e..4c46b7a04 100644 --- a/pkg/client/docs/sdks/ledger/README.md +++ b/pkg/client/docs/sdks/ledger/README.md @@ -4,6 +4,7 @@ ### Available Operations * [GetInfo](#getinfo) - Show server information +* [GetMetrics](#getmetrics) - Read in memory metrics ## GetInfo @@ -55,3 +56,54 @@ func main() { | ------------------------- | ------------------------- | ------------------------- | | sdkerrors.V2ErrorResponse | default | application/json | | sdkerrors.SDKError | 4xx-5xx | */* | + +## GetMetrics + +Read in memory metrics + +### Example Usage + +```go +package main + +import( + "github.com/formancehq/ledger/pkg/client/models/components" + "github.com/formancehq/ledger/pkg/client" + "context" + "log" +) + +func main() { + s := client.New( + client.WithSecurity(components.Security{ + ClientID: "", + ClientSecret: "", + }), + ) + + ctx := context.Background() + res, err := s.Ledger.GetMetrics(ctx) + if err != nil { + log.Fatal(err) + } + if res.Object != 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. | +| `opts` | [][operations.Option](../../models/operations/option.md) | :heavy_minus_sign: | The options for this request. | + + +### Response + +**[*operations.GetMetricsResponse](../../models/operations/getmetricsresponse.md), error** +| Error Object | Status Code | Content Type | +| ------------------------- | ------------------------- | ------------------------- | +| sdkerrors.V2ErrorResponse | default | application/json | +| sdkerrors.SDKError | 4xx-5xx | */* | diff --git a/pkg/client/formance.go b/pkg/client/formance.go index 11a85fa6d..b6ec8384d 100644 --- a/pkg/client/formance.go +++ b/pkg/client/formance.go @@ -143,9 +143,9 @@ func New(opts ...SDKOption) *Formance { sdkConfiguration: sdkConfiguration{ Language: "go", OpenAPIDocVersion: "v1", - SDKVersion: "0.4.30", + SDKVersion: "0.4.31", GenVersion: "2.384.1", - UserAgent: "speakeasy-sdk/go 0.4.30 2.384.1 v1 github.com/formancehq/ledger/pkg/client", + UserAgent: "speakeasy-sdk/go 0.4.31 2.384.1 v1 github.com/formancehq/ledger/pkg/client", Hooks: hooks.New(), }, } diff --git a/pkg/client/ledger.go b/pkg/client/ledger.go index 621bbfe98..a3af51ae0 100644 --- a/pkg/client/ledger.go +++ b/pkg/client/ledger.go @@ -222,3 +222,182 @@ func (s *Ledger) GetInfo(ctx context.Context, opts ...operations.Option) (*opera return res, nil } + +// GetMetrics - Read in memory metrics +func (s *Ledger) GetMetrics(ctx context.Context, opts ...operations.Option) (*operations.GetMetricsResponse, error) { + hookCtx := hooks.HookContext{ + Context: ctx, + OperationID: "getMetrics", + OAuth2Scopes: []string{"ledger:read", "ledger:read"}, + SecuritySource: s.sdkConfiguration.Security, + } + + 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) + } + } + + baseURL := utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + opURL, err := url.JoinPath(baseURL, "/_/metrics") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", 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, "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 + } + + 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 { + copyBody, err := req.GetBody() + if err != nil { + return nil, err + } + req.Body = copyBody + } + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, backoff.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.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + } + return httpRes, err + }) + + if err != nil { + return nil, err + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } else { + req, err = s.sdkConfiguration.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.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"default"}, httpRes.StatusCode) { + _httpRes, err := s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } + + res := &operations.GetMetricsResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + rawBody, err := io.ReadAll(httpRes.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %w", err) + } + httpRes.Body.Close() + httpRes.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + + switch { + case httpRes.StatusCode == 200: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + var out map[string]any + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.Object = out + default: + 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`): + var out sdkerrors.V2ErrorResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + return nil, &out + default: + return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + } + + return res, nil + +} diff --git a/pkg/client/models/operations/getmetrics.go b/pkg/client/models/operations/getmetrics.go new file mode 100644 index 000000000..ea68147d5 --- /dev/null +++ b/pkg/client/models/operations/getmetrics.go @@ -0,0 +1,27 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/formancehq/ledger/pkg/client/models/components" +) + +type GetMetricsResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // OK + Object map[string]any +} + +func (o *GetMetricsResponse) GetHTTPMeta() components.HTTPMetadata { + if o == nil { + return components.HTTPMetadata{} + } + return o.HTTPMeta +} + +func (o *GetMetricsResponse) GetObject() map[string]any { + if o == nil { + return nil + } + return o.Object +} diff --git a/pkg/features/features.go b/pkg/features/features.go new file mode 100644 index 000000000..bf8bb401d --- /dev/null +++ b/pkg/features/features.go @@ -0,0 +1,119 @@ +package features + +import ( + "fmt" + "github.com/formancehq/go-libs/v2/collectionutils" + "slices" + "strings" +) + +const ( + // FeatureMovesHistory is used to define if the ledger has to save funds movements history. + // Value is either ON or OFF + FeatureMovesHistory = "MOVES_HISTORY" + // FeatureMovesHistoryPostCommitEffectiveVolumes is used to define if the pvce property of funds movements history + // has to be updated with back dated transaction. + // Value is either SYNC or DISABLED. + // todo: depends on FeatureMovesHistory (dependency should be checked) + FeatureMovesHistoryPostCommitEffectiveVolumes = "MOVES_HISTORY_POST_COMMIT_EFFECTIVE_VOLUMES" + // FeatureHashLogs is used to defined it the logs has to be hashed. + FeatureHashLogs = "HASH_LOGS" + // FeatureAccountMetadataHistory is used to defined it the account metadata must be historized. + FeatureAccountMetadataHistory = "ACCOUNT_METADATA_HISTORY" + // FeatureTransactionMetadataHistory is used to defined it the transaction metadata must be historized. + FeatureTransactionMetadataHistory = "TRANSACTION_METADATA_HISTORY" + // FeatureIndexAddressSegments is used to defined it we want to index segments of accounts address. + // Without this feature, the ledger will not allow filtering on partial account address. + FeatureIndexAddressSegments = "INDEX_ADDRESS_SEGMENTS" + // FeatureIndexTransactionAccounts is used to defined it we want to index accounts used in a transaction. + FeatureIndexTransactionAccounts = "INDEX_TRANSACTION_ACCOUNTS" +) + +var ( + DefaultFeatures = FeatureSet{ + FeatureMovesHistory: "ON", + FeatureMovesHistoryPostCommitEffectiveVolumes: "SYNC", + FeatureHashLogs: "SYNC", + FeatureAccountMetadataHistory: "SYNC", + FeatureTransactionMetadataHistory: "SYNC", + FeatureIndexAddressSegments: "ON", + FeatureIndexTransactionAccounts: "ON", + } + MinimalFeatureSet = FeatureSet{ + FeatureMovesHistory: "OFF", + FeatureMovesHistoryPostCommitEffectiveVolumes: "DISABLED", + FeatureHashLogs: "DISABLED", + FeatureAccountMetadataHistory: "DISABLED", + FeatureTransactionMetadataHistory: "DISABLED", + FeatureIndexAddressSegments: "OFF", + FeatureIndexTransactionAccounts: "OFF", + } + FeatureConfigurations = map[string][]string{ + FeatureMovesHistory: {"ON", "OFF"}, + FeatureMovesHistoryPostCommitEffectiveVolumes: {"SYNC", "DISABLED"}, + FeatureHashLogs: {"SYNC", "DISABLED"}, + FeatureAccountMetadataHistory: {"SYNC", "DISABLED"}, + FeatureTransactionMetadataHistory: {"SYNC", "DISABLED"}, + FeatureIndexAddressSegments: {"ON", "OFF"}, + FeatureIndexTransactionAccounts: {"ON", "OFF"}, + } +) + +func ValidateFeatureWithValue(feature, value string) error { + possibleConfigurations, ok := FeatureConfigurations[feature] + if !ok { + return fmt.Errorf("feature %q not exists", feature) + } + if !slices.Contains(possibleConfigurations, value) { + return fmt.Errorf("configuration %s it not possible for feature %s", value, feature) + } + + return nil +} + +type FeatureSet map[string]string + +func (f FeatureSet) With(feature, value string) FeatureSet { + ret := FeatureSet{} + for k, v := range f { + ret[k] = v + } + ret[feature] = value + + return ret +} + +func (f FeatureSet) SortedKeys() []string { + ret := collectionutils.Keys(f) + slices.Sort(ret) + + return ret +} + +func (f FeatureSet) String() string { + if len(f) == 0 { + return "" + } + + ret := "" + for _, key := range f.SortedKeys() { + ret = ret + "," + shortenFeature(key) + "=" + f[key] + } + + return ret[1:] +} + +func (f FeatureSet) Match(features FeatureSet) bool { + for k, v := range features { + if f[k] != v { + return false + } + } + return true +} + +func shortenFeature(feature string) string { + return strings.Join(collectionutils.Map(strings.Split(feature, "_"), func(from string) string { + return from[:1] + }), "") +} diff --git a/pkg/generate/generator.go b/pkg/generate/generator.go index 11a718c0c..5681c6950 100644 --- a/pkg/generate/generator.go +++ b/pkg/generate/generator.go @@ -191,11 +191,6 @@ func NewGenerator(script string, opts ...Option) (*Generator, error) { runtime := goja.New() - _, err := runtime.RunString(script) - if err != nil { - return nil, err - } - for k, v := range cfg.globals { err := runtime.Set(k, v) if err != nil { @@ -203,6 +198,11 @@ func NewGenerator(script string, opts ...Option) (*Generator, error) { } } + _, err := runtime.RunString(script) + if err != nil { + return nil, err + } + runtime.SetFieldNameMapper(goja.TagFieldNameMapper("json", true)) err = runtime.Set("uuid", uuid.NewString) diff --git a/pkg/generate/set.go b/pkg/generate/set.go new file mode 100644 index 000000000..b3ae52f63 --- /dev/null +++ b/pkg/generate/set.go @@ -0,0 +1,73 @@ +package generate + +import ( + "context" + "errors" + "fmt" + "github.com/formancehq/go-libs/v2/logging" + "github.com/formancehq/ledger/pkg/client" + "golang.org/x/sync/errgroup" +) + +type GeneratorSet struct { + vus int + script string + targetedLedger string + client *client.Formance + untilLogID uint64 +} + +func (s *GeneratorSet) Run(ctx context.Context) error { + parallelContext, cancel := context.WithCancel(ctx) + defer cancel() + + errGroup, ctx := errgroup.WithContext(parallelContext) + + for vu := 0; vu < s.vus; vu++ { + generator, err := NewGenerator(s.script, WithGlobals(map[string]any{ + "vu": vu, + })) + if err != nil { + return fmt.Errorf("failed to create generator: %w", err) + } + + errGroup.Go(func() error { + defer cancel() + + iteration := 0 + + for { + logging.FromContext(ctx).Debugf("Run iteration %d/%d", vu, iteration) + + action, err := generator.Next(vu) + if err != nil { + return fmt.Errorf("iteration %d/%d failed: %w", vu, iteration, err) + } + + ret, err := action.Apply(ctx, s.client.Ledger.V2, s.targetedLedger) + if err != nil { + if errors.Is(err, context.Canceled) { + return nil + } + return fmt.Errorf("iteration %d/%d failed: %w", vu, iteration, err) + } + if s.untilLogID != 0 && uint64(ret.GetLogID()) >= s.untilLogID { + return nil + } + iteration++ + } + }) + } + + return errGroup.Wait() +} + +func NewGeneratorSet(vus int, script string, targetedLedger string, client *client.Formance, untilLogID uint64) *GeneratorSet { + return &GeneratorSet{ + vus: vus, + script: script, + targetedLedger: targetedLedger, + client: client, + untilLogID: untilLogID, + } +} diff --git a/pkg/testserver/server.go b/pkg/testserver/server.go index 45b1046d0..c479887a9 100644 --- a/pkg/testserver/server.go +++ b/pkg/testserver/server.go @@ -45,7 +45,7 @@ type Configuration struct { Debug bool OTLPConfig *OTLPConfig ExperimentalFeatures bool - DisableAutoUpgrade bool + DisableAutoUpgrade bool BulkMaxSize int ExperimentalNumscriptRewrite bool } diff --git a/test/e2e/api_ledgers_create_test.go b/test/e2e/api_ledgers_create_test.go index 9aedb0030..9242dcf32 100644 --- a/test/e2e/api_ledgers_create_test.go +++ b/test/e2e/api_ledgers_create_test.go @@ -6,9 +6,9 @@ import ( "github.com/formancehq/go-libs/v2/logging" "github.com/formancehq/go-libs/v2/pointer" . "github.com/formancehq/go-libs/v2/testing/api" - ledger "github.com/formancehq/ledger/internal" "github.com/formancehq/ledger/pkg/client/models/components" "github.com/formancehq/ledger/pkg/client/models/operations" + "github.com/formancehq/ledger/pkg/features" . "github.com/formancehq/ledger/pkg/testserver" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -49,8 +49,8 @@ var _ = Context("Ledger engine tests", func() { }) Context("with specific features set", func() { BeforeEach(func() { - createLedgerRequest.V2CreateLedgerRequest.Features = ledger.MinimalFeatureSet. - With(ledger.FeatureMovesHistoryPostCommitEffectiveVolumes, "DISABLED") + createLedgerRequest.V2CreateLedgerRequest.Features = features.MinimalFeatureSet. + With(features.FeatureMovesHistoryPostCommitEffectiveVolumes, "DISABLED") }) It("should be ok", func() { Expect(err).To(BeNil()) @@ -58,8 +58,8 @@ var _ = Context("Ledger engine tests", func() { }) Context("with invalid feature configuration", func() { BeforeEach(func() { - createLedgerRequest.V2CreateLedgerRequest.Features = ledger.MinimalFeatureSet. - With(ledger.FeatureMovesHistoryPostCommitEffectiveVolumes, "XXX") + createLedgerRequest.V2CreateLedgerRequest.Features = features.MinimalFeatureSet. + With(features.FeatureMovesHistoryPostCommitEffectiveVolumes, "XXX") }) It("should fail", func() { Expect(err).To(HaveErrorCode(string(components.V2ErrorsEnumValidation))) @@ -67,7 +67,7 @@ var _ = Context("Ledger engine tests", func() { }) Context("with invalid feature name", func() { BeforeEach(func() { - createLedgerRequest.V2CreateLedgerRequest.Features = ledger.MinimalFeatureSet. + createLedgerRequest.V2CreateLedgerRequest.Features = features.MinimalFeatureSet. With("foo", "XXX") }) It("should fail", func() { diff --git a/test/e2e/api_ledgers_import_test.go b/test/e2e/api_ledgers_import_test.go index 6f3aca9c9..e3830d6c8 100644 --- a/test/e2e/api_ledgers_import_test.go +++ b/test/e2e/api_ledgers_import_test.go @@ -5,12 +5,12 @@ package test_suite import ( "database/sql" . "github.com/formancehq/go-libs/v2/testing/api" + "github.com/formancehq/ledger/pkg/features" "io" "math/big" "github.com/formancehq/go-libs/v2/logging" "github.com/formancehq/go-libs/v2/pointer" - ledger "github.com/formancehq/ledger/internal" "github.com/formancehq/ledger/pkg/client/models/components" "github.com/formancehq/ledger/pkg/client/models/operations" . "github.com/formancehq/ledger/pkg/testserver" @@ -43,7 +43,7 @@ var _ = Context("Ledger engine tests", func() { createLedgerRequest = operations.V2CreateLedgerRequest{ Ledger: "foo", V2CreateLedgerRequest: &components.V2CreateLedgerRequest{ - Features: ledger.MinimalFeatureSet, + Features: features.MinimalFeatureSet, }, } }) @@ -121,7 +121,7 @@ var _ = Context("Ledger engine tests", func() { err := CreateLedger(ctx, testServer.GetValue(), operations.V2CreateLedgerRequest{ Ledger: ledgerCopyName, V2CreateLedgerRequest: &components.V2CreateLedgerRequest{ - Features: ledger.MinimalFeatureSet, + Features: features.MinimalFeatureSet, }, }) Expect(err).To(BeNil()) diff --git a/test/performance/benchmark_test.go b/test/performance/benchmark_test.go index 2182d827c..8f3f6c5d4 100644 --- a/test/performance/benchmark_test.go +++ b/test/performance/benchmark_test.go @@ -4,11 +4,9 @@ package performance_test import ( "context" - "encoding/json" "fmt" . "github.com/formancehq/go-libs/v2/collectionutils" "github.com/formancehq/ledger/pkg/generate" - "net/http" "sort" "sync/atomic" "testing" @@ -115,14 +113,11 @@ func (benchmark *Benchmark) Run(ctx context.Context) map[string][]Result { report.End = time.Now() // Fetch otel metrics - rsp, err := http.Get(env.URL() + "/_/metrics") - require.NoError(b, err) - if rsp.StatusCode == http.StatusOK { - ret := make(map[string]any) - require.NoError(b, json.NewDecoder(rsp.Body).Decode(&ret)) - report.InternalMetrics = ret + metrics, err := env.Client().Ledger.GetMetrics(ctx) + if err != nil { + b.Logf("Unable to fetch ledger metrics: %s", err) } else { - b.Logf("Unable to fetch ledger metrics, got status code %d", rsp.StatusCode) + report.InternalMetrics = metrics.Object } // Compute final results diff --git a/test/performance/features_test.go b/test/performance/features_test.go index a3afc7c02..299094a27 100644 --- a/test/performance/features_test.go +++ b/test/performance/features_test.go @@ -4,7 +4,7 @@ package performance_test import ( . "github.com/formancehq/go-libs/v2/collectionutils" - ledger "github.com/formancehq/ledger/internal" + features2 "github.com/formancehq/ledger/pkg/features" "sort" ) @@ -12,19 +12,19 @@ func buildAllPossibleConfigurations() []configuration { possibleConfigurations := make([]configuration, 0) possibleConfigurations = append(possibleConfigurations, configuration{ Name: "MINIMAL", - FeatureSet: ledger.MinimalFeatureSet, + FeatureSet: features2.MinimalFeatureSet, }) - fullConfiguration := ledger.MinimalFeatureSet - features := Keys(ledger.FeatureConfigurations) + fullConfiguration := features2.MinimalFeatureSet + features := Keys(features2.FeatureConfigurations) sort.Strings(features) for _, feature := range features { possibleConfigurations = append(possibleConfigurations, configuration{ Name: feature, - FeatureSet: ledger.MinimalFeatureSet.With(feature, ledger.FeatureConfigurations[feature][0]), + FeatureSet: features2.MinimalFeatureSet.With(feature, features2.FeatureConfigurations[feature][0]), }) - fullConfiguration = fullConfiguration.With(feature, ledger.FeatureConfigurations[feature][0]) + fullConfiguration = fullConfiguration.With(feature, features2.FeatureConfigurations[feature][0]) } possibleConfigurations = append(possibleConfigurations, configuration{ Name: "FULL", @@ -36,7 +36,7 @@ func buildAllPossibleConfigurations() []configuration { type configuration struct { Name string - FeatureSet ledger.FeatureSet + FeatureSet features2.FeatureSet } func (c configuration) String() string { diff --git a/test/rolling-upgrades/main_test.go b/test/rolling-upgrades/main_test.go index bfa92f7b9..c4ba2f72f 100644 --- a/test/rolling-upgrades/main_test.go +++ b/test/rolling-upgrades/main_test.go @@ -5,7 +5,7 @@ import ( "flag" "fmt" "github.com/formancehq/go-libs/v2/logging" - ledger "github.com/formancehq/ledger/internal" + "github.com/formancehq/ledger/pkg/features" corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1" "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v3" metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/meta/v1" @@ -197,10 +197,10 @@ func deployTest(ctx *pulumi.Context) error { pulumi.String("30"), } - for _, key := range ledger.MinimalFeatureSet.SortedKeys() { + for _, key := range features.MinimalFeatureSet.SortedKeys() { generatorArgs = append(generatorArgs, pulumi.String("--ledger-feature"), - pulumi.String(key+"="+ledger.MinimalFeatureSet[key]), + pulumi.String(key+"="+features.MinimalFeatureSet[key]), ) } diff --git a/test/stress/stress_test.go b/test/stress/stress_test.go index 68f25c1b1..23117440e 100644 --- a/test/stress/stress_test.go +++ b/test/stress/stress_test.go @@ -4,6 +4,7 @@ package test_suite import ( "fmt" + "github.com/formancehq/ledger/pkg/features" "math/big" "math/rand" "sync" @@ -13,7 +14,6 @@ import ( "github.com/formancehq/go-libs/v2/logging" "github.com/formancehq/go-libs/v2/pointer" "github.com/formancehq/go-libs/v2/testing/platform/pgtesting" - ledger "github.com/formancehq/ledger/internal" "github.com/formancehq/ledger/pkg/client/models/components" "github.com/formancehq/ledger/pkg/client/models/operations" . "github.com/formancehq/ledger/pkg/testserver" @@ -52,7 +52,7 @@ var _ = Context("Ledger stress tests", func() { Ledger: ledgerName, V2CreateLedgerRequest: &components.V2CreateLedgerRequest{ Bucket: &bucketName, - Features: ledger.MinimalFeatureSet.With(ledger.FeatureMovesHistory, "ON"), + Features: features.MinimalFeatureSet.With(features.FeatureMovesHistory, "ON"), }, }) Expect(err).ShouldNot(HaveOccurred()) diff --git a/tools/generator/cmd/root.go b/tools/generator/cmd/root.go index be4fae102..158d66770 100644 --- a/tools/generator/cmd/root.go +++ b/tools/generator/cmd/root.go @@ -14,7 +14,6 @@ import ( "github.com/spf13/cobra" "golang.org/x/oauth2" "golang.org/x/oauth2/clientcredentials" - "golang.org/x/sync/errgroup" "net/http" "os" "strings" @@ -163,49 +162,11 @@ func run(cmd *cobra.Command, args []string) error { } } - parallelContext, cancel := context.WithCancel(cmd.Context()) - defer cancel() - - errGroup, ctx := errgroup.WithContext(parallelContext) - logging.FromContext(cmd.Context()).Infof("Starting to generate data with %d vus", vus) - for vu := 0; vu < vus; vu++ { - generator, err := generate.NewGenerator(string(fileContent), generate.WithGlobals(map[string]any{ - "vu": vu, - })) - if err != nil { - return fmt.Errorf("failed to create generator: %w", err) - } - - errGroup.Go(func() error { - defer cancel() - - iteration := 0 - - for { - logging.FromContext(ctx).Infof("Run iteration %d/%d", vu, iteration) - action, err := generator.Next(vu) - if err != nil { - return fmt.Errorf("iteration %d/%d failed: %w", vu, iteration, err) - } - - ret, err := action.Apply(ctx, client.Ledger.V2, targetedLedger) - if err != nil { - if errors.Is(err, context.Canceled) { - return nil - } - return fmt.Errorf("iteration %d/%d failed: %w", vu, iteration, err) - } - if untilLogID != 0 && ret.GetLogID() >= untilLogID { - return nil - } - iteration++ - } - }) - } - - return errGroup.Wait() + return generate. + NewGeneratorSet(vus, string(fileContent), targetedLedger, client, uint64(untilLogID)). + Run(cmd.Context()) } func extractSliceSliceFlag(cmd *cobra.Command, flagName string) (map[string]string, error) { From e875a40820f3674c25e172b176faf328c19d036e Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Tue, 19 Nov 2024 12:06:36 +0100 Subject: [PATCH 20/71] fix: bulk error response --- example2.js | 20 +++++++++ internal/api/v2/controllers_bulk.go | 31 +++++++++++++- pkg/generate/generator.go | 17 ++++++-- pkg/generate/generator_test.go | 13 +++++- test/performance/example_scripts/example1.js | 10 +++-- test/rolling-upgrades/Earthfile | 44 +++++++++++--------- tools/generator/cmd/root.go | 1 + tools/generator/examples/example1.js | 10 +++-- 8 files changed, 113 insertions(+), 33 deletions(-) create mode 100644 example2.js diff --git a/example2.js b/example2.js new file mode 100644 index 000000000..80aa7bc7b --- /dev/null +++ b/example2.js @@ -0,0 +1,20 @@ +function next() { + let postings = []; + for(let i = 0; i < 500000; i++) { + postings.push({ + source: `world`, + destination: `banks`, + amount: 100, + asset: 'USD' + }) + } + return { + action: 'CREATE_TRANSACTION', + data: { + postings + } + } +} + + + diff --git a/internal/api/v2/controllers_bulk.go b/internal/api/v2/controllers_bulk.go index 7ecbecf32..9eb3ce18a 100644 --- a/internal/api/v2/controllers_bulk.go +++ b/internal/api/v2/controllers_bulk.go @@ -32,7 +32,11 @@ func bulkHandler(bulkMaxSize int) http.HandlerFunc { w.Header().Set("Content-Type", "application/json") ret, errorsInBulk, err := ProcessBulk(r.Context(), common.LedgerFromContext(r.Context()), b, api.QueryParamBool(r, "continueOnFailure")) - if err != nil || errorsInBulk { + if err != nil { + api.InternalServerError(w, r, err) + return + } + if errorsInBulk { w.WriteHeader(http.StatusBadRequest) } @@ -125,6 +129,31 @@ func ProcessBulk( continueOnFailure bool, ) ([]Result, bool, error) { + for i, element := range bulk { + switch element.Action { + case ActionCreateTransaction: + req := &TransactionRequest{} + if err := json.Unmarshal(element.Data, req); err != nil { + return nil, false, fmt.Errorf("error parsing element %d: %s", i, err) + } + case ActionAddMetadata: + req := &AddMetadataRequest{} + if err := json.Unmarshal(element.Data, req); err != nil { + return nil, false, fmt.Errorf("error parsing element %d: %s", i, err) + } + case ActionRevertTransaction: + req := &RevertTransactionRequest{} + if err := json.Unmarshal(element.Data, req); err != nil { + return nil, false, fmt.Errorf("error parsing element %d: %s", i, err) + } + case ActionDeleteMetadata: + req := &DeleteMetadataRequest{} + if err := json.Unmarshal(element.Data, req); err != nil { + return nil, false, fmt.Errorf("error parsing element %d: %s", i, err) + } + } + } + ret := make([]Result, 0, len(bulk)) errorsInBulk := false diff --git a/pkg/generate/generator.go b/pkg/generate/generator.go index 5681c6950..7f319d23b 100644 --- a/pkg/generate/generator.go +++ b/pkg/generate/generator.go @@ -6,9 +6,9 @@ import ( "errors" "fmt" "github.com/dop251/goja" + "github.com/formancehq/go-libs/v2/collectionutils" ledger "github.com/formancehq/ledger/internal" v2 "github.com/formancehq/ledger/internal/api/v2" - ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" "github.com/formancehq/ledger/pkg/client" "github.com/formancehq/ledger/pkg/client/models/components" "github.com/formancehq/ledger/pkg/client/models/operations" @@ -45,7 +45,7 @@ func (r Action) Apply(ctx context.Context, client *client.V2, l string) (*Result var bulkElement components.V2BulkElement switch r.Action { case v2.ActionCreateTransaction: - transactionRequest := &ledgercontroller.RunScript{} + transactionRequest := &v2.TransactionRequest{} err := json.Unmarshal(r.Data, transactionRequest) if err != nil { return nil, fmt.Errorf("failed to unmarshal transaction request: %w", err) @@ -61,8 +61,18 @@ func (r Action) Apply(ctx context.Context, client *client.V2, l string) (*Result }(), Script: &components.V2PostTransactionScript{ Plain: transactionRequest.Script.Plain, - Vars: transactionRequest.Script.Vars, + Vars: collectionutils.ConvertMap(transactionRequest.Script.Vars, func(from any) string { + return fmt.Sprint(from) + }), }, + Postings: collectionutils.Map(transactionRequest.Postings, func(p ledger.Posting) components.V2Posting { + return components.V2Posting{ + Amount: p.Amount, + Asset: p.Asset, + Destination: p.Destination, + Source: p.Source, + } + }), Reference: func() *string { if transactionRequest.Reference == "" { return nil @@ -161,6 +171,7 @@ func (r Action) Apply(ctx context.Context, client *client.V2, l string) (*Result if err != nil { return nil, fmt.Errorf("creating transaction: %w", err) } + if errorResponse := response.V2BulkResponse.Data[0].V2BulkElementResultError; errorResponse != nil { if errorResponse.ErrorCode != "" { errorDescription := errorResponse.ErrorDescription diff --git a/pkg/generate/generator_test.go b/pkg/generate/generator_test.go index dd32406f4..b06977485 100644 --- a/pkg/generate/generator_test.go +++ b/pkg/generate/generator_test.go @@ -71,14 +71,23 @@ function next(iteration) { return { action: 'CREATE_TRANSACTION', data: { - plain: ` + "`" + ` + script: { + vars: { + dst: "bank" + }, + plain: ` + "`" + ` +vars { + account $dst +} + send [USD/2 100] ( source = @world - destination = @bank + destination = $dst ) set_tx_meta("globalMetadata", "${globalMetadata}") ` + "`" + ` + } } } case 1: diff --git a/test/performance/example_scripts/example1.js b/test/performance/example_scripts/example1.js index 81e7c0997..0157f6c5b 100644 --- a/test/performance/example_scripts/example1.js +++ b/test/performance/example_scripts/example1.js @@ -19,10 +19,12 @@ function next(iteration) { return { action: 'CREATE_TRANSACTION', data: { - plain, - vars: { - order: `orders:${uuid()}`, - seller: `sellers:${iteration % 5}` + script: { + plain, + vars: { + order: `orders:${uuid()}`, + seller: `sellers:${iteration % 5}` + } } } } diff --git a/test/rolling-upgrades/Earthfile b/test/rolling-upgrades/Earthfile index 5df23e983..e05900a22 100644 --- a/test/rolling-upgrades/Earthfile +++ b/test/rolling-upgrades/Earthfile @@ -58,7 +58,7 @@ cluster-create: RUN curl -L -o vcluster "https://github.com/loft-sh/vcluster/releases/download/v0.20.4/vcluster-linux-${TARGETARCH}" RUN install -c -m 0755 vcluster /usr/local/bin && rm -f vcluster ARG CLUSTER_NAME=test - RUN --no-cache vcluster create $CLUSTER_NAME --connect=false --upgrade + RUN vcluster create $CLUSTER_NAME --connect=false run: ARG CLUSTER_NAME=test @@ -86,24 +86,30 @@ run: ARG NO_CLEANUP=false ARG NO_CLEANUP_ON_FAILURE=false - RUN --secret PULUMI_ACCESS_TOKEN --secret GITHUB_TOKEN sh -c ' - echo "Connecting to VCluster..." - vcluster connect ${CLUSTER_NAME} --namespace vcluster-${CLUSTER_NAME} & - export KUBECONFIG=/root/.kube/config; - - echo "Waiting for VCluster to be ready..." - until kubectl get nodes; do sleep 1s; done; - - echo "Running test..." - go test \ - --test-image ghcr.io/formancehq/ledger:$CLUSTER_NAME-rolling-upgrade-test \ - --latest-version $CLUSTER_NAME-main \ - --actual-version $CLUSTER_NAME-current \ - --project ledger \ - --stack-prefix-name $CLUSTER_NAME- \ - --no-cleanup=$NO_CLEANUP \ - --no-cleanup-on-failure=$NO_CLEANUP_ON_FAILURE; - ' + WITH DOCKER + RUN --secret PULUMI_ACCESS_TOKEN --secret GITHUB_TOKEN sh -c ' + set -e; + + echo "Connecting to VCluster..." + vcluster connect ${CLUSTER_NAME} --namespace vcluster-${CLUSTER_NAME}; + + echo "Connected on context '$(kubectl config current-context)'"; + + echo "Waiting for VCluster to be ready..." + until kubectl get nodes; do sleep 1s; done; + + echo "Running test..." + go test \ + --test-image ghcr.io/formancehq/ledger:$CLUSTER_NAME-rolling-upgrade-test \ + --latest-version $CLUSTER_NAME-main \ + --actual-version $CLUSTER_NAME-current \ + --project ledger \ + --stack-prefix-name $CLUSTER_NAME- \ + --no-cleanup=$NO_CLEANUP \ + --no-cleanup-on-failure=$NO_CLEANUP_ON_FAILURE; + ' + END + IF [ $NO_CLEANUP = "false" ] RUN vcluster delete $CLUSTER_NAME --delete-namespace END diff --git a/tools/generator/cmd/root.go b/tools/generator/cmd/root.go index 158d66770..84cca4463 100644 --- a/tools/generator/cmd/root.go +++ b/tools/generator/cmd/root.go @@ -62,6 +62,7 @@ func Execute() { } func run(cmd *cobra.Command, args []string) error { + ledgerUrl := args[0] scriptLocation := args[1] diff --git a/tools/generator/examples/example1.js b/tools/generator/examples/example1.js index cefe6c0b5..fc50dd1a1 100644 --- a/tools/generator/examples/example1.js +++ b/tools/generator/examples/example1.js @@ -19,10 +19,12 @@ function next(iteration) { return { action: 'CREATE_TRANSACTION', data: { - plain, - vars: { - order: `orders:${uuid()}`, - seller: `sellers:${iteration % 5}` + script: { + plain, + vars: { + order: `orders:${uuid()}`, + seller: `sellers:${iteration % 5}` + } } } } From 56d3308ad76eaf3c85d628b77d4c87952f2fd2c7 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Tue, 19 Nov 2024 18:31:59 +0100 Subject: [PATCH 21/71] fix: pre-commit --- Earthfile | 18 +++--- test/rolling-upgrades/go.mod | 11 ---- test/rolling-upgrades/go.sum | 121 ----------------------------------- tools/generator/go.mod | 2 +- 4 files changed, 9 insertions(+), 143 deletions(-) diff --git a/Earthfile b/Earthfile index 5ecc79d6a..05d23b7dc 100644 --- a/Earthfile +++ b/Earthfile @@ -33,7 +33,8 @@ generate: RUN apk update && apk add openjdk11 RUN go install go.uber.org/mock/mockgen@v0.4.0 RUN go install github.com/princjef/gomarkdoc/cmd/gomarkdoc@latest - COPY (+sources/*) /src + COPY --dir (+lint/*) /src/ + COPY (+tidy/*) /src/ WORKDIR /src RUN go generate ./... SAVE ARTIFACT internal AS LOCAL internal @@ -130,19 +131,16 @@ lint: SAVE ARTIFACT main.go AS LOCAL main.go pre-commit: - WAIT - BUILD +tidy - BUILD +lint - BUILD +openapi - BUILD +openapi-markdown - END + BUILD +tidy + BUILD +lint + BUILD +openapi + BUILD +openapi-markdown BUILD +generate BUILD +generate-client BUILD +export-docs-events - # todo: currently not working with earthly - #BUILD ./test/rolling-upgrades+pre-commit - #BUILD ./tools/*+pre-commit + BUILD ./test/*+pre-commit + BUILD ./tools/*+pre-commit openapi: FROM node:20-alpine diff --git a/test/rolling-upgrades/go.mod b/test/rolling-upgrades/go.mod index d2c0d9237..5d4f65e88 100644 --- a/test/rolling-upgrades/go.mod +++ b/test/rolling-upgrades/go.mod @@ -26,9 +26,7 @@ require ( github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/blang/semver v3.5.1+incompatible // indirect - github.com/buger/jsonparser v1.1.1 // indirect github.com/charmbracelet/bubbles v0.16.1 // indirect github.com/charmbracelet/bubbletea v0.24.2 // indirect github.com/charmbracelet/lipgloss v0.7.1 // indirect @@ -56,13 +54,10 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/hcl/v2 v2.17.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/invopop/jsonschema v0.12.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect - github.com/jinzhu/inflection v1.0.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/lithammer/shortuuid/v3 v3.0.7 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect @@ -84,7 +79,6 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 // indirect github.com/pulumi/esc v0.6.2 // indirect - github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect @@ -96,16 +90,11 @@ require ( github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/texttheater/golang-levenshtein v1.0.1 // indirect - github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect github.com/tweekmonster/luser v0.0.0-20161003172636-3fa38070dbd7 // indirect github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect github.com/uber/jaeger-lib v2.4.1+incompatible // indirect - github.com/uptrace/bun v1.2.5 // indirect github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 // indirect github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 // indirect - github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect - github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/zclconf/go-cty v1.13.2 // indirect go.opentelemetry.io/otel v1.32.0 // indirect diff --git a/test/rolling-upgrades/go.sum b/test/rolling-upgrades/go.sum index 23a56cbfa..e9ab1cc7b 100644 --- a/test/rolling-upgrades/go.sum +++ b/test/rolling-upgrades/go.sum @@ -1,16 +1,10 @@ dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= -github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/ThreeDotsLabs/watermill v1.4.1 h1:gjP6yZH+otMPjV0KsV07pl9TeMm9UQV/gqiuiuG5Drs= @@ -27,45 +21,11 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aws/aws-sdk-go-v2 v1.32.4 h1:S13INUiTxgrPueTmrm5DZ+MiAo99zYzHEFh1UNkOxNE= -github.com/aws/aws-sdk-go-v2 v1.32.4/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo= -github.com/aws/aws-sdk-go-v2/config v1.28.3 h1:kL5uAptPcPKaJ4q0sDUjUIdueO18Q7JDzl64GpVwdOM= -github.com/aws/aws-sdk-go-v2/config v1.28.3/go.mod h1:SPEn1KA8YbgQnwiJ/OISU4fz7+F6Fe309Jf0QTsRCl4= -github.com/aws/aws-sdk-go-v2/credentials v1.17.44 h1:qqfs5kulLUHUEXlHEZXLJkgGoF3kkUeFUTVA585cFpU= -github.com/aws/aws-sdk-go-v2/credentials v1.17.44/go.mod h1:0Lm2YJ8etJdEdw23s+q/9wTpOeo2HhNE97XcRa7T8MA= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19 h1:woXadbf0c7enQ2UGCi8gW/WuKmE0xIzxBF/eD94jMKQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19/go.mod h1:zminj5ucw7w0r65bP6nhyOd3xL6veAUMc3ElGMoLVb4= -github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.23 h1:B2qK61ZXCQu8tkD6eG/gUiIt9Vw9tmWFD7Xo02JPdMY= -github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.23/go.mod h1:02rz9vMZsrOX9IwUcpoGZM4jPprFNPmtD6t9Ume9ECY= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23 h1:A2w6m6Tmr+BNXjDsr7M90zkWjsu4JXHwrzPg235STs4= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23/go.mod h1:35EVp9wyeANdujZruvHiQUAo9E3vbhnIO1mTCAxMlY0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23 h1:pgYW9FCabt2M25MoHYCfMrVY2ghiiBKYWUVXfwZs+sU= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23/go.mod h1:c48kLgzO19wAu3CPkDWC28JbaJ+hfQlsdl7I2+oqIbk= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4 h1:tHxQi/XHPK0ctd/wdOw0t7Xrc2OxcRCnVzv8lwWPu0c= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4/go.mod h1:4GQbF1vJzG60poZqWatZlhP31y8PGCCVTvIGPdaaYJ0= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.5 h1:HJwZwRt2Z2Tdec+m+fPjvdmkq2s9Ra+VR0hjF7V2o40= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.5/go.mod h1:wrMCEwjFPms+V86TCQQeOxQF/If4vT44FGIOFiMC2ck= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4 h1:zcx9LiGWZ6i6pjdcoE9oXAB6mUdeyC36Ia/QEiIvYdg= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4/go.mod h1:Tp/ly1cTjRLGBBmNccFumbZ8oqpZlpdhFf80SrRh4is= -github.com/aws/aws-sdk-go-v2/service/sts v1.32.4 h1:yDxvkz3/uOKfxnv8YhzOi9m+2OGIxF+on3KOISbK5IU= -github.com/aws/aws-sdk-go-v2/service/sts v1.32.4/go.mod h1:9XEUty5v5UAsMiFOBJrNibZgwCeOma73jgGwwhgffa8= -github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= -github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= -github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= -github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY= @@ -79,8 +39,6 @@ github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vc github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= -github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= @@ -90,14 +48,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/djherbis/times v1.5.0 h1:79myA211VwPhFTqUk8xehWrsEO+zcIZj0zT8mXPVARU= github.com/djherbis/times v1.5.0/go.mod h1:5q7FDLvbNg1L/KaBmPcWlVR9NmoKo3+ucqUA3ijQhA0= -github.com/docker/cli v27.3.1+incompatible h1:qEGdFBF3Xu6SCvCYhc7CzaQTlBmqDuzxPDpigSyeKQQ= -github.com/docker/cli v27.3.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= -github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= @@ -125,12 +75,6 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= -github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= -github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -140,10 +84,6 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -160,21 +100,8 @@ github.com/hashicorp/hcl/v2 v2.17.0 h1:z1XvSUyXd1HP10U4lrLg5e0JMVz6CPaJvAgxM0KNZ github.com/hashicorp/hcl/v2 v2.17.0/go.mod h1:gJyW2PTShkJqQBKpAmPO3yxMxIuoXkOF2TpqXzrQyx4= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI= -github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= -github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= -github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= -github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= -github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= @@ -191,8 +118,6 @@ github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJV github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= @@ -215,10 +140,6 @@ github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= -github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= @@ -231,23 +152,13 @@ github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= -github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/opencontainers/runc v1.1.14 h1:rgSuzbmgz5DUJjeSnw337TxDbRuqjs6iqQck/2weR6w= -github.com/opencontainers/runc v1.1.14/go.mod h1:E4C2z+7BxR7GHXp0hAY53mek+x49X1LjPNeMTfRGvOA= github.com/opentracing/basictracer-go v1.1.0 h1:Oa1fTSBvAl8pa3U+IJYqrKm0NALwH9OsgwOqDv4xJW0= github.com/opentracing/basictracer-go v1.1.0/go.mod h1:V2HZueSJEp879yv285Aap1BS69fQMD+MNP1mRs6mBQc= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= -github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA= -github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI= github.com/pgavlin/fx v0.1.6 h1:r9jEg69DhNoCd3Xh0+5mIbdbS3PqWrVWujkY76MFRTU= github.com/pgavlin/fx v0.1.6/go.mod h1:KWZJ6fqBBSh8GxHYqwYCf3rYE7Gp2p0N8tJp8xv9u9M= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= @@ -267,8 +178,6 @@ github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.12.0 h1:vG/22IHpYupt+ZD+KOnRo5PqIr github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.12.0/go.mod h1:WnJK/yelFkTPdsx7jZuUZixRunf+QQlgCwoRi1mVF3A= github.com/pulumi/pulumi/sdk/v3 v3.117.0 h1:ImIsukZ2ZIYQG94uWdSZl9dJjJTosQSTsOQTauTNX7U= github.com/pulumi/pulumi/sdk/v3 v3.117.0/go.mod h1:kNea72+FQk82OjZ3yEP4dl6nbAl2ngE8PDBc0iFAaHg= -github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4= -github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= @@ -307,42 +216,18 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/texttheater/golang-levenshtein v1.0.1 h1:+cRNoVrfiwufQPhoMzB6N0Yf/Mqajr6t1lOv8GyGE2U= github.com/texttheater/golang-levenshtein v1.0.1/go.mod h1:PYAKrbF5sAiq9wd+H82hs7gNaen0CplQ9uvm6+enD/8= -github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= -github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= github.com/tweekmonster/luser v0.0.0-20161003172636-3fa38070dbd7 h1:X9dsIWPuuEJlPX//UmRKophhOKCGXc46RVIGuttks68= github.com/tweekmonster/luser v0.0.0-20161003172636-3fa38070dbd7/go.mod h1:UxoP3EypF8JfGEjAII8jx1q8rQyDnX8qdTCs/UQBVIE= github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= -github.com/uptrace/bun v1.2.5 h1:gSprL5xiBCp+tzcZHgENzJpXnmQwRM/A6s4HnBF85mc= -github.com/uptrace/bun v1.2.5/go.mod h1:vkQMS4NNs4VNZv92y53uBSHXRqYyJp4bGhMHgaNCQpY= -github.com/uptrace/bun/dialect/pgdialect v1.2.5 h1:dWLUxpjTdglzfBks2x+U2WIi+nRVjuh7Z3DLYVFswJk= -github.com/uptrace/bun/dialect/pgdialect v1.2.5/go.mod h1:stwnlE8/6x8cuQ2aXcZqwDK/d+6jxgO3iQewflJT6C4= -github.com/uptrace/bun/extra/bunotel v1.2.5 h1:kkuuTbrG9d5leYZuSBKhq2gtq346lIrxf98Mig2y128= -github.com/uptrace/bun/extra/bunotel v1.2.5/go.mod h1:rCHLszRZwppWE9cGDodO2FCI1qCrLwDjONp38KD3bA8= github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 h1:H8wwQwTe5sL6x30z71lUgNiwBdeCHQjrphCfLwqIHGo= github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2/go.mod h1:/kR4beFhlz2g+V5ik8jW+3PMiMQAPt29y6K64NNY53c= -github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 h1:ZjUj9BLYf9PEqBn8W/OapxhPjVRdC6CsXTdULHsyk5c= -github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2/go.mod h1:O8bHQfyinKwTXKkiKNGmLQS7vRsqRxIQTFZpYpHK3IQ= github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 h1:3/aHKUq7qaFMWxyQV0W2ryNgg8x8rVeKVA20KJUkfS0= github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2/go.mod h1:Zit4b8AQXaXvA68+nzmbyDzqiyFRISyw1JiD5JqUBjw= -github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= -github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= -github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= -github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= -github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/xo/dburl v0.23.2 h1:Fl88cvayrgE56JA/sqhNMLljCW/b7RmG1mMkKMZUFgA= -github.com/xo/dburl v0.23.2/go.mod h1:uazlaAQxj4gkshhfuuYyvwCBouOmNnG2aDxTCFZpmL4= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -360,10 +245,6 @@ go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQD go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw= -go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= -go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg= -go.uber.org/fx v1.23.0/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -460,8 +341,6 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/tools/generator/go.mod b/tools/generator/go.mod index 0adff009d..ae37af068 100644 --- a/tools/generator/go.mod +++ b/tools/generator/go.mod @@ -15,7 +15,6 @@ require ( github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.9.0 golang.org/x/oauth2 v0.23.0 - golang.org/x/sync v0.9.0 ) require ( @@ -98,6 +97,7 @@ require ( golang.org/x/crypto v0.28.0 // indirect golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect golang.org/x/net v0.30.0 // indirect + golang.org/x/sync v0.9.0 // indirect golang.org/x/sys v0.27.0 // indirect golang.org/x/text v0.20.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 // indirect From 1f033c3323b140b22004e0ac9472cb09265228ef Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 20 Nov 2024 17:17:17 +0100 Subject: [PATCH 22/71] fix: revert transactions when dest account balance < 0 fail --- internal/README.md | 14 ++++---- .../controller/ledger/controller_default.go | 8 ++--- internal/transaction.go | 3 +- test/e2e/api_ledgers_import_test.go | 32 ++++++++++++++++--- 4 files changed, 38 insertions(+), 19 deletions(-) diff --git a/internal/README.md b/internal/README.md index 6a8490675..a647da7f6 100644 --- a/internal/README.md +++ b/internal/README.md @@ -75,8 +75,8 @@ import "github.com/formancehq/ledger/internal" - [func \(s \*SavedMetadata\) UnmarshalJSON\(data \[\]byte\) error](<#SavedMetadata.UnmarshalJSON>) - [type Transaction](<#Transaction>) - [func NewTransaction\(\) Transaction](<#NewTransaction>) - - [func \(tx Transaction\) InvolvedAccountAndAssets\(\) map\[string\]\[\]string](<#Transaction.InvolvedAccountAndAssets>) - [func \(tx Transaction\) InvolvedAccounts\(\) \[\]string](<#Transaction.InvolvedAccounts>) + - [func \(tx Transaction\) InvolvedDestinations\(\) map\[string\]\[\]string](<#Transaction.InvolvedDestinations>) - [func \(tx Transaction\) IsReverted\(\) bool](<#Transaction.IsReverted>) - [func \(Transaction\) JSONSchemaExtend\(schema \*jsonschema.Schema\)](<#Transaction.JSONSchemaExtend>) - [func \(tx Transaction\) MarshalJSON\(\) \(\[\]byte, error\)](<#Transaction.MarshalJSON>) @@ -831,20 +831,20 @@ func NewTransaction() Transaction - -### func \(Transaction\) InvolvedAccountAndAssets + +### func \(Transaction\) InvolvedAccounts ```go -func (tx Transaction) InvolvedAccountAndAssets() map[string][]string +func (tx Transaction) InvolvedAccounts() []string ``` - -### func \(Transaction\) InvolvedAccounts + +### func \(Transaction\) InvolvedDestinations ```go -func (tx Transaction) InvolvedAccounts() []string +func (tx Transaction) InvolvedDestinations() map[string][]string ``` diff --git a/internal/controller/ledger/controller_default.go b/internal/controller/ledger/controller_default.go index b55d29e78..a10a87a16 100644 --- a/internal/controller/ledger/controller_default.go +++ b/internal/controller/ledger/controller_default.go @@ -324,7 +324,7 @@ func (ctrl *DefaultController) revertTransaction(ctx context.Context, sqlTX TX, return nil, newErrAlreadyReverted(parameters.Input.TransactionID) } - bq := originalTransaction.InvolvedAccountAndAssets() + bq := originalTransaction.InvolvedDestinations() balances, err := sqlTX.GetBalances(ctx, bq) if err != nil { @@ -345,15 +345,11 @@ func (ctrl *DefaultController) revertTransaction(ctx context.Context, sqlTX TX, balances[posting.Source][posting.Asset], big.NewInt(0).Neg(posting.Amount), ) - balances[posting.Destination][posting.Destination] = balances[posting.Destination][posting.Asset].Add( - balances[posting.Destination][posting.Asset], - big.NewInt(0).Set(posting.Amount), - ) } for account, forAccount := range balances { for asset, finalBalance := range forAccount { - if finalBalance.Cmp(new(big.Int)) < 0 { + if finalBalance.Cmp(new(big.Int)) < 0 && account != "world" { // todo(waiting): break dependency on machine package // notes(gfyrag): wait for the new interpreter return nil, machine.NewErrInsufficientFund("insufficient fund for %s/%s", account, asset) diff --git a/internal/transaction.go b/internal/transaction.go index c577f6b6d..31a61ff28 100644 --- a/internal/transaction.go +++ b/internal/transaction.go @@ -89,10 +89,9 @@ func (tx Transaction) WithInsertedAt(date time.Time) Transaction { return tx } -func (tx Transaction) InvolvedAccountAndAssets() map[string][]string { +func (tx Transaction) InvolvedDestinations() map[string][]string { ret := make(map[string][]string) for _, posting := range tx.Postings { - ret[posting.Source] = append(ret[posting.Source], posting.Asset) ret[posting.Destination] = append(ret[posting.Destination], posting.Asset) } diff --git a/test/e2e/api_ledgers_import_test.go b/test/e2e/api_ledgers_import_test.go index e3830d6c8..2de3099d9 100644 --- a/test/e2e/api_ledgers_import_test.go +++ b/test/e2e/api_ledgers_import_test.go @@ -53,7 +53,23 @@ var _ = Context("Ledger engine tests", func() { Context("with a set of all possible actions", func() { JustBeforeEach(func() { Expect(err).To(BeNil()) - tx, err := CreateTransaction(ctx, testServer.GetValue(), operations.V2CreateTransactionRequest{ + + firstTX, err := CreateTransaction(ctx, testServer.GetValue(), operations.V2CreateTransactionRequest{ + Ledger: createLedgerRequest.Ledger, + V2PostTransaction: components.V2PostTransaction{ + Script: &components.V2PostTransactionScript{ + Plain: `send [COIN 100] ( + source = @world + destination = @bob + ) + set_account_meta(@world, "foo", "bar") + `, + }, + }, + }) + Expect(err).To(BeNil()) + + thirdTx, err := CreateTransaction(ctx, testServer.GetValue(), operations.V2CreateTransactionRequest{ Ledger: createLedgerRequest.Ledger, V2PostTransaction: components.V2PostTransaction{ Script: &components.V2PostTransactionScript{ @@ -70,12 +86,20 @@ var _ = Context("Ledger engine tests", func() { Expect(AddMetadataToTransaction(ctx, testServer.GetValue(), operations.V2AddMetadataOnTransactionRequest{ Ledger: createLedgerRequest.Ledger, - ID: tx.ID, + ID: firstTX.ID, RequestBody: map[string]string{ "foo": "bar", }, })).To(BeNil()) + Expect(AddMetadataToTransaction(ctx, testServer.GetValue(), operations.V2AddMetadataOnTransactionRequest{ + Ledger: createLedgerRequest.Ledger, + ID: thirdTx.ID, + RequestBody: map[string]string{ + "foo": "baz", + }, + })).To(BeNil()) + Expect(AddMetadataToAccount(ctx, testServer.GetValue(), operations.V2AddMetadataToAccountRequest{ Ledger: createLedgerRequest.Ledger, Address: "bank", @@ -86,7 +110,7 @@ var _ = Context("Ledger engine tests", func() { Expect(DeleteTransactionMetadata(ctx, testServer.GetValue(), operations.V2DeleteTransactionMetadataRequest{ Ledger: createLedgerRequest.Ledger, - ID: tx.ID, + ID: firstTX.ID, Key: "foo", })).To(BeNil()) @@ -98,7 +122,7 @@ var _ = Context("Ledger engine tests", func() { _, err = RevertTransaction(ctx, testServer.GetValue(), operations.V2RevertTransactionRequest{ Ledger: createLedgerRequest.Ledger, - ID: tx.ID, + ID: firstTX.ID, }) Expect(err).To(BeNil()) }) From 24c7abaae3ccc5daed6a65483066bd75c3faa4fc Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 20 Nov 2024 18:00:39 +0100 Subject: [PATCH 23/71] fix(imports): completely reproduce ids (with holes) and dates --- Earthfile | 2 +- docs/api/README.md | 1 + .../controller/ledger/controller_default.go | 12 +++- .../ledger/controller_default_test.go | 4 +- internal/controller/ledger/store.go | 2 +- .../controller/ledger/store_generated_test.go | 9 +-- internal/storage/ledger/legacy/adapters.go | 5 +- .../ledger/legacy/transactions_test.go | 2 +- internal/storage/ledger/logs.go | 12 ++-- internal/storage/ledger/transactions.go | 43 ++++++++----- internal/storage/ledger/transactions_test.go | 8 +-- openapi.yaml | 1 + openapi/v2.yaml | 1 + pkg/client/.speakeasy/gen.lock | 6 +- pkg/client/.speakeasy/gen.yaml | 2 +- .../docs/models/components/v2logtype.md | 3 +- pkg/client/formance.go | 4 +- pkg/client/models/components/v2log.go | 3 + test/e2e/api_ledgers_import_test.go | 62 +++++++++++++++++-- test/rolling-upgrades/Earthfile | 2 +- 20 files changed, 133 insertions(+), 51 deletions(-) diff --git a/Earthfile b/Earthfile index 05d23b7dc..43deeaa4d 100644 --- a/Earthfile +++ b/Earthfile @@ -33,8 +33,8 @@ generate: RUN apk update && apk add openjdk11 RUN go install go.uber.org/mock/mockgen@v0.4.0 RUN go install github.com/princjef/gomarkdoc/cmd/gomarkdoc@latest - COPY --dir (+lint/*) /src/ COPY (+tidy/*) /src/ + COPY --dir (+sources/src/*) /src/ WORKDIR /src RUN go generate ./... SAVE ARTIFACT internal AS LOCAL internal diff --git a/docs/api/README.md b/docs/api/README.md index 613b21baa..4d6d4fa02 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -2748,6 +2748,7 @@ Authorization ( Scopes: ledger:write ) |type|NEW_TRANSACTION| |type|SET_METADATA| |type|REVERTED_TRANSACTION| +|type|DELETE_METADATA|

V2CreateTransactionResponse

diff --git a/internal/controller/ledger/controller_default.go b/internal/controller/ledger/controller_default.go index a10a87a16..133991779 100644 --- a/internal/controller/ledger/controller_default.go +++ b/internal/controller/ledger/controller_default.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "github.com/formancehq/go-libs/v2/time" "github.com/formancehq/ledger/pkg/features" "math/big" "reflect" @@ -176,10 +177,17 @@ func (ctrl *DefaultController) importLog(ctx context.Context, sqlTx TX, log ledg } } case ledger.RevertedTransaction: - _, _, err := sqlTx.RevertTransaction(ctx, payload.RevertedTransaction.ID) + _, _, err := sqlTx.RevertTransaction( + ctx, + payload.RevertedTransaction.ID, + *payload.RevertedTransaction.RevertedAt, + ) if err != nil { return fmt.Errorf("failed to revert transaction: %w", err) } + if err := sqlTx.CommitTransaction(ctx, &payload.RevertTransaction); err != nil { + return fmt.Errorf("failed to commit transaction: %w", err) + } case ledger.SavedMetadata: switch payload.TargetType { case ledger.MetaTargetTypeTransaction: @@ -316,7 +324,7 @@ func (ctrl *DefaultController) revertTransaction(ctx context.Context, sqlTX TX, hasBeenReverted bool err error ) - originalTransaction, hasBeenReverted, err := sqlTX.RevertTransaction(ctx, parameters.Input.TransactionID) + originalTransaction, hasBeenReverted, err := sqlTX.RevertTransaction(ctx, parameters.Input.TransactionID, time.Time{}) if err != nil { return nil, err } diff --git a/internal/controller/ledger/controller_default_test.go b/internal/controller/ledger/controller_default_test.go index ed3974972..58fa91329 100644 --- a/internal/controller/ledger/controller_default_test.go +++ b/internal/controller/ledger/controller_default_test.go @@ -88,8 +88,8 @@ func TestRevertTransaction(t *testing.T) { txToRevert := ledger.Transaction{} sqlTX.EXPECT(). - RevertTransaction(gomock.Any(), 1). - DoAndReturn(func(_ context.Context, _ int) (*ledger.Transaction, bool, error) { + RevertTransaction(gomock.Any(), 1, time.Time{}). + DoAndReturn(func(_ context.Context, _ int, _ time.Time) (*ledger.Transaction, bool, error) { txToRevert.RevertedAt = pointer.For(time.Now()) return &txToRevert, true, nil }) diff --git a/internal/controller/ledger/store.go b/internal/controller/ledger/store.go index 55bc839c4..7136b9344 100644 --- a/internal/controller/ledger/store.go +++ b/internal/controller/ledger/store.go @@ -38,7 +38,7 @@ type TX interface { // * the reverted transaction // * a boolean indicating if the transaction has been reverted. false indicates an already reverted transaction (unless error != nil) // * an error - RevertTransaction(ctx context.Context, id int) (*ledger.Transaction, bool, error) + RevertTransaction(ctx context.Context, id int, at time.Time) (*ledger.Transaction, bool, error) UpdateTransactionMetadata(ctx context.Context, transactionID int, m metadata.Metadata) (*ledger.Transaction, bool, error) DeleteTransactionMetadata(ctx context.Context, transactionID int, key string) (*ledger.Transaction, bool, error) UpdateAccountsMetadata(ctx context.Context, m map[string]metadata.Metadata) error diff --git a/internal/controller/ledger/store_generated_test.go b/internal/controller/ledger/store_generated_test.go index 1618ae7c3..0d5200f3c 100644 --- a/internal/controller/ledger/store_generated_test.go +++ b/internal/controller/ledger/store_generated_test.go @@ -13,6 +13,7 @@ import ( bunpaginate "github.com/formancehq/go-libs/v2/bun/bunpaginate" metadata "github.com/formancehq/go-libs/v2/metadata" migrations "github.com/formancehq/go-libs/v2/migrations" + time "github.com/formancehq/go-libs/v2/time" ledger "github.com/formancehq/ledger/internal" bun "github.com/uptrace/bun" gomock "go.uber.org/mock/gomock" @@ -159,9 +160,9 @@ func (mr *MockTXMockRecorder) LockLedger(ctx any) *gomock.Call { } // RevertTransaction mocks base method. -func (m *MockTX) RevertTransaction(ctx context.Context, id int) (*ledger.Transaction, bool, error) { +func (m *MockTX) RevertTransaction(ctx context.Context, id int, at time.Time) (*ledger.Transaction, bool, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RevertTransaction", ctx, id) + ret := m.ctrl.Call(m, "RevertTransaction", ctx, id, at) ret0, _ := ret[0].(*ledger.Transaction) ret1, _ := ret[1].(bool) ret2, _ := ret[2].(error) @@ -169,9 +170,9 @@ func (m *MockTX) RevertTransaction(ctx context.Context, id int) (*ledger.Transac } // RevertTransaction indicates an expected call of RevertTransaction. -func (mr *MockTXMockRecorder) RevertTransaction(ctx, id any) *gomock.Call { +func (mr *MockTXMockRecorder) RevertTransaction(ctx, id, at any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevertTransaction", reflect.TypeOf((*MockTX)(nil).RevertTransaction), ctx, id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevertTransaction", reflect.TypeOf((*MockTX)(nil).RevertTransaction), ctx, id, at) } // UpdateAccountsMetadata mocks base method. diff --git a/internal/storage/ledger/legacy/adapters.go b/internal/storage/ledger/legacy/adapters.go index 4fa415d2d..19bb9723a 100644 --- a/internal/storage/ledger/legacy/adapters.go +++ b/internal/storage/ledger/legacy/adapters.go @@ -6,6 +6,7 @@ import ( "github.com/formancehq/go-libs/v2/bun/bunpaginate" "github.com/formancehq/go-libs/v2/metadata" "github.com/formancehq/go-libs/v2/migrations" + "github.com/formancehq/go-libs/v2/time" ledger "github.com/formancehq/ledger/internal" ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" @@ -30,8 +31,8 @@ func (tx TX) CommitTransaction(ctx context.Context, transaction *ledger.Transact return tx.newStore.CommitTransaction(ctx, transaction) } -func (tx TX) RevertTransaction(ctx context.Context, id int) (*ledger.Transaction, bool, error) { - return tx.newStore.RevertTransaction(ctx, id) +func (tx TX) RevertTransaction(ctx context.Context, id int, at time.Time) (*ledger.Transaction, bool, error) { + return tx.newStore.RevertTransaction(ctx, id, at) } func (tx TX) UpdateTransactionMetadata(ctx context.Context, transactionID int, m metadata.Metadata) (*ledger.Transaction, bool, error) { diff --git a/internal/storage/ledger/legacy/transactions_test.go b/internal/storage/ledger/legacy/transactions_test.go index f0be6d1a7..090a6be47 100644 --- a/internal/storage/ledger/legacy/transactions_test.go +++ b/internal/storage/ledger/legacy/transactions_test.go @@ -142,7 +142,7 @@ func TestGetTransactions(t *testing.T) { err = store.newStore.CommitTransaction(ctx, &tx3BeforeRevert) require.NoError(t, err) - _, hasBeenReverted, err := store.newStore.RevertTransaction(ctx, tx3BeforeRevert.ID) + _, hasBeenReverted, err := store.newStore.RevertTransaction(ctx, tx3BeforeRevert.ID, time.Time{}) require.NoError(t, err) require.True(t, hasBeenReverted) diff --git a/internal/storage/ledger/logs.go b/internal/storage/ledger/logs.go index 9d18352aa..2f6da528f 100644 --- a/internal/storage/ledger/logs.go +++ b/internal/storage/ledger/logs.go @@ -78,7 +78,7 @@ func (s *Store) InsertLog(ctx context.Context, log *ledger.Log) error { return err } - _, err = s.db. + query := s.db. NewInsert(). Model(&Log{ Log: log, @@ -87,9 +87,13 @@ func (s *Store) InsertLog(ctx context.Context, log *ledger.Log) error { Memento: mementoData, }). ModelTableExpr(s.GetPrefixedRelationName("logs")). - Value("id", "nextval(?)", s.GetPrefixedRelationName(fmt.Sprintf(`"log_id_%d"`, s.ledger.ID))). - Returning("*"). - Exec(ctx) + Returning("*") + + if log.ID == 0 { + query = query.Value("id", "nextval(?)", s.GetPrefixedRelationName(fmt.Sprintf(`"log_id_%d"`, s.ledger.ID))) + } + + _, err = query.Exec(ctx) if err != nil { err := postgres.ResolveError(err) switch { diff --git a/internal/storage/ledger/transactions.go b/internal/storage/ledger/transactions.go index 6b49413b4..3626c5dc1 100644 --- a/internal/storage/ledger/transactions.go +++ b/internal/storage/ledger/transactions.go @@ -389,13 +389,17 @@ func (s *Store) InsertTransaction(ctx context.Context, tx *ledger.Transaction) e s.tracer, s.insertTransactionHistogram, func(ctx context.Context) (*ledger.Transaction, error) { - _, err := s.db.NewInsert(). + query := s.db.NewInsert(). Model(tx). ModelTableExpr(s.GetPrefixedRelationName("transactions")). - Value("id", "nextval(?)", s.GetPrefixedRelationName(fmt.Sprintf(`"transaction_id_%d"`, s.ledger.ID))). Value("ledger", "?", s.ledger.Name). - Returning("id, timestamp, inserted_at"). - Exec(ctx) + Returning("id, timestamp, inserted_at") + + if tx.ID == 0 { + query = query.Value("id", "nextval(?)", s.GetPrefixedRelationName(fmt.Sprintf(`"transaction_id_%d"`, s.ledger.ID))) + } + + _, err := query.Exec(ctx) if err != nil { err = postgres.ResolveError(err) switch { @@ -460,26 +464,31 @@ func (s *Store) updateTxWithRetrieve(ctx context.Context, id int, query *bun.Upd return &me.Transaction, me.Modified, nil } -func (s *Store) RevertTransaction(ctx context.Context, id int) (tx *ledger.Transaction, modified bool, err error) { +func (s *Store) RevertTransaction(ctx context.Context, id int, at time.Time) (tx *ledger.Transaction, modified bool, err error) { _, err = tracing.TraceWithMetric( ctx, "RevertTransaction", s.tracer, s.revertTransactionHistogram, func(ctx context.Context) (*ledger.Transaction, error) { - tx, modified, err = s.updateTxWithRetrieve( - ctx, - id, - s.db.NewUpdate(). - Model(&ledger.Transaction{}). - ModelTableExpr(s.GetPrefixedRelationName("transactions")). - Where("id = ?", id). - Where("reverted_at is null"). - Where("ledger = ?", s.ledger.Name). + query := s.db.NewUpdate(). + Model(&ledger.Transaction{}). + ModelTableExpr(s.GetPrefixedRelationName("transactions")). + Where("id = ?", id). + Where("reverted_at is null"). + Where("ledger = ?", s.ledger.Name). + Returning("*") + if at.IsZero() { + query = query. Set("reverted_at = (now() at time zone 'utc')"). - Set("updated_at = (now() at time zone 'utc')"). - Returning("*"), - ) + Set("updated_at = (now() at time zone 'utc')") + } else { + query = query. + Set("reverted_at = ?", at). + Set("updated_at = ?", at) + } + + tx, modified, err = s.updateTxWithRetrieve(ctx, id, query) return nil, err }, ) diff --git a/internal/storage/ledger/transactions_test.go b/internal/storage/ledger/transactions_test.go index 9b22eeb24..0b5c907ad 100644 --- a/internal/storage/ledger/transactions_test.go +++ b/internal/storage/ledger/transactions_test.go @@ -544,7 +544,7 @@ func TestTransactionsRevert(t *testing.T) { require.NoError(t, err) // Revert the tx - revertedTx, reverted, err := store.RevertTransaction(ctx, tx1.ID) + revertedTx, reverted, err := store.RevertTransaction(ctx, tx1.ID, time.Time{}) require.NoError(t, err) require.True(t, reverted) require.NotNil(t, revertedTx) @@ -556,12 +556,12 @@ func TestTransactionsRevert(t *testing.T) { require.Equal(t, tx1, *revertedTx) // Try to revert again - _, reverted, err = store.RevertTransaction(ctx, tx1.ID) + _, reverted, err = store.RevertTransaction(ctx, tx1.ID, time.Time{}) require.NoError(t, err) require.False(t, reverted) // Revert a not existing transaction - revertedTx, reverted, err = store.RevertTransaction(ctx, 2) + revertedTx, reverted, err = store.RevertTransaction(ctx, 2, time.Time{}) require.True(t, errors.Is(err, postgres.ErrNotFound)) require.False(t, reverted) require.Nil(t, revertedTx) @@ -765,7 +765,7 @@ func TestTransactionsList(t *testing.T) { err = store.CommitTransaction(ctx, &tx3BeforeRevert) require.NoError(t, err) - _, hasBeenReverted, err := store.RevertTransaction(ctx, tx3BeforeRevert.ID) + _, hasBeenReverted, err := store.RevertTransaction(ctx, tx3BeforeRevert.ID, time.Time{}) require.NoError(t, err) require.True(t, hasBeenReverted) diff --git a/openapi.yaml b/openapi.yaml index 181026535..742570a37 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -3391,6 +3391,7 @@ components: - NEW_TRANSACTION - SET_METADATA - REVERTED_TRANSACTION + - DELETE_METADATA data: type: object properties: {} diff --git a/openapi/v2.yaml b/openapi/v2.yaml index 194e2c331..71d17a9ac 100644 --- a/openapi/v2.yaml +++ b/openapi/v2.yaml @@ -1660,6 +1660,7 @@ components: - NEW_TRANSACTION - SET_METADATA - REVERTED_TRANSACTION + - DELETE_METADATA data: type: object properties: {} diff --git a/pkg/client/.speakeasy/gen.lock b/pkg/client/.speakeasy/gen.lock index 960ab7ad2..c273af6cc 100644 --- a/pkg/client/.speakeasy/gen.lock +++ b/pkg/client/.speakeasy/gen.lock @@ -1,12 +1,12 @@ lockVersion: 2.0.0 id: a9ac79e1-e429-4ee3-96c4-ec973f19bec3 management: - docChecksum: 743c41071ff98ac5e7d00e58f65650e3 + docChecksum: 25c171859e282a2487f6d9f9d3888d8a docVersion: v1 speakeasyVersion: 1.351.0 generationVersion: 2.384.1 - releaseVersion: 0.4.31 - configChecksum: 006527a5b70ea037906b0ff29a01a2ef + releaseVersion: 0.4.32 + configChecksum: 38c4107cc4da2c542bfe11bf874323aa features: go: additionalDependencies: 0.1.0 diff --git a/pkg/client/.speakeasy/gen.yaml b/pkg/client/.speakeasy/gen.yaml index 965163ad3..cb7842dbb 100644 --- a/pkg/client/.speakeasy/gen.yaml +++ b/pkg/client/.speakeasy/gen.yaml @@ -15,7 +15,7 @@ generation: auth: oAuth2ClientCredentialsEnabled: true go: - version: 0.4.31 + version: 0.4.32 additionalDependencies: {} allowUnknownFieldsInWeakUnions: false clientServerStatusCodesAsErrors: true diff --git a/pkg/client/docs/models/components/v2logtype.md b/pkg/client/docs/models/components/v2logtype.md index 28b0b930c..9689da640 100644 --- a/pkg/client/docs/models/components/v2logtype.md +++ b/pkg/client/docs/models/components/v2logtype.md @@ -7,4 +7,5 @@ | ------------------------------ | ------------------------------ | | `V2LogTypeNewTransaction` | NEW_TRANSACTION | | `V2LogTypeSetMetadata` | SET_METADATA | -| `V2LogTypeRevertedTransaction` | REVERTED_TRANSACTION | \ No newline at end of file +| `V2LogTypeRevertedTransaction` | REVERTED_TRANSACTION | +| `V2LogTypeDeleteMetadata` | DELETE_METADATA | \ No newline at end of file diff --git a/pkg/client/formance.go b/pkg/client/formance.go index b6ec8384d..11fd71ddc 100644 --- a/pkg/client/formance.go +++ b/pkg/client/formance.go @@ -143,9 +143,9 @@ func New(opts ...SDKOption) *Formance { sdkConfiguration: sdkConfiguration{ Language: "go", OpenAPIDocVersion: "v1", - SDKVersion: "0.4.31", + SDKVersion: "0.4.32", GenVersion: "2.384.1", - UserAgent: "speakeasy-sdk/go 0.4.31 2.384.1 v1 github.com/formancehq/ledger/pkg/client", + UserAgent: "speakeasy-sdk/go 0.4.32 2.384.1 v1 github.com/formancehq/ledger/pkg/client", Hooks: hooks.New(), }, } diff --git a/pkg/client/models/components/v2log.go b/pkg/client/models/components/v2log.go index aa2c7b4fb..ec310d56f 100644 --- a/pkg/client/models/components/v2log.go +++ b/pkg/client/models/components/v2log.go @@ -16,6 +16,7 @@ const ( V2LogTypeNewTransaction V2LogType = "NEW_TRANSACTION" V2LogTypeSetMetadata V2LogType = "SET_METADATA" V2LogTypeRevertedTransaction V2LogType = "REVERTED_TRANSACTION" + V2LogTypeDeleteMetadata V2LogType = "DELETE_METADATA" ) func (e V2LogType) ToPointer() *V2LogType { @@ -32,6 +33,8 @@ func (e *V2LogType) UnmarshalJSON(data []byte) error { case "SET_METADATA": fallthrough case "REVERTED_TRANSACTION": + fallthrough + case "DELETE_METADATA": *e = V2LogType(v) return nil default: diff --git a/test/e2e/api_ledgers_import_test.go b/test/e2e/api_ledgers_import_test.go index 2de3099d9..711639c48 100644 --- a/test/e2e/api_ledgers_import_test.go +++ b/test/e2e/api_ledgers_import_test.go @@ -4,19 +4,18 @@ package test_suite import ( "database/sql" - . "github.com/formancehq/go-libs/v2/testing/api" - "github.com/formancehq/ledger/pkg/features" - "io" - "math/big" - "github.com/formancehq/go-libs/v2/logging" "github.com/formancehq/go-libs/v2/pointer" + . "github.com/formancehq/go-libs/v2/testing/api" "github.com/formancehq/ledger/pkg/client/models/components" "github.com/formancehq/ledger/pkg/client/models/operations" + "github.com/formancehq/ledger/pkg/features" . "github.com/formancehq/ledger/pkg/testserver" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/uptrace/bun" + "io" + "math/big" ) var _ = Context("Ledger engine tests", func() { @@ -69,6 +68,23 @@ var _ = Context("Ledger engine tests", func() { }) Expect(err).To(BeNil()) + // add a tx with a dry run to trigger a hole in ids + _, err = CreateTransaction(ctx, testServer.GetValue(), operations.V2CreateTransactionRequest{ + Ledger: createLedgerRequest.Ledger, + DryRun: pointer.For(true), + V2PostTransaction: components.V2PostTransaction{ + Script: &components.V2PostTransactionScript{ + Plain: `send [COIN 100] ( + source = @world + destination = @bob + ) + set_account_meta(@world, "foo", "bar") + `, + }, + }, + }) + Expect(err).To(BeNil()) + thirdTx, err := CreateTransaction(ctx, testServer.GetValue(), operations.V2CreateTransactionRequest{ Ledger: createLedgerRequest.Ledger, V2PostTransaction: components.V2PostTransaction{ @@ -166,6 +182,42 @@ var _ = Context("Ledger engine tests", func() { When("importing data", func() { It("should be ok", func() { Expect(importLogs()).To(Succeed()) + + logsFromOriginalLedger, err := ListLogs(ctx, testServer.GetValue(), operations.V2ListLogsRequest{ + Ledger: createLedgerRequest.Ledger, + }) + Expect(err).To(Succeed()) + + logsFromNewLedger, err := ListLogs(ctx, testServer.GetValue(), operations.V2ListLogsRequest{ + Ledger: ledgerCopyName, + }) + Expect(err).To(Succeed()) + + Expect(logsFromOriginalLedger.Data).To(Equal(logsFromNewLedger.Data)) + + transactionsFromOriginalLedger, err := ListTransactions(ctx, testServer.GetValue(), operations.V2ListTransactionsRequest{ + Ledger: createLedgerRequest.Ledger, + }) + Expect(err).To(Succeed()) + + transactionsFromNewLedger, err := ListTransactions(ctx, testServer.GetValue(), operations.V2ListTransactionsRequest{ + Ledger: ledgerCopyName, + }) + Expect(err).To(Succeed()) + + Expect(transactionsFromOriginalLedger.Data).To(Equal(transactionsFromNewLedger.Data)) + + accountsFromOriginalLedger, err := ListAccounts(ctx, testServer.GetValue(), operations.V2ListAccountsRequest{ + Ledger: createLedgerRequest.Ledger, + }) + Expect(err).To(Succeed()) + + accountsFromNewLedger, err := ListAccounts(ctx, testServer.GetValue(), operations.V2ListAccountsRequest{ + Ledger: ledgerCopyName, + }) + Expect(err).To(Succeed()) + + Expect(accountsFromOriginalLedger.Data).To(Equal(accountsFromNewLedger.Data)) }) }) Context("with state to 'in-use'", func() { diff --git a/test/rolling-upgrades/Earthfile b/test/rolling-upgrades/Earthfile index e05900a22..7a149f1f1 100644 --- a/test/rolling-upgrades/Earthfile +++ b/test/rolling-upgrades/Earthfile @@ -58,7 +58,7 @@ cluster-create: RUN curl -L -o vcluster "https://github.com/loft-sh/vcluster/releases/download/v0.20.4/vcluster-linux-${TARGETARCH}" RUN install -c -m 0755 vcluster /usr/local/bin && rm -f vcluster ARG CLUSTER_NAME=test - RUN vcluster create $CLUSTER_NAME --connect=false + RUN vcluster create $CLUSTER_NAME --connect=false --upgrade run: ARG CLUSTER_NAME=test From 664d92cb0996d7b4b519ccfe930e303224de42f4 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 21 Nov 2024 12:32:54 +0100 Subject: [PATCH 24/71] chore: update libs --- go.mod | 26 ++++++++--------- go.sum | 56 ++++++++++++++++++------------------ test/rolling-upgrades/go.mod | 2 +- test/rolling-upgrades/go.sum | 4 +-- tools/generator/go.mod | 2 +- tools/generator/go.sum | 56 ++++++++++++++++++------------------ 6 files changed, 73 insertions(+), 73 deletions(-) diff --git a/go.mod b/go.mod index 2fca2d73f..b3e8e4963 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 github.com/bluele/gcache v0.0.2 github.com/dop251/goja v0.0.0-20241009100908-5f46f2705ca3 - github.com/formancehq/go-libs/v2 v2.0.1-0.20241114125605-4a3e447246a9 + github.com/formancehq/go-libs/v2 v2.0.1-0.20241121113152-18a3fc7aa771 github.com/formancehq/ledger/pkg/client v0.0.0-00010101000000-000000000000 github.com/go-chi/chi/v5 v5.1.0 github.com/go-chi/cors v1.2.1 @@ -68,20 +68,20 @@ require ( github.com/ajg/form v1.5.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/aws/aws-msk-iam-sasl-signer-go v1.0.0 // indirect - github.com/aws/aws-sdk-go-v2 v1.32.4 // indirect - github.com/aws/aws-sdk-go-v2/config v1.28.3 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.17.44 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19 // indirect + github.com/aws/aws-sdk-go-v2 v1.32.5 // indirect + github.com/aws/aws-sdk-go-v2/config v1.28.5 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.46 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 // indirect github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.23 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.24.5 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.32.4 // indirect - github.com/aws/smithy-go v1.22.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.24.6 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.33.1 // indirect + github.com/aws/smithy-go v1.22.1 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect diff --git a/go.sum b/go.sum index 1cb9b579f..b821cfba7 100644 --- a/go.sum +++ b/go.sum @@ -30,34 +30,34 @@ github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYW github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/aws/aws-msk-iam-sasl-signer-go v1.0.0 h1:UyjtGmO0Uwl/K+zpzPwLoXzMhcN9xmnR2nrqJoBrg3c= github.com/aws/aws-msk-iam-sasl-signer-go v1.0.0/go.mod h1:TJAXuFs2HcMib3sN5L0gUC+Q01Qvy3DemvA55WuC+iA= -github.com/aws/aws-sdk-go-v2 v1.32.4 h1:S13INUiTxgrPueTmrm5DZ+MiAo99zYzHEFh1UNkOxNE= -github.com/aws/aws-sdk-go-v2 v1.32.4/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo= -github.com/aws/aws-sdk-go-v2/config v1.28.3 h1:kL5uAptPcPKaJ4q0sDUjUIdueO18Q7JDzl64GpVwdOM= -github.com/aws/aws-sdk-go-v2/config v1.28.3/go.mod h1:SPEn1KA8YbgQnwiJ/OISU4fz7+F6Fe309Jf0QTsRCl4= -github.com/aws/aws-sdk-go-v2/credentials v1.17.44 h1:qqfs5kulLUHUEXlHEZXLJkgGoF3kkUeFUTVA585cFpU= -github.com/aws/aws-sdk-go-v2/credentials v1.17.44/go.mod h1:0Lm2YJ8etJdEdw23s+q/9wTpOeo2HhNE97XcRa7T8MA= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19 h1:woXadbf0c7enQ2UGCi8gW/WuKmE0xIzxBF/eD94jMKQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19/go.mod h1:zminj5ucw7w0r65bP6nhyOd3xL6veAUMc3ElGMoLVb4= +github.com/aws/aws-sdk-go-v2 v1.32.5 h1:U8vdWJuY7ruAkzaOdD7guwJjD06YSKmnKCJs7s3IkIo= +github.com/aws/aws-sdk-go-v2 v1.32.5/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= +github.com/aws/aws-sdk-go-v2/config v1.28.5 h1:Za41twdCXbuyyWv9LndXxZZv3QhTG1DinqlFsSuvtI0= +github.com/aws/aws-sdk-go-v2/config v1.28.5/go.mod h1:4VsPbHP8JdcdUDmbTVgNL/8w9SqOkM5jyY8ljIxLO3o= +github.com/aws/aws-sdk-go-v2/credentials v1.17.46 h1:AU7RcriIo2lXjUfHFnFKYsLCwgbz1E7Mm95ieIRDNUg= +github.com/aws/aws-sdk-go-v2/credentials v1.17.46/go.mod h1:1FmYyLGL08KQXQ6mcTlifyFXfJVCNJTVGuQP4m0d/UA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 h1:sDSXIrlsFSFJtWKLQS4PUWRvrT580rrnuLydJrCQ/yA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20/go.mod h1:WZ/c+w0ofps+/OUqMwWgnfrgzZH1DZO1RIkktICsqnY= github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.23 h1:B2qK61ZXCQu8tkD6eG/gUiIt9Vw9tmWFD7Xo02JPdMY= github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.23/go.mod h1:02rz9vMZsrOX9IwUcpoGZM4jPprFNPmtD6t9Ume9ECY= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23 h1:A2w6m6Tmr+BNXjDsr7M90zkWjsu4JXHwrzPg235STs4= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23/go.mod h1:35EVp9wyeANdujZruvHiQUAo9E3vbhnIO1mTCAxMlY0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23 h1:pgYW9FCabt2M25MoHYCfMrVY2ghiiBKYWUVXfwZs+sU= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23/go.mod h1:c48kLgzO19wAu3CPkDWC28JbaJ+hfQlsdl7I2+oqIbk= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 h1:4usbeaes3yJnCFC7kfeyhkdkPtoRYPa/hTmCqMpKpLI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24/go.mod h1:5CI1JemjVwde8m2WG3cz23qHKPOxbpkq0HaoreEgLIY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 h1:N1zsICrQglfzaBnrfM0Ys00860C+QFwu6u/5+LomP+o= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24/go.mod h1:dCn9HbJ8+K31i8IQ8EWmWj0EiIk0+vKiHNMxTTYveAg= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4 h1:tHxQi/XHPK0ctd/wdOw0t7Xrc2OxcRCnVzv8lwWPu0c= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4/go.mod h1:4GQbF1vJzG60poZqWatZlhP31y8PGCCVTvIGPdaaYJ0= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.5 h1:HJwZwRt2Z2Tdec+m+fPjvdmkq2s9Ra+VR0hjF7V2o40= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.5/go.mod h1:wrMCEwjFPms+V86TCQQeOxQF/If4vT44FGIOFiMC2ck= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4 h1:zcx9LiGWZ6i6pjdcoE9oXAB6mUdeyC36Ia/QEiIvYdg= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4/go.mod h1:Tp/ly1cTjRLGBBmNccFumbZ8oqpZlpdhFf80SrRh4is= -github.com/aws/aws-sdk-go-v2/service/sts v1.32.4 h1:yDxvkz3/uOKfxnv8YhzOi9m+2OGIxF+on3KOISbK5IU= -github.com/aws/aws-sdk-go-v2/service/sts v1.32.4/go.mod h1:9XEUty5v5UAsMiFOBJrNibZgwCeOma73jgGwwhgffa8= -github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= -github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 h1:wtpJ4zcwrSbwhECWQoI/g6WM9zqCcSpHDJIWSbMLOu4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5/go.mod h1:qu/W9HXQbbQ4+1+JcZp0ZNPV31ym537ZJN+fiS7Ti8E= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.6 h1:3zu537oLmsPfDMyjnUS2g+F2vITgy5pB74tHI+JBNoM= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.6/go.mod h1:WJSZH2ZvepM6t6jwu4w/Z45Eoi75lPN7DcydSRtJg6Y= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 h1:K0OQAsDywb0ltlFrZm0JHPY3yZp/S9OaoLU33S7vPS8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5/go.mod h1:ORITg+fyuMoeiQFiVGoqB3OydVTLkClw/ljbblMq6Cc= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.1 h1:6SZUVRQNvExYlMLbHdlKB48x0fLbc2iVROyaNEwBHbU= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.1/go.mod h1:GqWyYCwLXnlUB1lOAXQyNSPqPLQJvmo8J0DWBzp9mtg= +github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= +github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw= @@ -104,8 +104,8 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241114125605-4a3e447246a9 h1:l6jieaR+sn4Ff+puBDMbTYmT2HTYC7Yt7GTxBAwC3eU= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241114125605-4a3e447246a9/go.mod h1:m0uKkey9OC/AeyWMwjMfZqhLzoWrPFBk8vuYdSSYj4Y= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241121113152-18a3fc7aa771 h1:iv1nF1Q+zEkMtc5HhSxNdCRQGAT0s+oCpW5SzyXbnT0= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241121113152-18a3fc7aa771/go.mod h1:6vkHEfWEkDSPOv/G2o1Exxra3ouuYxRiCkznwKxTMHU= github.com/formancehq/numscript v0.0.9-0.20241009144012-1150c14a1417 h1:LOd5hxnXDIBcehFrpW1OnXk+VSs0yJXeu1iAOO+Hji4= github.com/formancehq/numscript v0.0.9-0.20241009144012-1150c14a1417/go.mod h1:btuSv05cYwi9BvLRxVs5zrunU+O1vTgigG1T6UsawcY= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= @@ -142,8 +142,8 @@ github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIx github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= +github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= diff --git a/test/rolling-upgrades/go.mod b/test/rolling-upgrades/go.mod index 5d4f65e88..3c7e86cb2 100644 --- a/test/rolling-upgrades/go.mod +++ b/test/rolling-upgrades/go.mod @@ -9,7 +9,7 @@ replace github.com/formancehq/ledger/pkg/client => ../../pkg/client replace github.com/formancehq/ledger => ../.. require ( - github.com/formancehq/go-libs/v2 v2.0.1-0.20241114125605-4a3e447246a9 + github.com/formancehq/go-libs/v2 v2.0.1-0.20241121113152-18a3fc7aa771 github.com/formancehq/ledger v0.0.0-00010101000000-000000000000 github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.12.0 github.com/pulumi/pulumi/sdk/v3 v3.117.0 diff --git a/test/rolling-upgrades/go.sum b/test/rolling-upgrades/go.sum index e9ab1cc7b..22d254e7e 100644 --- a/test/rolling-upgrades/go.sum +++ b/test/rolling-upgrades/go.sum @@ -56,8 +56,8 @@ github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241114125605-4a3e447246a9 h1:l6jieaR+sn4Ff+puBDMbTYmT2HTYC7Yt7GTxBAwC3eU= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241114125605-4a3e447246a9/go.mod h1:m0uKkey9OC/AeyWMwjMfZqhLzoWrPFBk8vuYdSSYj4Y= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241121113152-18a3fc7aa771 h1:iv1nF1Q+zEkMtc5HhSxNdCRQGAT0s+oCpW5SzyXbnT0= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241121113152-18a3fc7aa771/go.mod h1:6vkHEfWEkDSPOv/G2o1Exxra3ouuYxRiCkznwKxTMHU= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= diff --git a/tools/generator/go.mod b/tools/generator/go.mod index ae37af068..507ef38d4 100644 --- a/tools/generator/go.mod +++ b/tools/generator/go.mod @@ -9,7 +9,7 @@ replace github.com/formancehq/ledger => ../.. replace github.com/formancehq/ledger/pkg/client => ../../pkg/client require ( - github.com/formancehq/go-libs/v2 v2.0.1-0.20241114125605-4a3e447246a9 + github.com/formancehq/go-libs/v2 v2.0.1-0.20241121113152-18a3fc7aa771 github.com/formancehq/ledger v0.0.0-00010101000000-000000000000 github.com/formancehq/ledger/pkg/client v0.0.0-00010101000000-000000000000 github.com/spf13/cobra v1.8.1 diff --git a/tools/generator/go.sum b/tools/generator/go.sum index 44dc83bc1..085f8e789 100644 --- a/tools/generator/go.sum +++ b/tools/generator/go.sum @@ -28,34 +28,34 @@ github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYW github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/aws/aws-msk-iam-sasl-signer-go v1.0.0 h1:UyjtGmO0Uwl/K+zpzPwLoXzMhcN9xmnR2nrqJoBrg3c= github.com/aws/aws-msk-iam-sasl-signer-go v1.0.0/go.mod h1:TJAXuFs2HcMib3sN5L0gUC+Q01Qvy3DemvA55WuC+iA= -github.com/aws/aws-sdk-go-v2 v1.32.4 h1:S13INUiTxgrPueTmrm5DZ+MiAo99zYzHEFh1UNkOxNE= -github.com/aws/aws-sdk-go-v2 v1.32.4/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo= -github.com/aws/aws-sdk-go-v2/config v1.28.3 h1:kL5uAptPcPKaJ4q0sDUjUIdueO18Q7JDzl64GpVwdOM= -github.com/aws/aws-sdk-go-v2/config v1.28.3/go.mod h1:SPEn1KA8YbgQnwiJ/OISU4fz7+F6Fe309Jf0QTsRCl4= -github.com/aws/aws-sdk-go-v2/credentials v1.17.44 h1:qqfs5kulLUHUEXlHEZXLJkgGoF3kkUeFUTVA585cFpU= -github.com/aws/aws-sdk-go-v2/credentials v1.17.44/go.mod h1:0Lm2YJ8etJdEdw23s+q/9wTpOeo2HhNE97XcRa7T8MA= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19 h1:woXadbf0c7enQ2UGCi8gW/WuKmE0xIzxBF/eD94jMKQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19/go.mod h1:zminj5ucw7w0r65bP6nhyOd3xL6veAUMc3ElGMoLVb4= +github.com/aws/aws-sdk-go-v2 v1.32.5 h1:U8vdWJuY7ruAkzaOdD7guwJjD06YSKmnKCJs7s3IkIo= +github.com/aws/aws-sdk-go-v2 v1.32.5/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= +github.com/aws/aws-sdk-go-v2/config v1.28.5 h1:Za41twdCXbuyyWv9LndXxZZv3QhTG1DinqlFsSuvtI0= +github.com/aws/aws-sdk-go-v2/config v1.28.5/go.mod h1:4VsPbHP8JdcdUDmbTVgNL/8w9SqOkM5jyY8ljIxLO3o= +github.com/aws/aws-sdk-go-v2/credentials v1.17.46 h1:AU7RcriIo2lXjUfHFnFKYsLCwgbz1E7Mm95ieIRDNUg= +github.com/aws/aws-sdk-go-v2/credentials v1.17.46/go.mod h1:1FmYyLGL08KQXQ6mcTlifyFXfJVCNJTVGuQP4m0d/UA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 h1:sDSXIrlsFSFJtWKLQS4PUWRvrT580rrnuLydJrCQ/yA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20/go.mod h1:WZ/c+w0ofps+/OUqMwWgnfrgzZH1DZO1RIkktICsqnY= github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.23 h1:B2qK61ZXCQu8tkD6eG/gUiIt9Vw9tmWFD7Xo02JPdMY= github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.23/go.mod h1:02rz9vMZsrOX9IwUcpoGZM4jPprFNPmtD6t9Ume9ECY= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23 h1:A2w6m6Tmr+BNXjDsr7M90zkWjsu4JXHwrzPg235STs4= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23/go.mod h1:35EVp9wyeANdujZruvHiQUAo9E3vbhnIO1mTCAxMlY0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23 h1:pgYW9FCabt2M25MoHYCfMrVY2ghiiBKYWUVXfwZs+sU= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23/go.mod h1:c48kLgzO19wAu3CPkDWC28JbaJ+hfQlsdl7I2+oqIbk= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 h1:4usbeaes3yJnCFC7kfeyhkdkPtoRYPa/hTmCqMpKpLI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24/go.mod h1:5CI1JemjVwde8m2WG3cz23qHKPOxbpkq0HaoreEgLIY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 h1:N1zsICrQglfzaBnrfM0Ys00860C+QFwu6u/5+LomP+o= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24/go.mod h1:dCn9HbJ8+K31i8IQ8EWmWj0EiIk0+vKiHNMxTTYveAg= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4 h1:tHxQi/XHPK0ctd/wdOw0t7Xrc2OxcRCnVzv8lwWPu0c= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4/go.mod h1:4GQbF1vJzG60poZqWatZlhP31y8PGCCVTvIGPdaaYJ0= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.5 h1:HJwZwRt2Z2Tdec+m+fPjvdmkq2s9Ra+VR0hjF7V2o40= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.5/go.mod h1:wrMCEwjFPms+V86TCQQeOxQF/If4vT44FGIOFiMC2ck= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4 h1:zcx9LiGWZ6i6pjdcoE9oXAB6mUdeyC36Ia/QEiIvYdg= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4/go.mod h1:Tp/ly1cTjRLGBBmNccFumbZ8oqpZlpdhFf80SrRh4is= -github.com/aws/aws-sdk-go-v2/service/sts v1.32.4 h1:yDxvkz3/uOKfxnv8YhzOi9m+2OGIxF+on3KOISbK5IU= -github.com/aws/aws-sdk-go-v2/service/sts v1.32.4/go.mod h1:9XEUty5v5UAsMiFOBJrNibZgwCeOma73jgGwwhgffa8= -github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= -github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 h1:wtpJ4zcwrSbwhECWQoI/g6WM9zqCcSpHDJIWSbMLOu4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5/go.mod h1:qu/W9HXQbbQ4+1+JcZp0ZNPV31ym537ZJN+fiS7Ti8E= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.6 h1:3zu537oLmsPfDMyjnUS2g+F2vITgy5pB74tHI+JBNoM= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.6/go.mod h1:WJSZH2ZvepM6t6jwu4w/Z45Eoi75lPN7DcydSRtJg6Y= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 h1:K0OQAsDywb0ltlFrZm0JHPY3yZp/S9OaoLU33S7vPS8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5/go.mod h1:ORITg+fyuMoeiQFiVGoqB3OydVTLkClw/ljbblMq6Cc= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.1 h1:6SZUVRQNvExYlMLbHdlKB48x0fLbc2iVROyaNEwBHbU= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.1/go.mod h1:GqWyYCwLXnlUB1lOAXQyNSPqPLQJvmo8J0DWBzp9mtg= +github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= +github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw= @@ -100,8 +100,8 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241114125605-4a3e447246a9 h1:l6jieaR+sn4Ff+puBDMbTYmT2HTYC7Yt7GTxBAwC3eU= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241114125605-4a3e447246a9/go.mod h1:m0uKkey9OC/AeyWMwjMfZqhLzoWrPFBk8vuYdSSYj4Y= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241121113152-18a3fc7aa771 h1:iv1nF1Q+zEkMtc5HhSxNdCRQGAT0s+oCpW5SzyXbnT0= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241121113152-18a3fc7aa771/go.mod h1:6vkHEfWEkDSPOv/G2o1Exxra3ouuYxRiCkznwKxTMHU= github.com/formancehq/numscript v0.0.9-0.20241009144012-1150c14a1417 h1:LOd5hxnXDIBcehFrpW1OnXk+VSs0yJXeu1iAOO+Hji4= github.com/formancehq/numscript v0.0.9-0.20241009144012-1150c14a1417/go.mod h1:btuSv05cYwi9BvLRxVs5zrunU+O1vTgigG1T6UsawcY= github.com/gkampitakis/ciinfo v0.3.0 h1:gWZlOC2+RYYttL0hBqcoQhM7h1qNkVqvRCV1fOvpAv8= @@ -135,8 +135,8 @@ github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIx github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= +github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= From d6ea95e0e6e105737091e3ce2ce0b41e51bc361d Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 21 Nov 2024 15:29:40 +0100 Subject: [PATCH 25/71] feat: add abstraction over bucket creation --- internal/storage/bucket/bucket.go | 302 +---------------- internal/storage/bucket/default_bucket.go | 306 ++++++++++++++++++ ...{bucket_test.go => default_bucket_test.go} | 2 +- internal/storage/driver/adapters.go | 2 +- internal/storage/driver/driver.go | 18 +- internal/storage/driver/module.go | 3 + internal/storage/ledger/legacy/main_test.go | 2 +- internal/storage/ledger/store.go | 4 +- internal/storage/ledger/transactions_test.go | 2 +- 9 files changed, 341 insertions(+), 300 deletions(-) create mode 100644 internal/storage/bucket/default_bucket.go rename internal/storage/bucket/{bucket_test.go => default_bucket_test.go} (96%) diff --git a/internal/storage/bucket/bucket.go b/internal/storage/bucket/bucket.go index 2570750e2..55601bcd1 100644 --- a/internal/storage/bucket/bucket.go +++ b/internal/storage/bucket/bucket.go @@ -1,306 +1,30 @@ package bucket import ( - "bytes" "context" - _ "embed" - "fmt" "github.com/formancehq/go-libs/v2/migrations" ledger "github.com/formancehq/ledger/internal" - "github.com/formancehq/ledger/pkg/features" "github.com/uptrace/bun" "go.opentelemetry.io/otel/trace" - "text/template" ) -// stateless version (+1 regarding directory name, as migrations start from 1 in the lib) -const MinimalSchemaVersion = 12 - -type Bucket struct { - name string - db *bun.DB +type Bucket interface { + Migrate(ctx context.Context, tracer trace.Tracer, minimalVersionReached chan struct{}, opts ...migrations.Option) error + AddLedger(ctx context.Context, ledger ledger.Ledger, db bun.IDB) error + HasMinimalVersion(ctx context.Context) (bool, error) + GetMigrationsInfo(ctx context.Context) ([]migrations.Info, error) } -func (b *Bucket) Migrate(ctx context.Context, tracer trace.Tracer, minimalVersionReached chan struct{}, options ...migrations.Option) error { - return migrate(ctx, tracer, b.db, b.name, minimalVersionReached, options...) +type Factory interface { + Create(db *bun.DB, name string) Bucket } -func (b *Bucket) HasMinimalVersion(ctx context.Context) (bool, error) { - migrator := GetMigrator(b.db, b.name) - lastVersion, err := migrator.GetLastVersion(ctx) - if err != nil { - return false, err - } - - return lastVersion >= MinimalSchemaVersion, nil -} +type DefaultFactory struct {} -func (b *Bucket) GetMigrationsInfo(ctx context.Context) ([]migrations.Info, error) { - return GetMigrator(b.db, b.name).GetMigrations(ctx) +func (f *DefaultFactory) Create(db *bun.DB, name string) Bucket { + return NewDefault(db, name) } -func (b *Bucket) AddLedger(ctx context.Context, l ledger.Ledger, db bun.IDB) error { - - for _, setup := range ledgerSetups { - if l.Features.Match(setup.requireFeatures) { - tpl := template.Must(template.New("sql").Parse(setup.script)) - buf := bytes.NewBuffer(nil) - if err := tpl.Execute(buf, l); err != nil { - return fmt.Errorf("executing template: %w", err) - } - - _, err := db.ExecContext(ctx, buf.String()) - if err != nil { - return fmt.Errorf("executing sql: %w", err) - } - } - } - - return nil -} - -func New(db *bun.DB, name string) *Bucket { - return &Bucket{ - db: db, - name: name, - } -} - -type ledgerSetup struct { - requireFeatures features.FeatureSet - script string -} - -var ledgerSetups = []ledgerSetup{ - { - script: ` - -- create a sequence for transactions by ledger instead of a sequence of the table as we want to have contiguous ids - -- notes: we can still have "holes" on ids since a sql transaction can be reverted after a usage of the sequence - create sequence "{{.Bucket}}"."transaction_id_{{.ID}}" owned by "{{.Bucket}}".transactions.id; - select setval('"{{.Bucket}}"."transaction_id_{{.ID}}"', coalesce(( - select max(id) + 1 - from "{{.Bucket}}".transactions - where ledger = '{{ .Name }}' - ), 1)::bigint, false); - `, - }, - { - script: ` - -- create a sequence for logs by ledger instead of a sequence of the table as we want to have contiguous ids - -- notes: we can still have "holes" on id since a sql transaction can be reverted after a usage of the sequence - create sequence "{{.Bucket}}"."log_id_{{.ID}}" owned by "{{.Bucket}}".logs.id; - select setval('"{{.Bucket}}"."log_id_{{.ID}}"', coalesce(( - select max(id) + 1 - from "{{.Bucket}}".logs - where ledger = '{{ .Name }}' - ), 1)::bigint, false); - `, - }, - { - requireFeatures: features.FeatureSet{ - features.FeatureMovesHistoryPostCommitEffectiveVolumes: "SYNC", - }, - script: `create index "pcev_{{.ID}}" on "{{.Bucket}}".moves (accounts_address, asset, effective_date desc) where ledger = '{{.Name}}';`, - }, - { - requireFeatures: features.FeatureSet{ - features.FeatureMovesHistoryPostCommitEffectiveVolumes: "SYNC", - }, - script: ` - create trigger "set_effective_volumes_{{.ID}}" - before insert - on "{{.Bucket}}"."moves" - for each row - when ( - new.ledger = '{{.Name}}' - ) - execute procedure "{{.Bucket}}".set_effective_volumes(); - `, - }, - { - requireFeatures: features.FeatureSet{ - features.FeatureMovesHistoryPostCommitEffectiveVolumes: "SYNC", - }, - script: ` - create trigger "update_effective_volumes_{{.ID}}" - after insert - on "{{.Bucket}}"."moves" - for each row - when ( - new.ledger = '{{.Name}}' - ) - execute procedure "{{.Bucket}}".update_effective_volumes(); - `, - }, - { - requireFeatures: features.FeatureSet{ - features.FeatureHashLogs: "SYNC", - }, - script: ` - create trigger "set_log_hash_{{.ID}}" - before insert - on "{{.Bucket}}"."logs" - for each row - when ( - new.ledger = '{{.Name}}' - ) - execute procedure "{{.Bucket}}".set_log_hash(); - `, - }, - { - requireFeatures: features.FeatureSet{ - features.FeatureAccountMetadataHistory: "SYNC", - }, - script: ` - create trigger "update_account_metadata_history_{{.ID}}" - after update - on "{{.Bucket}}"."accounts" - for each row - when ( - new.ledger = '{{.Name}}' - ) - execute procedure "{{.Bucket}}".update_account_metadata_history(); - `, - }, - { - requireFeatures: features.FeatureSet{ - features.FeatureAccountMetadataHistory: "SYNC", - }, - script: ` - create trigger "insert_account_metadata_history_{{.ID}}" - after insert - on "{{.Bucket}}"."accounts" - for each row - when ( - new.ledger = '{{.Name}}' - ) - execute procedure "{{.Bucket}}".insert_account_metadata_history(); - `, - }, - { - requireFeatures: features.FeatureSet{ - features.FeatureTransactionMetadataHistory: "SYNC", - }, - script: ` - create trigger "update_transaction_metadata_history_{{.ID}}" - after update - on "{{.Bucket}}"."transactions" - for each row - when ( - new.ledger = '{{.Name}}' - ) - execute procedure "{{.Bucket}}".update_transaction_metadata_history(); - `, - }, - { - requireFeatures: features.FeatureSet{ - features.FeatureTransactionMetadataHistory: "SYNC", - }, - script: ` - create trigger "insert_transaction_metadata_history_{{.ID}}" - after insert - on "{{.Bucket}}"."transactions" - for each row - when ( - new.ledger = '{{.Name}}' - ) - execute procedure "{{.Bucket}}".insert_transaction_metadata_history(); - `, - }, - { - requireFeatures: features.FeatureSet{ - features.FeatureIndexTransactionAccounts: "SYNC", - }, - script: ` - create index "transactions_sources_{{.ID}}" on "{{.Bucket}}".transactions using gin (sources jsonb_path_ops) where ledger = '{{.Name}}'; - `, - }, - { - requireFeatures: features.FeatureSet{ - features.FeatureIndexTransactionAccounts: "ON", - }, - script: ` - create index "transactions_destinations_{{.ID}}" on "{{.Bucket}}".transactions using gin (destinations jsonb_path_ops) where ledger = '{{.Name}}'; - `, - }, - { - requireFeatures: features.FeatureSet{ - features.FeatureIndexTransactionAccounts: "ON", - }, - script: ` - create trigger "transaction_set_addresses_{{.ID}}" - before insert - on "{{.Bucket}}"."transactions" - for each row - when ( - new.ledger = '{{.Name}}' - ) - execute procedure "{{.Bucket}}".set_transaction_addresses(); - `, - }, - { - requireFeatures: features.FeatureSet{ - features.FeatureIndexAddressSegments: "ON", - }, - script: ` - create index "accounts_address_array_{{.ID}}" on "{{.Bucket}}".accounts using gin (address_array jsonb_ops) where ledger = '{{.Name}}'; - `, - }, - { - requireFeatures: features.FeatureSet{ - features.FeatureIndexAddressSegments: "ON", - }, - script: ` - create index "accounts_address_array_length_{{.ID}}" on "{{.Bucket}}".accounts (jsonb_array_length(address_array)) where ledger = '{{.Name}}'; - `, - }, - { - requireFeatures: features.FeatureSet{ - features.FeatureIndexAddressSegments: "ON", - }, - script: ` - create trigger "accounts_set_address_array_{{.ID}}" - before insert - on "{{.Bucket}}"."accounts" - for each row - when ( - new.ledger = '{{.Name}}' - ) - execute procedure "{{.Bucket}}".set_address_array_for_account(); - `, - }, - { - requireFeatures: features.FeatureSet{ - features.FeatureIndexAddressSegments: "ON", - features.FeatureIndexTransactionAccounts: "ON", - }, - script: ` - create index "transactions_sources_arrays_{{.ID}}" on "{{.Bucket}}".transactions using gin (sources_arrays jsonb_path_ops) where ledger = '{{.Name}}'; - `, - }, - { - requireFeatures: features.FeatureSet{ - features.FeatureIndexAddressSegments: "ON", - features.FeatureIndexTransactionAccounts: "ON", - }, - script: ` - create index "transactions_destinations_arrays_{{.ID}}" on "{{.Bucket}}".transactions using gin (destinations_arrays jsonb_path_ops) where ledger = '{{.Name}}'; - `, - }, - { - requireFeatures: features.FeatureSet{ - features.FeatureIndexAddressSegments: "ON", - features.FeatureIndexTransactionAccounts: "ON", - }, - script: ` - create trigger "transaction_set_addresses_segments_{{.ID}}" - before insert - on "{{.Bucket}}"."transactions" - for each row - when ( - new.ledger = '{{.Name}}' - ) - execute procedure "{{.Bucket}}".set_transaction_addresses_segments(); - `, - }, -} +func NewDefaultFactory() *DefaultFactory { + return &DefaultFactory{} +} \ No newline at end of file diff --git a/internal/storage/bucket/default_bucket.go b/internal/storage/bucket/default_bucket.go new file mode 100644 index 000000000..6b583d49e --- /dev/null +++ b/internal/storage/bucket/default_bucket.go @@ -0,0 +1,306 @@ +package bucket + +import ( + "bytes" + "context" + _ "embed" + "fmt" + "github.com/formancehq/go-libs/v2/migrations" + ledger "github.com/formancehq/ledger/internal" + "github.com/formancehq/ledger/pkg/features" + "github.com/uptrace/bun" + "go.opentelemetry.io/otel/trace" + "text/template" +) + +// stateless version (+1 regarding directory name, as migrations start from 1 in the lib) +const MinimalSchemaVersion = 12 + +type DefaultBucket struct { + name string + db *bun.DB +} + +func (b *DefaultBucket) Migrate(ctx context.Context, tracer trace.Tracer, minimalVersionReached chan struct{}, options ...migrations.Option) error { + return migrate(ctx, tracer, b.db, b.name, minimalVersionReached, options...) +} + +func (b *DefaultBucket) HasMinimalVersion(ctx context.Context) (bool, error) { + migrator := GetMigrator(b.db, b.name) + lastVersion, err := migrator.GetLastVersion(ctx) + if err != nil { + return false, err + } + + return lastVersion >= MinimalSchemaVersion, nil +} + +func (b *DefaultBucket) GetMigrationsInfo(ctx context.Context) ([]migrations.Info, error) { + return GetMigrator(b.db, b.name).GetMigrations(ctx) +} + +func (b *DefaultBucket) AddLedger(ctx context.Context, l ledger.Ledger, db bun.IDB) error { + + for _, setup := range ledgerSetups { + if l.Features.Match(setup.requireFeatures) { + tpl := template.Must(template.New("sql").Parse(setup.script)) + buf := bytes.NewBuffer(nil) + if err := tpl.Execute(buf, l); err != nil { + return fmt.Errorf("executing template: %w", err) + } + + _, err := db.ExecContext(ctx, buf.String()) + if err != nil { + return fmt.Errorf("executing sql: %w", err) + } + } + } + + return nil +} + +func NewDefault(db *bun.DB, name string) *DefaultBucket { + return &DefaultBucket{ + db: db, + name: name, + } +} + +type ledgerSetup struct { + requireFeatures features.FeatureSet + script string +} + +var ledgerSetups = []ledgerSetup{ + { + script: ` + -- create a sequence for transactions by ledger instead of a sequence of the table as we want to have contiguous ids + -- notes: we can still have "holes" on ids since a sql transaction can be reverted after a usage of the sequence + create sequence "{{.Bucket}}"."transaction_id_{{.ID}}" owned by "{{.Bucket}}".transactions.id; + select setval('"{{.Bucket}}"."transaction_id_{{.ID}}"', coalesce(( + select max(id) + 1 + from "{{.Bucket}}".transactions + where ledger = '{{ .Name }}' + ), 1)::bigint, false); + `, + }, + { + script: ` + -- create a sequence for logs by ledger instead of a sequence of the table as we want to have contiguous ids + -- notes: we can still have "holes" on id since a sql transaction can be reverted after a usage of the sequence + create sequence "{{.Bucket}}"."log_id_{{.ID}}" owned by "{{.Bucket}}".logs.id; + select setval('"{{.Bucket}}"."log_id_{{.ID}}"', coalesce(( + select max(id) + 1 + from "{{.Bucket}}".logs + where ledger = '{{ .Name }}' + ), 1)::bigint, false); + `, + }, + { + requireFeatures: features.FeatureSet{ + features.FeatureMovesHistoryPostCommitEffectiveVolumes: "SYNC", + }, + script: `create index "pcev_{{.ID}}" on "{{.Bucket}}".moves (accounts_address, asset, effective_date desc) where ledger = '{{.Name}}';`, + }, + { + requireFeatures: features.FeatureSet{ + features.FeatureMovesHistoryPostCommitEffectiveVolumes: "SYNC", + }, + script: ` + create trigger "set_effective_volumes_{{.ID}}" + before insert + on "{{.Bucket}}"."moves" + for each row + when ( + new.ledger = '{{.Name}}' + ) + execute procedure "{{.Bucket}}".set_effective_volumes(); + `, + }, + { + requireFeatures: features.FeatureSet{ + features.FeatureMovesHistoryPostCommitEffectiveVolumes: "SYNC", + }, + script: ` + create trigger "update_effective_volumes_{{.ID}}" + after insert + on "{{.Bucket}}"."moves" + for each row + when ( + new.ledger = '{{.Name}}' + ) + execute procedure "{{.Bucket}}".update_effective_volumes(); + `, + }, + { + requireFeatures: features.FeatureSet{ + features.FeatureHashLogs: "SYNC", + }, + script: ` + create trigger "set_log_hash_{{.ID}}" + before insert + on "{{.Bucket}}"."logs" + for each row + when ( + new.ledger = '{{.Name}}' + ) + execute procedure "{{.Bucket}}".set_log_hash(); + `, + }, + { + requireFeatures: features.FeatureSet{ + features.FeatureAccountMetadataHistory: "SYNC", + }, + script: ` + create trigger "update_account_metadata_history_{{.ID}}" + after update + on "{{.Bucket}}"."accounts" + for each row + when ( + new.ledger = '{{.Name}}' + ) + execute procedure "{{.Bucket}}".update_account_metadata_history(); + `, + }, + { + requireFeatures: features.FeatureSet{ + features.FeatureAccountMetadataHistory: "SYNC", + }, + script: ` + create trigger "insert_account_metadata_history_{{.ID}}" + after insert + on "{{.Bucket}}"."accounts" + for each row + when ( + new.ledger = '{{.Name}}' + ) + execute procedure "{{.Bucket}}".insert_account_metadata_history(); + `, + }, + { + requireFeatures: features.FeatureSet{ + features.FeatureTransactionMetadataHistory: "SYNC", + }, + script: ` + create trigger "update_transaction_metadata_history_{{.ID}}" + after update + on "{{.Bucket}}"."transactions" + for each row + when ( + new.ledger = '{{.Name}}' + ) + execute procedure "{{.Bucket}}".update_transaction_metadata_history(); + `, + }, + { + requireFeatures: features.FeatureSet{ + features.FeatureTransactionMetadataHistory: "SYNC", + }, + script: ` + create trigger "insert_transaction_metadata_history_{{.ID}}" + after insert + on "{{.Bucket}}"."transactions" + for each row + when ( + new.ledger = '{{.Name}}' + ) + execute procedure "{{.Bucket}}".insert_transaction_metadata_history(); + `, + }, + { + requireFeatures: features.FeatureSet{ + features.FeatureIndexTransactionAccounts: "SYNC", + }, + script: ` + create index "transactions_sources_{{.ID}}" on "{{.Bucket}}".transactions using gin (sources jsonb_path_ops) where ledger = '{{.Name}}'; + `, + }, + { + requireFeatures: features.FeatureSet{ + features.FeatureIndexTransactionAccounts: "ON", + }, + script: ` + create index "transactions_destinations_{{.ID}}" on "{{.Bucket}}".transactions using gin (destinations jsonb_path_ops) where ledger = '{{.Name}}'; + `, + }, + { + requireFeatures: features.FeatureSet{ + features.FeatureIndexTransactionAccounts: "ON", + }, + script: ` + create trigger "transaction_set_addresses_{{.ID}}" + before insert + on "{{.Bucket}}"."transactions" + for each row + when ( + new.ledger = '{{.Name}}' + ) + execute procedure "{{.Bucket}}".set_transaction_addresses(); + `, + }, + { + requireFeatures: features.FeatureSet{ + features.FeatureIndexAddressSegments: "ON", + }, + script: ` + create index "accounts_address_array_{{.ID}}" on "{{.Bucket}}".accounts using gin (address_array jsonb_ops) where ledger = '{{.Name}}'; + `, + }, + { + requireFeatures: features.FeatureSet{ + features.FeatureIndexAddressSegments: "ON", + }, + script: ` + create index "accounts_address_array_length_{{.ID}}" on "{{.Bucket}}".accounts (jsonb_array_length(address_array)) where ledger = '{{.Name}}'; + `, + }, + { + requireFeatures: features.FeatureSet{ + features.FeatureIndexAddressSegments: "ON", + }, + script: ` + create trigger "accounts_set_address_array_{{.ID}}" + before insert + on "{{.Bucket}}"."accounts" + for each row + when ( + new.ledger = '{{.Name}}' + ) + execute procedure "{{.Bucket}}".set_address_array_for_account(); + `, + }, + { + requireFeatures: features.FeatureSet{ + features.FeatureIndexAddressSegments: "ON", + features.FeatureIndexTransactionAccounts: "ON", + }, + script: ` + create index "transactions_sources_arrays_{{.ID}}" on "{{.Bucket}}".transactions using gin (sources_arrays jsonb_path_ops) where ledger = '{{.Name}}'; + `, + }, + { + requireFeatures: features.FeatureSet{ + features.FeatureIndexAddressSegments: "ON", + features.FeatureIndexTransactionAccounts: "ON", + }, + script: ` + create index "transactions_destinations_arrays_{{.ID}}" on "{{.Bucket}}".transactions using gin (destinations_arrays jsonb_path_ops) where ledger = '{{.Name}}'; + `, + }, + { + requireFeatures: features.FeatureSet{ + features.FeatureIndexAddressSegments: "ON", + features.FeatureIndexTransactionAccounts: "ON", + }, + script: ` + create trigger "transaction_set_addresses_segments_{{.ID}}" + before insert + on "{{.Bucket}}"."transactions" + for each row + when ( + new.ledger = '{{.Name}}' + ) + execute procedure "{{.Bucket}}".set_transaction_addresses_segments(); + `, + }, +} diff --git a/internal/storage/bucket/bucket_test.go b/internal/storage/bucket/default_bucket_test.go similarity index 96% rename from internal/storage/bucket/bucket_test.go rename to internal/storage/bucket/default_bucket_test.go index 0b26c0f49..1201be4ac 100644 --- a/internal/storage/bucket/bucket_test.go +++ b/internal/storage/bucket/default_bucket_test.go @@ -30,6 +30,6 @@ func TestBuckets(t *testing.T) { require.NoError(t, driver.Migrate(ctx, db)) - b := bucket.New(db, name) + b := bucket.NewDefault(db, name) require.NoError(t, b.Migrate(ctx, noop.Tracer{}, make(chan struct{}))) } diff --git a/internal/storage/driver/adapters.go b/internal/storage/driver/adapters.go index 7116a4b11..7150c6503 100644 --- a/internal/storage/driver/adapters.go +++ b/internal/storage/driver/adapters.go @@ -31,4 +31,4 @@ func NewControllerStorageDriverAdapter(d *Driver) *DefaultStorageDriverAdapter { return &DefaultStorageDriverAdapter{Driver: d} } -var _ systemcontroller.Store = (*DefaultStorageDriverAdapter)(nil) +var _ systemcontroller.Store = (*DefaultStorageDriverAdapter)(nil) \ No newline at end of file diff --git a/internal/storage/driver/driver.go b/internal/storage/driver/driver.go index 1e719ce63..9ea2750bb 100644 --- a/internal/storage/driver/driver.go +++ b/internal/storage/driver/driver.go @@ -32,6 +32,7 @@ const ( type Driver struct { db *bun.DB + bucketFactory bucket.Factory tracer trace.Tracer meter metric.Meter migratorLockRetryInterval time.Duration @@ -43,7 +44,7 @@ func (d *Driver) CreateLedger(ctx context.Context, l *ledger.Ledger) (*ledgersto l.Metadata = metadata.Metadata{} } - b := bucket.New(d.db, l.Bucket) + b := d.bucketFactory.Create(d.db, l.Bucket) if err := b.Migrate( ctx, d.tracer, @@ -89,7 +90,7 @@ func (d *Driver) OpenLedger(ctx context.Context, name string) (*ledgerstore.Stor return ledgerstore.New( d.db, - bucket.New(d.db, ret.Bucket), + d.bucketFactory.Create(d.db, ret.Bucket), *ret, ledgerstore.WithMeter(d.meter), ledgerstore.WithTracer(d.tracer), @@ -197,7 +198,7 @@ func (d *Driver) GetLedger(ctx context.Context, name string) (*ledger.Ledger, er } func (d *Driver) UpgradeBucket(ctx context.Context, name string) error { - return bucket.New(d.db, name).Migrate( + return d.bucketFactory.Create(d.db, name).Migrate( ctx, d.tracer, make(chan struct{}), @@ -222,7 +223,7 @@ func (d *Driver) UpgradeAllBuckets(ctx context.Context, minimalVersionReached ch grp, ctx := errgroup.WithContext(ctx) for _, bucketName := range buckets { grp.Go(func() error { - b := bucket.New(d.db, bucketName) + b := d.bucketFactory.Create(d.db, bucketName) minimalVersionReached := make(chan struct{}) @@ -269,7 +270,7 @@ func (d *Driver) GetDB() *bun.DB { func New(db *bun.DB, opts ...Option) *Driver { ret := &Driver{ - db: db, + db: db, } for _, opt := range append(defaultOptions, opts...) { opt(ret) @@ -297,7 +298,14 @@ func WithMigratorLockRetryInterval(interval time.Duration) Option { } } +func WithBucketFactory(factory bucket.Factory) Option { + return func(d *Driver) { + d.bucketFactory = factory + } +} + var defaultOptions = []Option{ WithMeter(noopmetrics.Meter{}), WithTracer(nooptracer.Tracer{}), + WithBucketFactory(bucket.NewDefaultFactory()), } diff --git a/internal/storage/driver/module.go b/internal/storage/driver/module.go index 3cdcda27a..58bfede53 100644 --- a/internal/storage/driver/module.go +++ b/internal/storage/driver/module.go @@ -2,6 +2,7 @@ package driver import ( "context" + "github.com/formancehq/ledger/internal/storage/bucket" "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/trace" @@ -22,8 +23,10 @@ type ModuleConfiguration struct { func NewFXModule() fx.Option { return fx.Options( + fx.Provide(fx.Annotate(bucket.NewDefaultFactory, fx.As(new(bucket.Factory)))), fx.Provide(func( db *bun.DB, + bucketFactory bucket.Factory, tracerProvider trace.TracerProvider, meterProvider metric.MeterProvider, ) (*Driver, error) { diff --git a/internal/storage/ledger/legacy/main_test.go b/internal/storage/ledger/legacy/main_test.go index 4b614743e..67b2fe138 100644 --- a/internal/storage/ledger/legacy/main_test.go +++ b/internal/storage/ledger/legacy/main_test.go @@ -68,7 +68,7 @@ func newLedgerStore(t T) *testStore { l := ledger.MustNewWithDefault(ledgerName) - b := bucket.New(db, ledger.DefaultBucket) + b := bucket.NewDefault(db, ledger.DefaultBucket) require.NoError(t, b.Migrate(ctx, noop.Tracer{}, make(chan struct{}))) require.NoError(t, b.AddLedger(ctx, l, db)) diff --git a/internal/storage/ledger/store.go b/internal/storage/ledger/store.go index ed9bfb6d3..3d332f797 100644 --- a/internal/storage/ledger/store.go +++ b/internal/storage/ledger/store.go @@ -19,7 +19,7 @@ import ( type Store struct { db bun.IDB - bucket *bucket.Bucket + bucket bucket.Bucket ledger ledger.Ledger tracer trace.Tracer @@ -84,7 +84,7 @@ func (s *Store) LockLedger(ctx context.Context) error { return postgres.ResolveError(err) } -func New(db bun.IDB, bucket *bucket.Bucket, ledger ledger.Ledger, opts ...Option) *Store { +func New(db bun.IDB, bucket bucket.Bucket, ledger ledger.Ledger, opts ...Option) *Store { ret := &Store{ db: db, ledger: ledger, diff --git a/internal/storage/ledger/transactions_test.go b/internal/storage/ledger/transactions_test.go index 0b5c907ad..990472852 100644 --- a/internal/storage/ledger/transactions_test.go +++ b/internal/storage/ledger/transactions_test.go @@ -621,7 +621,7 @@ func TestTransactionsInsert(t *testing.T) { require.NoError(t, migrator.UpByOne(ctx)) } - b := bucket.New(driver.GetDB(), ledgerName) + b := bucket.NewDefault(driver.GetDB(), ledgerName) err := b.AddLedger(ctx, l, driver.GetDB()) require.NoError(t, err) From 2a30286ef57b72e94c7169700c0333dc4baab6d0 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 21 Nov 2024 17:38:41 +0100 Subject: [PATCH 26/71] feat: add system store --- cmd/buckets_upgrade.go | 8 +- cmd/serve.go | 4 +- internal/storage/bucket/bucket.go | 40 +++-- internal/storage/bucket/default_bucket.go | 12 +- .../storage/bucket/default_bucket_test.go | 6 +- internal/storage/bucket/migrations_test.go | 2 +- .../storage/driver/bucket_generated_test.go | 140 ++++++++++++++++++ internal/storage/driver/driver.go | 121 ++++----------- internal/storage/driver/driver_test.go | 9 +- internal/storage/driver/main_test.go | 1 + internal/storage/driver/mocks.go | 4 + internal/storage/driver/module.go | 11 +- internal/storage/driver/rollbacks.go | 29 ++++ internal/storage/ledger/legacy/main_test.go | 8 +- internal/storage/ledger/main_test.go | 40 ++--- internal/storage/ledger/transactions_test.go | 24 ++- internal/storage/system/main_test.go | 23 +++ .../storage/{driver => system}/migrations.go | 26 +--- .../{driver => system}/migrations_test.go | 2 +- internal/storage/system/store.go | 124 ++++++++++++++++ test/e2e/app_lifecycle_test.go | 2 +- test/e2e/suite_test.go | 2 +- test/migrations/upgrade_test.go | 4 +- 23 files changed, 462 insertions(+), 180 deletions(-) create mode 100644 internal/storage/driver/bucket_generated_test.go create mode 100644 internal/storage/driver/mocks.go create mode 100644 internal/storage/driver/rollbacks.go create mode 100644 internal/storage/system/main_test.go rename internal/storage/{driver => system}/migrations.go (91%) rename internal/storage/{driver => system}/migrations_test.go (97%) create mode 100644 internal/storage/system/store.go diff --git a/cmd/buckets_upgrade.go b/cmd/buckets_upgrade.go index 71ca745d6..797d778ad 100644 --- a/cmd/buckets_upgrade.go +++ b/cmd/buckets_upgrade.go @@ -4,7 +4,9 @@ import ( "github.com/formancehq/go-libs/v2/bun/bunconnect" "github.com/formancehq/go-libs/v2/logging" "github.com/formancehq/go-libs/v2/service" + "github.com/formancehq/ledger/internal/storage/bucket" "github.com/formancehq/ledger/internal/storage/driver" + systemstore "github.com/formancehq/ledger/internal/storage/system" "github.com/spf13/cobra" ) @@ -51,7 +53,11 @@ func getDriver(cmd *cobra.Command) (*driver.Driver, error) { return nil, err } - driver := driver.New(db) + driver := driver.New( + db, + systemstore.New(db), + bucket.NewDefaultFactory(db), + ) if err := driver.Initialize(cmd.Context()); err != nil { return nil, err } diff --git a/cmd/serve.go b/cmd/serve.go index 96788e230..1489d3a2c 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -2,6 +2,7 @@ package cmd import ( "github.com/formancehq/go-libs/v2/logging" + systemstore "github.com/formancehq/ledger/internal/storage/system" "net/http" "net/http/pprof" "time" @@ -10,7 +11,6 @@ import ( "github.com/formancehq/go-libs/v2/health" "github.com/formancehq/go-libs/v2/httpserver" "github.com/formancehq/go-libs/v2/otlp" - "github.com/formancehq/ledger/internal/storage/driver" "github.com/go-chi/chi/v5" "go.opentelemetry.io/otel/sdk/metric" @@ -137,7 +137,7 @@ func NewServeCommand() *cobra.Command { otlptraces.AddFlags(cmd.Flags()) auth.AddFlags(cmd.Flags()) publish.AddFlags(ServiceName, cmd.Flags(), func(cd *publish.ConfigDefault) { - cd.PublisherCircuitBreakerSchema = driver.SchemaSystem + cd.PublisherCircuitBreakerSchema = systemstore.SchemaSystem }) iam.AddFlags(cmd.Flags()) diff --git a/internal/storage/bucket/bucket.go b/internal/storage/bucket/bucket.go index 55601bcd1..dc5215b41 100644 --- a/internal/storage/bucket/bucket.go +++ b/internal/storage/bucket/bucket.go @@ -6,25 +6,47 @@ import ( ledger "github.com/formancehq/ledger/internal" "github.com/uptrace/bun" "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" ) type Bucket interface { - Migrate(ctx context.Context, tracer trace.Tracer, minimalVersionReached chan struct{}, opts ...migrations.Option) error - AddLedger(ctx context.Context, ledger ledger.Ledger, db bun.IDB) error + Migrate(ctx context.Context, minimalVersionReached chan struct{}, opts ...migrations.Option) error + AddLedger(ctx context.Context, ledger ledger.Ledger) error HasMinimalVersion(ctx context.Context) (bool, error) GetMigrationsInfo(ctx context.Context) ([]migrations.Info, error) } type Factory interface { - Create(db *bun.DB, name string) Bucket + Create(name string) Bucket } -type DefaultFactory struct {} +type DefaultFactory struct { + tracer trace.Tracer + db *bun.DB +} + +func (f *DefaultFactory) Create(name string) Bucket { + return NewDefault(f.db, f.tracer, name) +} -func (f *DefaultFactory) Create(db *bun.DB, name string) Bucket { - return NewDefault(db, name) +func NewDefaultFactory(db *bun.DB, options ...DefaultFactoryOption) *DefaultFactory { + ret := &DefaultFactory{ + db: db, + } + for _, option := range append(defaultOptions, options...) { + option(ret) + } + return ret } -func NewDefaultFactory() *DefaultFactory { - return &DefaultFactory{} -} \ No newline at end of file +type DefaultFactoryOption func(factory *DefaultFactory) + +func WithTracer(tracer trace.Tracer) DefaultFactoryOption { + return func(factory *DefaultFactory) { + factory.tracer = tracer + } +} + +var defaultOptions = []DefaultFactoryOption{ + WithTracer(noop.Tracer{}), +} diff --git a/internal/storage/bucket/default_bucket.go b/internal/storage/bucket/default_bucket.go index 6b583d49e..17805b41d 100644 --- a/internal/storage/bucket/default_bucket.go +++ b/internal/storage/bucket/default_bucket.go @@ -19,10 +19,11 @@ const MinimalSchemaVersion = 12 type DefaultBucket struct { name string db *bun.DB + tracer trace.Tracer } -func (b *DefaultBucket) Migrate(ctx context.Context, tracer trace.Tracer, minimalVersionReached chan struct{}, options ...migrations.Option) error { - return migrate(ctx, tracer, b.db, b.name, minimalVersionReached, options...) +func (b *DefaultBucket) Migrate(ctx context.Context, minimalVersionReached chan struct{}, options ...migrations.Option) error { + return migrate(ctx, b.tracer, b.db, b.name, minimalVersionReached, options...) } func (b *DefaultBucket) HasMinimalVersion(ctx context.Context) (bool, error) { @@ -39,7 +40,7 @@ func (b *DefaultBucket) GetMigrationsInfo(ctx context.Context) ([]migrations.Inf return GetMigrator(b.db, b.name).GetMigrations(ctx) } -func (b *DefaultBucket) AddLedger(ctx context.Context, l ledger.Ledger, db bun.IDB) error { +func (b *DefaultBucket) AddLedger(ctx context.Context, l ledger.Ledger) error { for _, setup := range ledgerSetups { if l.Features.Match(setup.requireFeatures) { @@ -49,7 +50,7 @@ func (b *DefaultBucket) AddLedger(ctx context.Context, l ledger.Ledger, db bun.I return fmt.Errorf("executing template: %w", err) } - _, err := db.ExecContext(ctx, buf.String()) + _, err := b.db.ExecContext(ctx, buf.String()) if err != nil { return fmt.Errorf("executing sql: %w", err) } @@ -59,10 +60,11 @@ func (b *DefaultBucket) AddLedger(ctx context.Context, l ledger.Ledger, db bun.I return nil } -func NewDefault(db *bun.DB, name string) *DefaultBucket { +func NewDefault(db *bun.DB, tracer trace.Tracer, name string) *DefaultBucket { return &DefaultBucket{ db: db, name: name, + tracer: tracer, } } diff --git a/internal/storage/bucket/default_bucket_test.go b/internal/storage/bucket/default_bucket_test.go index 1201be4ac..26d94ad8e 100644 --- a/internal/storage/bucket/default_bucket_test.go +++ b/internal/storage/bucket/default_bucket_test.go @@ -5,7 +5,7 @@ package bucket_test import ( "github.com/formancehq/go-libs/v2/bun/bundebug" "github.com/formancehq/ledger/internal/storage/bucket" - "github.com/formancehq/ledger/internal/storage/driver" + "github.com/formancehq/ledger/internal/storage/system" "go.opentelemetry.io/otel/trace/noop" "testing" @@ -30,6 +30,6 @@ func TestBuckets(t *testing.T) { require.NoError(t, driver.Migrate(ctx, db)) - b := bucket.NewDefault(db, name) - require.NoError(t, b.Migrate(ctx, noop.Tracer{}, make(chan struct{}))) + b := bucket.NewDefault(db, noop.Tracer{}, name) + require.NoError(t, b.Migrate(ctx, make(chan struct{}))) } diff --git a/internal/storage/bucket/migrations_test.go b/internal/storage/bucket/migrations_test.go index f86f49809..abd7723d6 100644 --- a/internal/storage/bucket/migrations_test.go +++ b/internal/storage/bucket/migrations_test.go @@ -7,7 +7,7 @@ import ( "github.com/formancehq/go-libs/v2/logging" "github.com/formancehq/go-libs/v2/migrations" "github.com/formancehq/ledger/internal/storage/bucket" - "github.com/formancehq/ledger/internal/storage/driver" + "github.com/formancehq/ledger/internal/storage/system" "github.com/google/uuid" _ "github.com/jackc/pgx/v5/stdlib" "github.com/stretchr/testify/require" diff --git a/internal/storage/driver/bucket_generated_test.go b/internal/storage/driver/bucket_generated_test.go new file mode 100644 index 000000000..0820838c9 --- /dev/null +++ b/internal/storage/driver/bucket_generated_test.go @@ -0,0 +1,140 @@ +// Code generated by MockGen. DO NOT EDIT. +// +// Generated by this command: +// +// mockgen -write_source_comment=false -write_package_comment=false -source ../bucket/bucket.go -destination bucket_generated_test.go -package driver . Factory +package driver + +import ( + context "context" + reflect "reflect" + + migrations "github.com/formancehq/go-libs/v2/migrations" + ledger "github.com/formancehq/ledger/internal" + bucket "github.com/formancehq/ledger/internal/storage/bucket" + bun "github.com/uptrace/bun" + gomock "go.uber.org/mock/gomock" +) + +// MockBucket is a mock of Bucket interface. +type MockBucket struct { + ctrl *gomock.Controller + recorder *MockBucketMockRecorder +} + +// MockBucketMockRecorder is the mock recorder for MockBucket. +type MockBucketMockRecorder struct { + mock *MockBucket +} + +// NewMockBucket creates a new mock instance. +func NewMockBucket(ctrl *gomock.Controller) *MockBucket { + mock := &MockBucket{ctrl: ctrl} + mock.recorder = &MockBucketMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBucket) EXPECT() *MockBucketMockRecorder { + return m.recorder +} + +// AddLedger mocks base method. +func (m *MockBucket) AddLedger(ctx context.Context, ledger ledger.Ledger) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddLedger", ctx, ledger) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddLedger indicates an expected call of AddLedger. +func (mr *MockBucketMockRecorder) AddLedger(ctx, ledger any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddLedger", reflect.TypeOf((*MockBucket)(nil).AddLedger), ctx, ledger) +} + +// GetMigrationsInfo mocks base method. +func (m *MockBucket) GetMigrationsInfo(ctx context.Context) ([]migrations.Info, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMigrationsInfo", ctx) + ret0, _ := ret[0].([]migrations.Info) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMigrationsInfo indicates an expected call of GetMigrationsInfo. +func (mr *MockBucketMockRecorder) GetMigrationsInfo(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMigrationsInfo", reflect.TypeOf((*MockBucket)(nil).GetMigrationsInfo), ctx) +} + +// HasMinimalVersion mocks base method. +func (m *MockBucket) HasMinimalVersion(ctx context.Context) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HasMinimalVersion", ctx) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HasMinimalVersion indicates an expected call of HasMinimalVersion. +func (mr *MockBucketMockRecorder) HasMinimalVersion(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasMinimalVersion", reflect.TypeOf((*MockBucket)(nil).HasMinimalVersion), ctx) +} + +// Migrate mocks base method. +func (m *MockBucket) Migrate(ctx context.Context, minimalVersionReached chan struct{}, opts ...migrations.Option) error { + m.ctrl.T.Helper() + varargs := []any{ctx, minimalVersionReached} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Migrate", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Migrate indicates an expected call of Migrate. +func (mr *MockBucketMockRecorder) Migrate(ctx, minimalVersionReached any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, minimalVersionReached}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Migrate", reflect.TypeOf((*MockBucket)(nil).Migrate), varargs...) +} + +// MockFactory is a mock of Factory interface. +type MockFactory struct { + ctrl *gomock.Controller + recorder *MockFactoryMockRecorder +} + +// MockFactoryMockRecorder is the mock recorder for MockFactory. +type MockFactoryMockRecorder struct { + mock *MockFactory +} + +// NewMockFactory creates a new mock instance. +func NewMockFactory(ctrl *gomock.Controller) *MockFactory { + mock := &MockFactory{ctrl: ctrl} + mock.recorder = &MockFactoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockFactory) EXPECT() *MockFactoryMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockFactory) Create(db *bun.DB, name string) bucket.Bucket { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", db, name) + ret0, _ := ret[0].(bucket.Bucket) + return ret0 +} + +// Create indicates an expected call of Create. +func (mr *MockFactoryMockRecorder) Create(db, name any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockFactory)(nil).Create), db, name) +} diff --git a/internal/storage/driver/driver.go b/internal/storage/driver/driver.go index 9ea2750bb..49ce60d51 100644 --- a/internal/storage/driver/driver.go +++ b/internal/storage/driver/driver.go @@ -8,6 +8,7 @@ import ( "github.com/formancehq/go-libs/v2/migrations" "github.com/formancehq/go-libs/v2/platform/postgres" systemcontroller "github.com/formancehq/ledger/internal/controller/system" + systemstore "github.com/formancehq/ledger/internal/storage/system" "go.opentelemetry.io/otel/metric" noopmetrics "go.opentelemetry.io/otel/metric/noop" "go.opentelemetry.io/otel/trace" @@ -26,12 +27,9 @@ import ( "github.com/formancehq/go-libs/v2/logging" ) -const ( - SchemaSystem = "_system" -) - type Driver struct { db *bun.DB + systemStore systemstore.Store bucketFactory bucket.Factory tracer trace.Tracer meter metric.Meter @@ -44,28 +42,23 @@ func (d *Driver) CreateLedger(ctx context.Context, l *ledger.Ledger) (*ledgersto l.Metadata = metadata.Metadata{} } - b := d.bucketFactory.Create(d.db, l.Bucket) + b := d.bucketFactory.Create(l.Bucket) if err := b.Migrate( ctx, - d.tracer, make(chan struct{}), migrations.WithLockRetryInterval(d.migratorLockRetryInterval), ); err != nil { return nil, fmt.Errorf("migrating bucket: %w", err) } - _, err := d.db.NewInsert(). - Model(l). - Returning("id, added_at"). - Exec(ctx) - if err != nil { + if err := d.systemStore.CreateLedger(ctx, l); err != nil { if errors.Is(postgres.ResolveError(err), postgres.ErrConstraintsFailed{}) { return nil, systemcontroller.ErrLedgerAlreadyExists } return nil, postgres.ResolveError(err) } - if err := b.AddLedger(ctx, *l, d.db); err != nil { + if err := b.AddLedger(ctx, *l); err != nil { return nil, fmt.Errorf("adding ledger to bucket: %w", err) } @@ -79,18 +72,14 @@ func (d *Driver) CreateLedger(ctx context.Context, l *ledger.Ledger) (*ledgersto } func (d *Driver) OpenLedger(ctx context.Context, name string) (*ledgerstore.Store, *ledger.Ledger, error) { - ret := &ledger.Ledger{} - if err := d.db.NewSelect(). - Model(ret). - Column("*"). - Where("name = ?", name). - Scan(ctx); err != nil { - return nil, nil, postgres.ResolveError(err) + ret, err := d.systemStore.GetLedger(ctx, name) + if err != nil { + return nil, nil, err } return ledgerstore.New( d.db, - d.bucketFactory.Create(d.db, ret.Bucket), + d.bucketFactory.Create(ret.Bucket), *ret, ledgerstore.WithMeter(d.meter), ledgerstore.WithTracer(d.tracer), @@ -104,7 +93,7 @@ func (d *Driver) Initialize(ctx context.Context) error { return fmt.Errorf("detecting rollbacks: %w", err) } - err = Migrate(ctx, d.db, migrations.WithLockRetryInterval(d.migratorLockRetryInterval)) + err = systemstore.Migrate(ctx, d.db, migrations.WithLockRetryInterval(d.migratorLockRetryInterval)) if err != nil { constraintsFailed := postgres.ErrConstraintsFailed{} if errors.As(err, &constraintsFailed) && @@ -123,30 +112,22 @@ func (d *Driver) Initialize(ctx context.Context) error { func (d *Driver) detectRollbacks(ctx context.Context) error { logging.FromContext(ctx).Debugf("Checking for downgrades on system schema") - if err := detectDowngrades(GetMigrator(d.db), ctx); err != nil { + if err := detectDowngrades(systemstore.GetMigrator(d.db), ctx); err != nil { return fmt.Errorf("detecting rollbacks of system schema: %w", err) } - type row struct { - Bucket string `bun:"bucket"` - } - rows := make([]row, 0) - if err := d.db.NewSelect(). - DistinctOn("bucket"). - ModelTableExpr("_system.ledgers"). - Column("bucket"). - Scan(ctx, &rows); err != nil { - err = postgres.ResolveError(err) - if errors.Is(err, postgres.ErrMissingTable) { - return nil + buckets, err := d.systemStore.GetDistinctBuckets(ctx) + if err != nil { + if !errors.Is(err, postgres.ErrMissingTable) { + return fmt.Errorf("getting distinct buckets: %w", err) } - return err + return nil } - for _, r := range rows { - logging.FromContext(ctx).Debugf("Checking for downgrades on bucket '%s'", r.Bucket) - if err := detectDowngrades(bucket.GetMigrator(d.db, r.Bucket), ctx); err != nil { - return fmt.Errorf("detecting rollbacks on bucket '%s': %w", r.Bucket, err) + for _, b := range buckets { + logging.FromContext(ctx).Debugf("Checking for downgrades on bucket '%s'", b) + if err := detectDowngrades(bucket.GetMigrator(d.db, b), ctx); err != nil { + return fmt.Errorf("detecting rollbacks on bucket '%s': %w", b, err) } } @@ -154,53 +135,24 @@ func (d *Driver) detectRollbacks(ctx context.Context) error { } func (d *Driver) UpdateLedgerMetadata(ctx context.Context, name string, m metadata.Metadata) error { - _, err := d.db.NewUpdate(). - Model(&ledger.Ledger{}). - Set("metadata = metadata || ?", m). - Where("name = ?", name). - Exec(ctx) - return err + return d.systemStore.UpdateLedgerMetadata(ctx, name, m) } func (d *Driver) DeleteLedgerMetadata(ctx context.Context, name string, key string) error { - _, err := d.db.NewUpdate(). - Model(&ledger.Ledger{}). - Set("metadata = metadata - ?", key). - Where("name = ?", name). - Exec(ctx) - return err + return d.systemStore.DeleteLedgerMetadata(ctx, name, key) } func (d *Driver) ListLedgers(ctx context.Context, q ledgercontroller.ListLedgersQuery) (*bunpaginate.Cursor[ledger.Ledger], error) { - query := d.db.NewSelect(). - Model(&ledger.Ledger{}). - Column("*"). - Order("added_at asc") - - return bunpaginate.UsingOffset[ledgercontroller.PaginatedQueryOptions[struct{}], ledger.Ledger]( - ctx, - query, - bunpaginate.OffsetPaginatedQuery[ledgercontroller.PaginatedQueryOptions[struct{}]](q), - ) + return d.systemStore.ListLedgers(ctx, q) } func (d *Driver) GetLedger(ctx context.Context, name string) (*ledger.Ledger, error) { - ret := &ledger.Ledger{} - if err := d.db.NewSelect(). - Model(ret). - Column("*"). - Where("name = ?", name). - Scan(ctx); err != nil { - return nil, postgres.ResolveError(err) - } - - return ret, nil + return d.systemStore.GetLedger(ctx, name) } func (d *Driver) UpgradeBucket(ctx context.Context, name string) error { - return d.bucketFactory.Create(d.db, name).Migrate( + return d.bucketFactory.Create(name).Migrate( ctx, - d.tracer, make(chan struct{}), migrations.WithLockRetryInterval(d.migratorLockRetryInterval), ) @@ -208,14 +160,9 @@ func (d *Driver) UpgradeBucket(ctx context.Context, name string) error { func (d *Driver) UpgradeAllBuckets(ctx context.Context, minimalVersionReached chan struct{}) error { - var buckets []string - err := d.db.NewSelect(). - DistinctOn("bucket"). - Model(&ledger.Ledger{}). - Column("bucket"). - Scan(ctx, &buckets) + buckets, err := d.systemStore.GetDistinctBuckets(ctx) if err != nil { - return fmt.Errorf("getting buckets: %w", err) + return fmt.Errorf("getting distinct buckets: %w", err) } sem := make(chan struct{}, len(buckets)) @@ -223,7 +170,7 @@ func (d *Driver) UpgradeAllBuckets(ctx context.Context, minimalVersionReached ch grp, ctx := errgroup.WithContext(ctx) for _, bucketName := range buckets { grp.Go(func() error { - b := d.bucketFactory.Create(d.db, bucketName) + b := d.bucketFactory.Create(bucketName) minimalVersionReached := make(chan struct{}) @@ -239,7 +186,6 @@ func (d *Driver) UpgradeAllBuckets(ctx context.Context, minimalVersionReached ch logging.FromContext(ctx).Infof("Upgrading bucket '%s'", bucketName) if err := b.Migrate( ctx, - d.tracer, minimalVersionReached, migrations.WithLockRetryInterval(d.migratorLockRetryInterval), ); err != nil { @@ -268,9 +214,11 @@ func (d *Driver) GetDB() *bun.DB { return d.db } -func New(db *bun.DB, opts ...Option) *Driver { +func New(db *bun.DB, systemStore systemstore.Store, bucketFactory bucket.Factory, opts ...Option) *Driver { ret := &Driver{ db: db, + bucketFactory: bucketFactory, + systemStore: systemStore, } for _, opt := range append(defaultOptions, opts...) { opt(ret) @@ -298,14 +246,7 @@ func WithMigratorLockRetryInterval(interval time.Duration) Option { } } -func WithBucketFactory(factory bucket.Factory) Option { - return func(d *Driver) { - d.bucketFactory = factory - } -} - var defaultOptions = []Option{ WithMeter(noopmetrics.Meter{}), WithTracer(nooptracer.Tracer{}), - WithBucketFactory(bucket.NewDefaultFactory()), } diff --git a/internal/storage/driver/driver_test.go b/internal/storage/driver/driver_test.go index cc45c6425..73de97337 100644 --- a/internal/storage/driver/driver_test.go +++ b/internal/storage/driver/driver_test.go @@ -13,7 +13,9 @@ import ( "github.com/formancehq/go-libs/v2/testing/docker" ledger "github.com/formancehq/ledger/internal" ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" + "github.com/formancehq/ledger/internal/storage/bucket" "github.com/formancehq/ledger/internal/storage/driver" + systemstore "github.com/formancehq/ledger/internal/storage/system" "github.com/google/uuid" "github.com/uptrace/bun" "golang.org/x/sync/errgroup" @@ -181,7 +183,12 @@ func newStorageDriver(t docker.T, driverOptions ...driver.Option) *driver.Driver db, err := bunconnect.OpenSQLDB(ctx, pgDatabase.ConnectionOptions(), hooks...) require.NoError(t, err) - d := driver.New(db, driverOptions...) + d := driver.New( + db, + systemstore.New(db), + bucket.NewDefaultFactory(db), + driverOptions..., + ) require.NoError(t, d.Initialize(logging.TestingContext())) diff --git a/internal/storage/driver/main_test.go b/internal/storage/driver/main_test.go index 017d038d0..f07b55c5b 100644 --- a/internal/storage/driver/main_test.go +++ b/internal/storage/driver/main_test.go @@ -18,6 +18,7 @@ func TestMain(m *testing.M) { utils.WithTestMain(func(t *utils.TestingTForMain) int { srv = pgtesting.CreatePostgresServer(t, docker.NewPool(t, logging.Testing())) + return m.Run() }) } diff --git a/internal/storage/driver/mocks.go b/internal/storage/driver/mocks.go new file mode 100644 index 000000000..c83ea64d1 --- /dev/null +++ b/internal/storage/driver/mocks.go @@ -0,0 +1,4 @@ +//go:generate mockgen -write_source_comment=false -write_package_comment=false -source ../bucket/bucket.go -destination bucket_generated_test.go -package driver . Bucket +//go:generate mockgen -write_source_comment=false -write_package_comment=false -source ../bucket/bucket.go -destination bucket_generated_test.go -package driver . Factory + +package driver diff --git a/internal/storage/driver/module.go b/internal/storage/driver/module.go index 58bfede53..e0a85b142 100644 --- a/internal/storage/driver/module.go +++ b/internal/storage/driver/module.go @@ -3,6 +3,7 @@ package driver import ( "context" "github.com/formancehq/ledger/internal/storage/bucket" + systemstore "github.com/formancehq/ledger/internal/storage/system" "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/trace" @@ -23,14 +24,22 @@ type ModuleConfiguration struct { func NewFXModule() fx.Option { return fx.Options( - fx.Provide(fx.Annotate(bucket.NewDefaultFactory, fx.As(new(bucket.Factory)))), + fx.Provide(fx.Annotate(func(db *bun.DB, tracerProvider trace.TracerProvider) bucket.Factory { + return bucket.NewDefaultFactory(db, bucket.WithTracer(tracerProvider.Tracer("store"))) + })), + fx.Provide(func(db *bun.DB) systemstore.Store { + return systemstore.New(db) + }), fx.Provide(func( db *bun.DB, bucketFactory bucket.Factory, + systemStore systemstore.Store, tracerProvider trace.TracerProvider, meterProvider metric.MeterProvider, ) (*Driver, error) { return New(db, + systemStore, + bucketFactory, WithMeter(meterProvider.Meter("store")), WithTracer(tracerProvider.Tracer("store")), ), nil diff --git a/internal/storage/driver/rollbacks.go b/internal/storage/driver/rollbacks.go new file mode 100644 index 000000000..0201e58d0 --- /dev/null +++ b/internal/storage/driver/rollbacks.go @@ -0,0 +1,29 @@ +package driver + +import ( + "context" + "errors" + "fmt" + "github.com/formancehq/go-libs/v2/migrations" +) + +func detectDowngrades(migrator *migrations.Migrator, ctx context.Context) error { + lastVersion, err := migrator.GetLastVersion(ctx) + if err != nil { + if !errors.Is(err, migrations.ErrMissingVersionTable) { + return fmt.Errorf("failed to get last version: %w", err) + } + } + if err == nil && lastVersion != -1 { + allMigrations, err := migrator.GetMigrations(ctx) + if err != nil { + return fmt.Errorf("failed to get all migrations: %w", err) + } + + if len(allMigrations) < lastVersion { + return newErrRollbackDetected(lastVersion, len(allMigrations)) + } + } + + return nil +} diff --git a/internal/storage/ledger/legacy/main_test.go b/internal/storage/ledger/legacy/main_test.go index 67b2fe138..364414da3 100644 --- a/internal/storage/ledger/legacy/main_test.go +++ b/internal/storage/ledger/legacy/main_test.go @@ -7,9 +7,9 @@ import ( "github.com/formancehq/go-libs/v2/testing/docker" "github.com/formancehq/go-libs/v2/testing/utils" "github.com/formancehq/ledger/internal/storage/bucket" - systemstore "github.com/formancehq/ledger/internal/storage/driver" ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" "github.com/formancehq/ledger/internal/storage/ledger/legacy" + systemstore "github.com/formancehq/ledger/internal/storage/system" "go.opentelemetry.io/otel/trace/noop" "os" "testing" @@ -68,9 +68,9 @@ func newLedgerStore(t T) *testStore { l := ledger.MustNewWithDefault(ledgerName) - b := bucket.NewDefault(db, ledger.DefaultBucket) - require.NoError(t, b.Migrate(ctx, noop.Tracer{}, make(chan struct{}))) - require.NoError(t, b.AddLedger(ctx, l, db)) + b := bucket.NewDefault(db, noop.Tracer{}, ledger.DefaultBucket) + require.NoError(t, b.Migrate(ctx, make(chan struct{}))) + require.NoError(t, b.AddLedger(ctx, l)) return &testStore{ Store: legacy.New(db, ledger.DefaultBucket, l.Name), diff --git a/internal/storage/ledger/main_test.go b/internal/storage/ledger/main_test.go index 8b72798d2..8f4b5ea0a 100644 --- a/internal/storage/ledger/main_test.go +++ b/internal/storage/ledger/main_test.go @@ -4,10 +4,11 @@ package ledger_test import ( "database/sql" - "github.com/formancehq/go-libs/v2/bun/bunconnect" . "github.com/formancehq/go-libs/v2/testing/utils" + "github.com/formancehq/ledger/internal/storage/bucket" "github.com/formancehq/ledger/internal/storage/driver" ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" + systemstore "github.com/formancehq/ledger/internal/storage/system" "math/big" "os" "testing" @@ -28,8 +29,9 @@ import ( ) var ( - srv = NewDeferred[*pgtesting.PostgresServer]() - bunDB = NewDeferred[*bun.DB]() + srv = NewDeferred[*pgtesting.PostgresServer]() + defaultBunDB = NewDeferred[*bun.DB]() + defaultDriver = NewDeferred[*driver.Driver]() ) func TestMain(m *testing.M) { @@ -37,7 +39,7 @@ func TestMain(m *testing.M) { srv.LoadAsync(func() *pgtesting.PostgresServer { ret := pgtesting.CreatePostgresServer(t, docker.NewPool(t, logging.Testing()), pgtesting.WithExtension("pgcrypto")) - bunDB.LoadAsync(func() *bun.DB { + defaultBunDB.LoadAsync(func() *bun.DB { db, err := sql.Open("pgx", ret.GetDSN()) require.NoError(t, err) @@ -47,7 +49,8 @@ func TestMain(m *testing.M) { } bunDB.SetMaxOpenConns(100) - require.NoError(t, driver.Migrate(logging.TestingContext(), bunDB)) + require.NoError(t, systemstore.Migrate(logging.TestingContext(), bunDB)) + defaultDriver.SetValue(driver.New(bunDB, systemstore.New(bunDB), bucket.NewDefaultFactory(bunDB))) return bunDB }) @@ -64,38 +67,17 @@ type T interface { Cleanup(func()) } -func newDriver(t T) *driver.Driver { - t.Helper() - - ctx := logging.TestingContext() - - Wait(srv, bunDB) - - pgDatabase := srv.GetValue().NewDatabase(t) - - hooks := make([]bun.QueryHook, 0) - if os.Getenv("DEBUG") == "true" { - hooks = append(hooks, bundebug.NewQueryHook()) - } - - db, err := bunconnect.OpenSQLDB(ctx, pgDatabase.ConnectionOptions(), hooks...) - require.NoError(t, err) - - require.NoError(t, driver.Migrate(ctx, db)) - - return driver.New(bunDB.GetValue()) -} - func newLedgerStore(t T) *ledgerstore.Store { t.Helper() - driver := newDriver(t) + <-defaultDriver.Done() + ledgerName := uuid.NewString()[:8] ctx := logging.TestingContext() l := ledger.MustNewWithDefault(ledgerName) - store, err := driver.CreateLedger(ctx, &l) + store, err := defaultDriver.GetValue().CreateLedger(ctx, &l) require.NoError(t, err) return store diff --git a/internal/storage/ledger/transactions_test.go b/internal/storage/ledger/transactions_test.go index 990472852..2b0340f94 100644 --- a/internal/storage/ledger/transactions_test.go +++ b/internal/storage/ledger/transactions_test.go @@ -7,9 +7,13 @@ import ( "database/sql" "fmt" "github.com/alitto/pond" + "github.com/formancehq/go-libs/v2/bun/bunconnect" "github.com/formancehq/ledger/internal/storage/bucket" + driver "github.com/formancehq/ledger/internal/storage/driver" ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" + systemstore "github.com/formancehq/ledger/internal/storage/system" "github.com/google/uuid" + "go.opentelemetry.io/otel/trace/noop" "math/big" "slices" "testing" @@ -188,7 +192,7 @@ func TestTransactionDeleteMetadata(t *testing.T) { require.NoError(t, err) // Get from database and check metadata presence - tx, err := store.GetTransaction(context.Background(), ledgercontroller.NewGetTransactionQuery(tx1.ID)) + tx, err := store.GetTransaction(ctx, ledgercontroller.NewGetTransactionQuery(tx1.ID)) require.NoError(t, err) require.Equal(t, tx.Metadata, metadata.Metadata{"foo1": "bar1", "foo2": "bar2"}) @@ -197,7 +201,7 @@ func TestTransactionDeleteMetadata(t *testing.T) { require.NoError(t, err) require.True(t, modified) - tx, err = store.GetTransaction(context.Background(), ledgercontroller.NewGetTransactionQuery(tx1.ID)) + tx, err = store.GetTransaction(ctx, ledgercontroller.NewGetTransactionQuery(tx1.ID)) require.NoError(t, err) require.Equal(t, metadata.Metadata{"foo2": "bar2"}, tx.Metadata) @@ -610,7 +614,17 @@ func TestTransactionsInsert(t *testing.T) { t.Run("check reference conflict with minimal store version", func(t *testing.T) { t.Parallel() - driver := newDriver(t) + // Waiting for the pg server to be ready + <-srv.Done() + + // Create a dedicated database for this test as we want to run migrations only until the minimal schema version + db := srv.GetValue().NewDatabase(t) + bunDB, err := bunconnect.OpenSQLDB(ctx, db.ConnectionOptions()) + require.NoError(t, err) + + driver := driver.New(bunDB, systemstore.New(bunDB), bucket.NewDefaultFactory(bunDB)) + require.NoError(t, driver.Initialize(ctx)) + ledgerName := uuid.NewString()[:8] l := ledger.MustNewWithDefault(ledgerName) @@ -621,8 +635,8 @@ func TestTransactionsInsert(t *testing.T) { require.NoError(t, migrator.UpByOne(ctx)) } - b := bucket.NewDefault(driver.GetDB(), ledgerName) - err := b.AddLedger(ctx, l, driver.GetDB()) + b := bucket.NewDefault(driver.GetDB(), noop.Tracer{}, ledgerName) + err = b.AddLedger(ctx, l) require.NoError(t, err) store := ledgerstore.New(driver.GetDB(), b, l) diff --git a/internal/storage/system/main_test.go b/internal/storage/system/main_test.go new file mode 100644 index 000000000..017d038d0 --- /dev/null +++ b/internal/storage/system/main_test.go @@ -0,0 +1,23 @@ +//go:build it + +package driver_test + +import ( + "testing" + + "github.com/formancehq/go-libs/v2/testing/docker" + "github.com/formancehq/go-libs/v2/testing/utils" + + "github.com/formancehq/go-libs/v2/logging" + "github.com/formancehq/go-libs/v2/testing/platform/pgtesting" +) + +var srv *pgtesting.PostgresServer + +func TestMain(m *testing.M) { + utils.WithTestMain(func(t *utils.TestingTForMain) int { + srv = pgtesting.CreatePostgresServer(t, docker.NewPool(t, logging.Testing())) + + return m.Run() + }) +} diff --git a/internal/storage/driver/migrations.go b/internal/storage/system/migrations.go similarity index 91% rename from internal/storage/driver/migrations.go rename to internal/storage/system/migrations.go index 890f8ee8d..d33f627d4 100644 --- a/internal/storage/driver/migrations.go +++ b/internal/storage/system/migrations.go @@ -3,11 +3,8 @@ package driver import ( "context" "database/sql" - "errors" - "fmt" - "github.com/formancehq/go-libs/v2/time" - "github.com/formancehq/go-libs/v2/platform/postgres" + "github.com/formancehq/go-libs/v2/time" "github.com/formancehq/go-libs/v2/migrations" "github.com/uptrace/bun" @@ -213,27 +210,6 @@ func Migrate(ctx context.Context, db *bun.DB, options ...migrations.Option) erro return GetMigrator(db, options...).Up(ctx) } -func detectDowngrades(migrator *migrations.Migrator, ctx context.Context) error { - lastVersion, err := migrator.GetLastVersion(ctx) - if err != nil { - if !errors.Is(err, migrations.ErrMissingVersionTable) { - return fmt.Errorf("failed to get last version: %w", err) - } - } - if err == nil && lastVersion != -1 { - allMigrations, err := migrator.GetMigrations(ctx) - if err != nil { - return fmt.Errorf("failed to get all migrations: %w", err) - } - - if len(allMigrations) < lastVersion { - return newErrRollbackDetected(lastVersion, len(allMigrations)) - } - } - - return nil -} - const aggregateObjects = ` create or replace function public.jsonb_concat(a jsonb, b jsonb) returns jsonb as 'select $1 || $2' diff --git a/internal/storage/driver/migrations_test.go b/internal/storage/system/migrations_test.go similarity index 97% rename from internal/storage/driver/migrations_test.go rename to internal/storage/system/migrations_test.go index ceefe2e70..4d5c98811 100644 --- a/internal/storage/driver/migrations_test.go +++ b/internal/storage/system/migrations_test.go @@ -6,7 +6,7 @@ import ( "context" "fmt" "github.com/formancehq/go-libs/v2/testing/migrations" - "github.com/formancehq/ledger/internal/storage/driver" + "github.com/formancehq/ledger/internal/storage/system" "os" "testing" diff --git a/internal/storage/system/store.go b/internal/storage/system/store.go new file mode 100644 index 000000000..151ba429d --- /dev/null +++ b/internal/storage/system/store.go @@ -0,0 +1,124 @@ +package driver + +import ( + "context" + "errors" + "fmt" + "github.com/formancehq/go-libs/v2/metadata" + "github.com/formancehq/go-libs/v2/platform/postgres" + ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" + systemcontroller "github.com/formancehq/ledger/internal/controller/system" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/trace" + + "github.com/formancehq/go-libs/v2/bun/bunpaginate" + ledger "github.com/formancehq/ledger/internal" + "github.com/uptrace/bun" +) + +type Store interface { + CreateLedger(ctx context.Context, l *ledger.Ledger) error + DeleteLedgerMetadata(ctx context.Context, name string, key string) error + UpdateLedgerMetadata(ctx context.Context, name string, m metadata.Metadata) error + ListLedgers(ctx context.Context, q ledgercontroller.ListLedgersQuery) (*bunpaginate.Cursor[ledger.Ledger], error) + GetLedger(ctx context.Context, name string) (*ledger.Ledger, error) + GetDistinctBuckets(ctx context.Context) ([]string, error) +} + +const ( + SchemaSystem = "_system" +) + +type DefaultStore struct { + db *bun.DB + tracer trace.Tracer + meter metric.Meter +} + +func (d *DefaultStore) GetDistinctBuckets(ctx context.Context) ([]string, error) { + var buckets []string + err := d.db.NewSelect(). + DistinctOn("bucket"). + Model(&ledger.Ledger{}). + Column("bucket"). + Scan(ctx, &buckets) + if err != nil { + return nil, fmt.Errorf("getting buckets: %w", postgres.ResolveError(err)) + } + + return buckets, nil +} + +func (d *DefaultStore) CreateLedger(ctx context.Context, l *ledger.Ledger) error { + + if l.Metadata == nil { + l.Metadata = metadata.Metadata{} + } + + _, err := d.db.NewInsert(). + Model(l). + Returning("id, added_at"). + Exec(ctx) + if err != nil { + if errors.Is(postgres.ResolveError(err), postgres.ErrConstraintsFailed{}) { + return systemcontroller.ErrLedgerAlreadyExists + } + return postgres.ResolveError(err) + } + + return nil +} + +func (d *DefaultStore) UpdateLedgerMetadata(ctx context.Context, name string, m metadata.Metadata) error { + _, err := d.db.NewUpdate(). + Model(&ledger.Ledger{}). + Set("metadata = metadata || ?", m). + Where("name = ?", name). + Exec(ctx) + return err +} + +func (d *DefaultStore) DeleteLedgerMetadata(ctx context.Context, name string, key string) error { + _, err := d.db.NewUpdate(). + Model(&ledger.Ledger{}). + Set("metadata = metadata - ?", key). + Where("name = ?", name). + Exec(ctx) + return err +} + +func (d *DefaultStore) ListLedgers(ctx context.Context, q ledgercontroller.ListLedgersQuery) (*bunpaginate.Cursor[ledger.Ledger], error) { + query := d.db.NewSelect(). + Model(&ledger.Ledger{}). + Column("*"). + Order("added_at asc") + + return bunpaginate.UsingOffset[ledgercontroller.PaginatedQueryOptions[struct{}], ledger.Ledger]( + ctx, + query, + bunpaginate.OffsetPaginatedQuery[ledgercontroller.PaginatedQueryOptions[struct{}]](q), + ) +} + +func (d *DefaultStore) GetLedger(ctx context.Context, name string) (*ledger.Ledger, error) { + ret := &ledger.Ledger{} + if err := d.db.NewSelect(). + Model(ret). + Column("*"). + Where("name = ?", name). + Scan(ctx); err != nil { + return nil, postgres.ResolveError(err) + } + + return ret, nil +} + +func (d *DefaultStore) GetDB() *bun.DB { + return d.db +} + +func New(db *bun.DB) *DefaultStore { + return &DefaultStore{ + db: db, + } +} diff --git a/test/e2e/app_lifecycle_test.go b/test/e2e/app_lifecycle_test.go index a591a46ae..badcb6bc5 100644 --- a/test/e2e/app_lifecycle_test.go +++ b/test/e2e/app_lifecycle_test.go @@ -12,7 +12,7 @@ import ( "github.com/formancehq/go-libs/v2/time" ledger "github.com/formancehq/ledger/internal" "github.com/formancehq/ledger/internal/storage/bucket" - "github.com/formancehq/ledger/internal/storage/driver" + "github.com/formancehq/ledger/internal/storage/system" "github.com/formancehq/ledger/pkg/client/models/components" "github.com/formancehq/ledger/pkg/client/models/operations" ledgerevents "github.com/formancehq/ledger/pkg/events" diff --git a/test/e2e/suite_test.go b/test/e2e/suite_test.go index 571d50c1f..329004535 100644 --- a/test/e2e/suite_test.go +++ b/test/e2e/suite_test.go @@ -9,7 +9,7 @@ import ( "github.com/formancehq/go-libs/v2/testing/platform/natstesting" ledger "github.com/formancehq/ledger/internal" "github.com/formancehq/ledger/internal/storage/bucket" - "github.com/formancehq/ledger/internal/storage/driver" + "github.com/formancehq/ledger/internal/storage/system" "os" "testing" diff --git a/test/migrations/upgrade_test.go b/test/migrations/upgrade_test.go index 412fcbed3..e609374d2 100644 --- a/test/migrations/upgrade_test.go +++ b/test/migrations/upgrade_test.go @@ -7,7 +7,9 @@ import ( "github.com/formancehq/go-libs/v2/logging" "github.com/formancehq/go-libs/v2/testing/docker" "github.com/formancehq/go-libs/v2/testing/platform/pgtesting" + "github.com/formancehq/ledger/internal/storage/bucket" "github.com/formancehq/ledger/internal/storage/driver" + systemstore "github.com/formancehq/ledger/internal/storage/system" "github.com/ory/dockertest/v3" dockerlib "github.com/ory/dockertest/v3/docker" "github.com/stretchr/testify/require" @@ -51,7 +53,7 @@ func TestMigrations(t *testing.T) { require.NoError(t, err) // Migrate database - driver := driver.New(db) + driver := driver.New(db, systemstore.New(db), bucket.NewDefaultFactory(db)) require.NoError(t, driver.Initialize(ctx)) require.NoError(t, driver.UpgradeAllBuckets(ctx, make(chan struct{}))) } From 821ff220b0cd2101ab7248ca5d35474688b53ef8 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 21 Nov 2024 18:04:20 +0100 Subject: [PATCH 27/71] feat: clean db usage from driver --- cmd/buckets_upgrade.go | 18 ++++---- cmd/root.go | 4 +- internal/storage/bucket/bucket.go | 5 +++ internal/storage/driver/driver.go | 47 ++++++++------------ internal/storage/driver/driver_test.go | 3 +- internal/storage/driver/module.go | 14 +++++- internal/storage/ledger/factory.go | 27 +++++++++++ internal/storage/ledger/main_test.go | 6 ++- internal/storage/ledger/transactions_test.go | 12 +++-- internal/storage/system/store.go | 12 +++++ test/migrations/upgrade_test.go | 7 ++- 11 files changed, 108 insertions(+), 47 deletions(-) create mode 100644 internal/storage/ledger/factory.go diff --git a/cmd/buckets_upgrade.go b/cmd/buckets_upgrade.go index 797d778ad..61f578b6e 100644 --- a/cmd/buckets_upgrade.go +++ b/cmd/buckets_upgrade.go @@ -6,8 +6,10 @@ import ( "github.com/formancehq/go-libs/v2/service" "github.com/formancehq/ledger/internal/storage/bucket" "github.com/formancehq/ledger/internal/storage/driver" + "github.com/formancehq/ledger/internal/storage/ledger" systemstore "github.com/formancehq/ledger/internal/storage/system" "github.com/spf13/cobra" + "github.com/uptrace/bun" ) func NewBucketUpgrade() *cobra.Command { @@ -19,12 +21,12 @@ func NewBucketUpgrade() *cobra.Command { logger := logging.NewDefaultLogger(cmd.OutOrStdout(), service.IsDebug(cmd), false, false) cmd.SetContext(logging.ContextWithLogger(cmd.Context(), logger)) - driver, err := getDriver(cmd) + driver, db, err := getDriver(cmd) if err != nil { return err } defer func() { - _ = driver.GetDB().Close() + _ = db.Close() }() if args[0] == "*" { @@ -41,26 +43,26 @@ func NewBucketUpgrade() *cobra.Command { return cmd } -func getDriver(cmd *cobra.Command) (*driver.Driver, error) { +func getDriver(cmd *cobra.Command) (*driver.Driver, *bun.DB, error) { connectionOptions, err := bunconnect.ConnectionOptionsFromFlags(cmd) if err != nil { - return nil, err + return nil, nil, err } db, err := bunconnect.OpenSQLDB(cmd.Context(), *connectionOptions) if err != nil { - return nil, err + return nil, nil, err } driver := driver.New( - db, + ledger.NewFactory(db), systemstore.New(db), bucket.NewDefaultFactory(db), ) if err := driver.Initialize(cmd.Context()); err != nil { - return nil, err + return nil, nil, err } - return driver, nil + return driver, db, nil } diff --git a/cmd/root.go b/cmd/root.go index 960079ee4..e50555cd0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -36,12 +36,12 @@ func NewRootCommand() *cobra.Command { root.AddCommand(version) root.AddCommand(bunmigrate.NewDefaultCommand(func(cmd *cobra.Command, _ []string, _ *bun.DB) error { // todo: use provided db ... - driver, err := getDriver(cmd) + driver, db, err := getDriver(cmd) if err != nil { return err } defer func() { - _ = driver.GetDB().Close() + _ = db.Close() }() return driver.UpgradeAllBuckets(cmd.Context(), make(chan struct{})) diff --git a/internal/storage/bucket/bucket.go b/internal/storage/bucket/bucket.go index dc5215b41..e3188c3a4 100644 --- a/internal/storage/bucket/bucket.go +++ b/internal/storage/bucket/bucket.go @@ -18,6 +18,7 @@ type Bucket interface { type Factory interface { Create(name string) Bucket + GetMigrator(b string) *migrations.Migrator } type DefaultFactory struct { @@ -29,6 +30,10 @@ func (f *DefaultFactory) Create(name string) Bucket { return NewDefault(f.db, f.tracer, name) } +func (f *DefaultFactory) GetMigrator(b string) *migrations.Migrator { + return GetMigrator(f.db, b) +} + func NewDefaultFactory(db *bun.DB, options ...DefaultFactoryOption) *DefaultFactory { ret := &DefaultFactory{ db: db, diff --git a/internal/storage/driver/driver.go b/internal/storage/driver/driver.go index 49ce60d51..97f561299 100644 --- a/internal/storage/driver/driver.go +++ b/internal/storage/driver/driver.go @@ -19,16 +19,14 @@ import ( ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" "github.com/formancehq/go-libs/v2/bun/bunpaginate" + "github.com/formancehq/go-libs/v2/logging" ledger "github.com/formancehq/ledger/internal" "github.com/formancehq/ledger/internal/storage/bucket" ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" - "github.com/uptrace/bun" - - "github.com/formancehq/go-libs/v2/logging" ) type Driver struct { - db *bun.DB + ledgerStoreFactory ledgerstore.Factory systemStore systemstore.Store bucketFactory bucket.Factory tracer trace.Tracer @@ -62,13 +60,7 @@ func (d *Driver) CreateLedger(ctx context.Context, l *ledger.Ledger) (*ledgersto return nil, fmt.Errorf("adding ledger to bucket: %w", err) } - return ledgerstore.New( - d.db, - b, - *l, - ledgerstore.WithMeter(d.meter), - ledgerstore.WithTracer(d.tracer), - ), nil + return d.ledgerStoreFactory.Create(b, *l), nil } func (d *Driver) OpenLedger(ctx context.Context, name string) (*ledgerstore.Store, *ledger.Ledger, error) { @@ -77,13 +69,9 @@ func (d *Driver) OpenLedger(ctx context.Context, name string) (*ledgerstore.Stor return nil, nil, err } - return ledgerstore.New( - d.db, - d.bucketFactory.Create(ret.Bucket), - *ret, - ledgerstore.WithMeter(d.meter), - ledgerstore.WithTracer(d.tracer), - ), ret, nil + store := d.ledgerStoreFactory.Create(d.bucketFactory.Create(ret.Bucket), *ret) + + return store, ret, err } func (d *Driver) Initialize(ctx context.Context) error { @@ -93,7 +81,7 @@ func (d *Driver) Initialize(ctx context.Context) error { return fmt.Errorf("detecting rollbacks: %w", err) } - err = systemstore.Migrate(ctx, d.db, migrations.WithLockRetryInterval(d.migratorLockRetryInterval)) + err = d.systemStore.Migrate(ctx, migrations.WithLockRetryInterval(d.migratorLockRetryInterval)) if err != nil { constraintsFailed := postgres.ErrConstraintsFailed{} if errors.As(err, &constraintsFailed) && @@ -112,7 +100,7 @@ func (d *Driver) Initialize(ctx context.Context) error { func (d *Driver) detectRollbacks(ctx context.Context) error { logging.FromContext(ctx).Debugf("Checking for downgrades on system schema") - if err := detectDowngrades(systemstore.GetMigrator(d.db), ctx); err != nil { + if err := detectDowngrades(d.systemStore.GetMigrator(), ctx); err != nil { return fmt.Errorf("detecting rollbacks of system schema: %w", err) } @@ -126,7 +114,7 @@ func (d *Driver) detectRollbacks(ctx context.Context) error { for _, b := range buckets { logging.FromContext(ctx).Debugf("Checking for downgrades on bucket '%s'", b) - if err := detectDowngrades(bucket.GetMigrator(d.db, b), ctx); err != nil { + if err := detectDowngrades(d.bucketFactory.GetMigrator(b), ctx); err != nil { return fmt.Errorf("detecting rollbacks on bucket '%s': %w", b, err) } } @@ -210,15 +198,16 @@ func (d *Driver) UpgradeAllBuckets(ctx context.Context, minimalVersionReached ch return grp.Wait() } -func (d *Driver) GetDB() *bun.DB { - return d.db -} - -func New(db *bun.DB, systemStore systemstore.Store, bucketFactory bucket.Factory, opts ...Option) *Driver { +func New( + ledgerStoreFactory ledgerstore.Factory, + systemStore systemstore.Store, + bucketFactory bucket.Factory, + opts ...Option, +) *Driver { ret := &Driver{ - db: db, - bucketFactory: bucketFactory, - systemStore: systemStore, + ledgerStoreFactory: ledgerStoreFactory, + bucketFactory: bucketFactory, + systemStore: systemStore, } for _, opt := range append(defaultOptions, opts...) { opt(ret) diff --git a/internal/storage/driver/driver_test.go b/internal/storage/driver/driver_test.go index 73de97337..a4234a598 100644 --- a/internal/storage/driver/driver_test.go +++ b/internal/storage/driver/driver_test.go @@ -15,6 +15,7 @@ import ( ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" "github.com/formancehq/ledger/internal/storage/bucket" "github.com/formancehq/ledger/internal/storage/driver" + ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" systemstore "github.com/formancehq/ledger/internal/storage/system" "github.com/google/uuid" "github.com/uptrace/bun" @@ -184,7 +185,7 @@ func newStorageDriver(t docker.T, driverOptions ...driver.Option) *driver.Driver require.NoError(t, err) d := driver.New( - db, + ledgerstore.NewFactory(db), systemstore.New(db), bucket.NewDefaultFactory(db), driverOptions..., diff --git a/internal/storage/driver/module.go b/internal/storage/driver/module.go index e0a85b142..1c0ff88c0 100644 --- a/internal/storage/driver/module.go +++ b/internal/storage/driver/module.go @@ -3,6 +3,7 @@ package driver import ( "context" "github.com/formancehq/ledger/internal/storage/bucket" + ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" systemstore "github.com/formancehq/ledger/internal/storage/system" "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/trace" @@ -32,12 +33,23 @@ func NewFXModule() fx.Option { }), fx.Provide(func( db *bun.DB, + tracerProvider trace.TracerProvider, + meterProvider metric.MeterProvider, + ) ledgerstore.Factory { + return ledgerstore.NewFactory(db, + ledgerstore.WithMeter(meterProvider.Meter("store")), + ledgerstore.WithTracer(tracerProvider.Tracer("store")), + ) + }), + fx.Provide(func( bucketFactory bucket.Factory, + ledgerStoreFactory ledgerstore.Factory, systemStore systemstore.Store, tracerProvider trace.TracerProvider, meterProvider metric.MeterProvider, ) (*Driver, error) { - return New(db, + return New( + ledgerStoreFactory, systemStore, bucketFactory, WithMeter(meterProvider.Meter("store")), diff --git a/internal/storage/ledger/factory.go b/internal/storage/ledger/factory.go new file mode 100644 index 000000000..d96c4e17f --- /dev/null +++ b/internal/storage/ledger/factory.go @@ -0,0 +1,27 @@ +package ledger + +import ( + ledger "github.com/formancehq/ledger/internal" + "github.com/formancehq/ledger/internal/storage/bucket" + "github.com/uptrace/bun" +) + +type Factory interface { + Create(bucket.Bucket, ledger.Ledger) *Store +} + +type DefaultFactory struct { + db *bun.DB + options []Option +} + +func NewFactory(db *bun.DB, options ...Option) *DefaultFactory { + return &DefaultFactory{ + db: db, + options: options, + } +} + +func (d *DefaultFactory) Create(b bucket.Bucket, l ledger.Ledger) *Store { + return New(d.db, b, l, d.options...) +} diff --git a/internal/storage/ledger/main_test.go b/internal/storage/ledger/main_test.go index 8f4b5ea0a..9ea1cc84a 100644 --- a/internal/storage/ledger/main_test.go +++ b/internal/storage/ledger/main_test.go @@ -50,7 +50,11 @@ func TestMain(m *testing.M) { bunDB.SetMaxOpenConns(100) require.NoError(t, systemstore.Migrate(logging.TestingContext(), bunDB)) - defaultDriver.SetValue(driver.New(bunDB, systemstore.New(bunDB), bucket.NewDefaultFactory(bunDB))) + defaultDriver.SetValue(driver.New( + ledgerstore.NewFactory(bunDB), + systemstore.New(bunDB), + bucket.NewDefaultFactory(bunDB), + )) return bunDB }) diff --git a/internal/storage/ledger/transactions_test.go b/internal/storage/ledger/transactions_test.go index 2b0340f94..4548947a0 100644 --- a/internal/storage/ledger/transactions_test.go +++ b/internal/storage/ledger/transactions_test.go @@ -622,7 +622,11 @@ func TestTransactionsInsert(t *testing.T) { bunDB, err := bunconnect.OpenSQLDB(ctx, db.ConnectionOptions()) require.NoError(t, err) - driver := driver.New(bunDB, systemstore.New(bunDB), bucket.NewDefaultFactory(bunDB)) + driver := driver.New( + ledgerstore.NewFactory(bunDB), + systemstore.New(bunDB), + bucket.NewDefaultFactory(bunDB), + ) require.NoError(t, driver.Initialize(ctx)) ledgerName := uuid.NewString()[:8] @@ -630,16 +634,16 @@ func TestTransactionsInsert(t *testing.T) { l := ledger.MustNewWithDefault(ledgerName) l.Bucket = ledgerName - migrator := bucket.GetMigrator(driver.GetDB(), ledgerName) + migrator := bucket.GetMigrator(bunDB, ledgerName) for i := 0; i < bucket.MinimalSchemaVersion; i++ { require.NoError(t, migrator.UpByOne(ctx)) } - b := bucket.NewDefault(driver.GetDB(), noop.Tracer{}, ledgerName) + b := bucket.NewDefault(bunDB, noop.Tracer{}, ledgerName) err = b.AddLedger(ctx, l) require.NoError(t, err) - store := ledgerstore.New(driver.GetDB(), b, l) + store := ledgerstore.New(bunDB, b, l) const nbTry = 100 diff --git a/internal/storage/system/store.go b/internal/storage/system/store.go index 151ba429d..5b64c68ad 100644 --- a/internal/storage/system/store.go +++ b/internal/storage/system/store.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "github.com/formancehq/go-libs/v2/metadata" + "github.com/formancehq/go-libs/v2/migrations" "github.com/formancehq/go-libs/v2/platform/postgres" ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" systemcontroller "github.com/formancehq/ledger/internal/controller/system" @@ -23,6 +24,9 @@ type Store interface { ListLedgers(ctx context.Context, q ledgercontroller.ListLedgersQuery) (*bunpaginate.Cursor[ledger.Ledger], error) GetLedger(ctx context.Context, name string) (*ledger.Ledger, error) GetDistinctBuckets(ctx context.Context) ([]string, error) + + Migrate(ctx context.Context, options ...migrations.Option) error + GetMigrator(options ...migrations.Option) *migrations.Migrator } const ( @@ -113,6 +117,14 @@ func (d *DefaultStore) GetLedger(ctx context.Context, name string) (*ledger.Ledg return ret, nil } +func (d *DefaultStore) Migrate(ctx context.Context, options ...migrations.Option) error { + return d.GetMigrator(options...).Up(ctx) +} + +func (d *DefaultStore) GetMigrator(options ...migrations.Option) *migrations.Migrator { + return GetMigrator(d.db, options...) +} + func (d *DefaultStore) GetDB() *bun.DB { return d.db } diff --git a/test/migrations/upgrade_test.go b/test/migrations/upgrade_test.go index e609374d2..a4ef5ccf8 100644 --- a/test/migrations/upgrade_test.go +++ b/test/migrations/upgrade_test.go @@ -9,6 +9,7 @@ import ( "github.com/formancehq/go-libs/v2/testing/platform/pgtesting" "github.com/formancehq/ledger/internal/storage/bucket" "github.com/formancehq/ledger/internal/storage/driver" + "github.com/formancehq/ledger/internal/storage/ledger" systemstore "github.com/formancehq/ledger/internal/storage/system" "github.com/ory/dockertest/v3" dockerlib "github.com/ory/dockertest/v3/docker" @@ -53,7 +54,11 @@ func TestMigrations(t *testing.T) { require.NoError(t, err) // Migrate database - driver := driver.New(db, systemstore.New(db), bucket.NewDefaultFactory(db)) + driver := driver.New( + ledger.NewFactory(db), + systemstore.New(db), + bucket.NewDefaultFactory(db), + ) require.NoError(t, driver.Initialize(ctx)) require.NoError(t, driver.UpgradeAllBuckets(ctx, make(chan struct{}))) } From 3c0b19697e7e2a12f761048666ced3f80dc72475 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 21 Nov 2024 18:53:22 +0100 Subject: [PATCH 28/71] test: split tests --- example2.js | 20 - .../storage/bucket/default_bucket_test.go | 2 +- internal/storage/bucket/migrations_test.go | 2 +- ...ated_test.go => buckets_generated_test.go} | 47 ++- internal/storage/driver/driver.go | 17 +- internal/storage/driver/driver_test.go | 350 ++++++++++++------ .../storage/driver/ledger_generated_test.go | 52 +++ internal/storage/driver/main_test.go | 24 -- internal/storage/driver/mocks.go | 6 +- .../storage/driver/system_generated_test.go | 165 +++++++++ internal/storage/system/main_test.go | 2 +- internal/storage/system/migrations.go | 2 +- internal/storage/system/migrations_test.go | 5 +- internal/storage/system/store.go | 13 +- internal/storage/system/store_test.go | 169 +++++++++ test/e2e/app_lifecycle_test.go | 2 +- test/e2e/suite_test.go | 2 +- 17 files changed, 674 insertions(+), 206 deletions(-) delete mode 100644 example2.js rename internal/storage/driver/{bucket_generated_test.go => buckets_generated_test.go} (73%) create mode 100644 internal/storage/driver/ledger_generated_test.go delete mode 100644 internal/storage/driver/main_test.go create mode 100644 internal/storage/driver/system_generated_test.go create mode 100644 internal/storage/system/store_test.go diff --git a/example2.js b/example2.js deleted file mode 100644 index 80aa7bc7b..000000000 --- a/example2.js +++ /dev/null @@ -1,20 +0,0 @@ -function next() { - let postings = []; - for(let i = 0; i < 500000; i++) { - postings.push({ - source: `world`, - destination: `banks`, - amount: 100, - asset: 'USD' - }) - } - return { - action: 'CREATE_TRANSACTION', - data: { - postings - } - } -} - - - diff --git a/internal/storage/bucket/default_bucket_test.go b/internal/storage/bucket/default_bucket_test.go index 26d94ad8e..2c81a1111 100644 --- a/internal/storage/bucket/default_bucket_test.go +++ b/internal/storage/bucket/default_bucket_test.go @@ -28,7 +28,7 @@ func TestBuckets(t *testing.T) { db.AddQueryHook(bundebug.NewQueryHook()) } - require.NoError(t, driver.Migrate(ctx, db)) + require.NoError(t, system.Migrate(ctx, db)) b := bucket.NewDefault(db, noop.Tracer{}, name) require.NoError(t, b.Migrate(ctx, make(chan struct{}))) diff --git a/internal/storage/bucket/migrations_test.go b/internal/storage/bucket/migrations_test.go index abd7723d6..8d787ac98 100644 --- a/internal/storage/bucket/migrations_test.go +++ b/internal/storage/bucket/migrations_test.go @@ -23,7 +23,7 @@ func TestMigrations(t *testing.T) { db, err := bunconnect.OpenSQLDB(ctx, pgDatabase.ConnectionOptions()) require.NoError(t, err) - require.NoError(t, driver.Migrate(ctx, db)) + require.NoError(t, system.Migrate(ctx, db)) if testing.Verbose() { db.AddQueryHook(bundebug.NewQueryHook()) } diff --git a/internal/storage/driver/bucket_generated_test.go b/internal/storage/driver/buckets_generated_test.go similarity index 73% rename from internal/storage/driver/bucket_generated_test.go rename to internal/storage/driver/buckets_generated_test.go index 0820838c9..92e52b8f1 100644 --- a/internal/storage/driver/bucket_generated_test.go +++ b/internal/storage/driver/buckets_generated_test.go @@ -2,7 +2,7 @@ // // Generated by this command: // -// mockgen -write_source_comment=false -write_package_comment=false -source ../bucket/bucket.go -destination bucket_generated_test.go -package driver . Factory +// mockgen -write_source_comment=false -write_package_comment=false -source ../bucket/bucket.go -destination buckets_generated_test.go -package driver --mock_names Factory=BucketFactory . Factory package driver import ( @@ -12,7 +12,6 @@ import ( migrations "github.com/formancehq/go-libs/v2/migrations" ledger "github.com/formancehq/ledger/internal" bucket "github.com/formancehq/ledger/internal/storage/bucket" - bun "github.com/uptrace/bun" gomock "go.uber.org/mock/gomock" ) @@ -102,39 +101,53 @@ func (mr *MockBucketMockRecorder) Migrate(ctx, minimalVersionReached any, opts . return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Migrate", reflect.TypeOf((*MockBucket)(nil).Migrate), varargs...) } -// MockFactory is a mock of Factory interface. -type MockFactory struct { +// BucketFactory is a mock of Factory interface. +type BucketFactory struct { ctrl *gomock.Controller - recorder *MockFactoryMockRecorder + recorder *BucketFactoryMockRecorder } -// MockFactoryMockRecorder is the mock recorder for MockFactory. -type MockFactoryMockRecorder struct { - mock *MockFactory +// BucketFactoryMockRecorder is the mock recorder for BucketFactory. +type BucketFactoryMockRecorder struct { + mock *BucketFactory } -// NewMockFactory creates a new mock instance. -func NewMockFactory(ctrl *gomock.Controller) *MockFactory { - mock := &MockFactory{ctrl: ctrl} - mock.recorder = &MockFactoryMockRecorder{mock} +// NewBucketFactory creates a new mock instance. +func NewBucketFactory(ctrl *gomock.Controller) *BucketFactory { + mock := &BucketFactory{ctrl: ctrl} + mock.recorder = &BucketFactoryMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockFactory) EXPECT() *MockFactoryMockRecorder { +func (m *BucketFactory) EXPECT() *BucketFactoryMockRecorder { return m.recorder } // Create mocks base method. -func (m *MockFactory) Create(db *bun.DB, name string) bucket.Bucket { +func (m *BucketFactory) Create(name string) bucket.Bucket { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Create", db, name) + ret := m.ctrl.Call(m, "Create", name) ret0, _ := ret[0].(bucket.Bucket) return ret0 } // Create indicates an expected call of Create. -func (mr *MockFactoryMockRecorder) Create(db, name any) *gomock.Call { +func (mr *BucketFactoryMockRecorder) Create(name any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockFactory)(nil).Create), db, name) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*BucketFactory)(nil).Create), name) +} + +// GetMigrator mocks base method. +func (m *BucketFactory) GetMigrator(b string) *migrations.Migrator { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMigrator", b) + ret0, _ := ret[0].(*migrations.Migrator) + return ret0 +} + +// GetMigrator indicates an expected call of GetMigrator. +func (mr *BucketFactoryMockRecorder) GetMigrator(b any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMigrator", reflect.TypeOf((*BucketFactory)(nil).GetMigrator), b) } diff --git a/internal/storage/driver/driver.go b/internal/storage/driver/driver.go index 97f561299..cc72eb5b6 100644 --- a/internal/storage/driver/driver.go +++ b/internal/storage/driver/driver.go @@ -158,6 +158,9 @@ func (d *Driver) UpgradeAllBuckets(ctx context.Context, minimalVersionReached ch grp, ctx := errgroup.WithContext(ctx) for _, bucketName := range buckets { grp.Go(func() error { + logger := logging.FromContext(ctx).WithFields(map[string]any{ + "bucket": bucketName, + }) b := d.bucketFactory.Create(bucketName) minimalVersionReached := make(chan struct{}) @@ -167,19 +170,21 @@ func (d *Driver) UpgradeAllBuckets(ctx context.Context, minimalVersionReached ch case <-ctx.Done(): return case <-minimalVersionReached: + logger.Infof("Reached minimal workable version") sem <- struct{}{} } }() - logging.FromContext(ctx).Infof("Upgrading bucket '%s'", bucketName) + logger.Infof("Upgrading...") if err := b.Migrate( ctx, minimalVersionReached, migrations.WithLockRetryInterval(d.migratorLockRetryInterval), ); err != nil { + logger.Errorf("Error upgrading: %s", err) return err } - logging.FromContext(ctx).Infof("Bucket '%s' up to date", bucketName) + logging.Infof("Up to date") return nil }) @@ -193,7 +198,13 @@ func (d *Driver) UpgradeAllBuckets(ctx context.Context, minimalVersionReached ch } } - close(minimalVersionReached) + logging.FromContext(ctx).Infof("All buckets have reached minimal workable version") + select { + case <-minimalVersionReached: + // already closed + default: + close(minimalVersionReached) + } return grp.Wait() } diff --git a/internal/storage/driver/driver_test.go b/internal/storage/driver/driver_test.go index a4234a598..9d5dbc791 100644 --- a/internal/storage/driver/driver_test.go +++ b/internal/storage/driver/driver_test.go @@ -1,197 +1,303 @@ -//go:build it - package driver_test import ( "context" - "fmt" - "github.com/formancehq/go-libs/v2/bun/bunconnect" - "github.com/formancehq/go-libs/v2/bun/bundebug" + "errors" "github.com/formancehq/go-libs/v2/bun/bunpaginate" + "github.com/formancehq/go-libs/v2/logging" "github.com/formancehq/go-libs/v2/metadata" + "github.com/formancehq/go-libs/v2/migrations" "github.com/formancehq/go-libs/v2/pointer" - "github.com/formancehq/go-libs/v2/testing/docker" ledger "github.com/formancehq/ledger/internal" ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" "github.com/formancehq/ledger/internal/storage/bucket" "github.com/formancehq/ledger/internal/storage/driver" ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" - systemstore "github.com/formancehq/ledger/internal/storage/system" "github.com/google/uuid" - "github.com/uptrace/bun" - "golang.org/x/sync/errgroup" - "os" - "slices" - "testing" - "time" - - "github.com/formancehq/go-libs/v2/logging" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "testing" ) func TestUpgradeAllLedgers(t *testing.T) { t.Parallel() - d := newStorageDriver(t) ctx := logging.TestingContext() - count := 30 + t.Run("single bucket with no error", func(t *testing.T) { + t.Parallel() - for i := 0; i < count; i++ { - name := fmt.Sprintf("ledger%d", i) - _, err := d.CreateLedger(ctx, pointer.For(ledger.MustNewWithDefault(name))) - require.NoError(t, err) - } + ctrl := gomock.NewController(t) + ledgerStoreFactory := driver.NewLedgerStoreFactory(ctrl) + bucketFactory := driver.NewBucketFactory(ctrl) + systemStore := driver.NewSystemStore(ctrl) + + d := driver.New( + ledgerStoreFactory, + systemStore, + bucketFactory, + ) + + bucket := driver.NewMockBucket(ctrl) + + systemStore.EXPECT(). + GetDistinctBuckets(ctx). + Return([]string{ledger.DefaultBucket}, nil) + + bucketFactory.EXPECT(). + Create(ledger.DefaultBucket). + AnyTimes(). + Return(bucket) + + bucket.EXPECT(). + Migrate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, minimalVersionReached chan struct{}, opts ...migrations.Option) error { + close(minimalVersionReached) + return nil + }) + + require.NoError(t, d.UpgradeAllBuckets(ctx, make(chan struct{}))) + }) + + t.Run("with concurrent buckets", func(t *testing.T) { + t.Parallel() + + t.Run("and no error", func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + ledgerStoreFactory := driver.NewLedgerStoreFactory(ctrl) + bucketFactory := driver.NewBucketFactory(ctrl) + systemStore := driver.NewSystemStore(ctrl) + + d := driver.New( + ledgerStoreFactory, + systemStore, + bucketFactory, + ) - require.NoError(t, d.UpgradeAllBuckets(ctx, make(chan struct{}))) + bucketList := []string{"bucket1", "bucket2", "bucket3"} + buckets := make(map[string]bucket.Bucket) + + for _, name := range bucketList { + bucket := driver.NewMockBucket(ctrl) + buckets[name] = bucket + + bucketFactory.EXPECT(). + Create(name). + Return(bucket) + + bucket.EXPECT(). + Migrate(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, minimalVersionReached chan struct{}, opts ...migrations.Option) error { + close(minimalVersionReached) + return nil + }) + } + + systemStore.EXPECT(). + GetDistinctBuckets(ctx). + Return(bucketList, nil) + + require.NoError(t, d.UpgradeAllBuckets(ctx, make(chan struct{}))) + }) + + t.Run("and error", func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + ledgerStoreFactory := driver.NewLedgerStoreFactory(ctrl) + bucketFactory := driver.NewBucketFactory(ctrl) + systemStore := driver.NewSystemStore(ctrl) + + d := driver.New( + ledgerStoreFactory, + systemStore, + bucketFactory, + ) + + bucket1 := driver.NewMockBucket(ctrl) + bucket2 := driver.NewMockBucket(ctrl) + bucketList := []string{"bucket1", "bucket2"} + allBucketsMinimalVersionReached := make(chan struct{}) + + bucketFactory.EXPECT(). + Create(gomock.AnyOf( + gomock.Eq("bucket1"), + gomock.Eq("bucket2"), + )). + AnyTimes(). + DoAndReturn(func(name string) bucket.Bucket { + if name == "bucket1" { + return bucket1 + } + return bucket2 + }) + + bucket1MigrationStarted := make(chan struct{}) + bucket1.EXPECT(). + Migrate(gomock.Any(), gomock.Any(), gomock.Any()). + AnyTimes(). + DoAndReturn(func(ctx context.Context, minimalVersionReached chan struct{}, opts ...migrations.Option) error { + close(minimalVersionReached) + close(bucket1MigrationStarted) + <-ctx.Done() + + return ctx.Err() + }) + + bucket2.EXPECT(). + Migrate(gomock.Any(), gomock.Any(), gomock.Any()). + AnyTimes(). + DoAndReturn(func(ctx context.Context, minimalVersionReached chan struct{}, opts ...migrations.Option) error { + select { + case <-ctx.Done(): + return ctx.Err() + case <-bucket1MigrationStarted: + return errors.New("unknown error") + } + }) + + systemStore.EXPECT(). + GetDistinctBuckets(ctx). + AnyTimes(). + Return(bucketList, nil) + + err := d.UpgradeAllBuckets(ctx, allBucketsMinimalVersionReached) + require.Error(t, err) + + bucket1MigrationStarted = make(chan struct{}) + err = d.UpgradeAllBuckets(ctx, allBucketsMinimalVersionReached) + require.Error(t, err) + }) + }) } func TestLedgersCreate(t *testing.T) { t.Parallel() ctx := logging.TestingContext() - driver := newStorageDriver(t, driver.WithMigratorLockRetryInterval(100*time.Millisecond)) - const count = 30 - grp, ctx := errgroup.WithContext(ctx) - createdLedgersChan := make(chan ledger.Ledger, count) + ctrl := gomock.NewController(t) + ledgerStoreFactory := driver.NewLedgerStoreFactory(ctrl) + bucketFactory := driver.NewBucketFactory(ctrl) + systemStore := driver.NewSystemStore(ctrl) - for i := range count { - grp.Go(func() error { - l := ledger.MustNewWithDefault(fmt.Sprintf("ledger%d", i)) + d := driver.New( + ledgerStoreFactory, + systemStore, + bucketFactory, + ) - ctx, cancel := context.WithDeadline(ctx, time.Now().Add(40*time.Second)) - defer cancel() + l := pointer.For(ledger.MustNewWithDefault("test")) - _, err := driver.CreateLedger(ctx, &l) - if err != nil { - return err - } - createdLedgersChan <- l + bucket := driver.NewMockBucket(ctrl) + bucketFactory.EXPECT(). + Create(ledger.DefaultBucket). + Return(bucket) - return nil - }) - } + systemStore.EXPECT(). + CreateLedger(gomock.Any(), l) - require.NoError(t, grp.Wait()) + bucket.EXPECT(). + Migrate(gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil) - close(createdLedgersChan) + bucket.EXPECT(). + AddLedger(gomock.Any(), *l). + Return(nil) - createdLedgers := make([]ledger.Ledger, 0) - for createdLedger := range createdLedgersChan { - createdLedgers = append(createdLedgers, createdLedger) - } + ledgerStoreFactory.EXPECT(). + Create(gomock.Any(), *l). + Return(&ledgerstore.Store{}) - slices.SortStableFunc(createdLedgers, func(a, b ledger.Ledger) int { - return a.ID - b.ID - }) - - for i, createdLedger := range createdLedgers { - require.Equal(t, i+1, createdLedger.ID) - require.NotEmpty(t, createdLedger.AddedAt) - } + _, err := d.CreateLedger(ctx, l) + require.NoError(t, err) } func TestLedgersList(t *testing.T) { t.Parallel() ctx := logging.TestingContext() - driver := newStorageDriver(t) - - ledgers := make([]ledger.Ledger, 0) - pageSize := uint64(2) - count := uint64(10) - for i := uint64(0); i < count; i++ { - m := metadata.Metadata{} - if i%2 == 0 { - m["foo"] = "bar" - } - l := ledger.MustNewWithDefault(fmt.Sprintf("ledger%d", i)).WithMetadata(m) - _, err := driver.CreateLedger(ctx, &l) - require.NoError(t, err) - - ledgers = append(ledgers, l) - } - cursor, err := driver.ListLedgers(ctx, ledgercontroller.NewListLedgersQuery(pageSize)) - require.NoError(t, err) - require.Len(t, cursor.Data, int(pageSize)) - require.Equal(t, ledgers[:pageSize], cursor.Data) + ctrl := gomock.NewController(t) + ledgerStoreFactory := driver.NewLedgerStoreFactory(ctrl) + bucketFactory := driver.NewBucketFactory(ctrl) + systemStore := driver.NewSystemStore(ctrl) - for i := pageSize; i < count; i += pageSize { - query := ledgercontroller.ListLedgersQuery{} - require.NoError(t, bunpaginate.UnmarshalCursor(cursor.Next, &query)) + driver := driver.New( + ledgerStoreFactory, + systemStore, + bucketFactory, + ) - cursor, err = driver.ListLedgers(ctx, query) - require.NoError(t, err) - require.Len(t, cursor.Data, 2) - require.Equal(t, ledgers[i:i+pageSize], cursor.Data) - } + query := ledgercontroller.NewListLedgersQuery(15) + + systemStore.EXPECT(). + ListLedgers(gomock.Any(), query). + Return(&bunpaginate.Cursor[ledger.Ledger]{ + Data: []ledger.Ledger{ + ledger.MustNewWithDefault("testing"), + }, + }, nil) + + cursor, err := driver.ListLedgers(ctx, query) + require.NoError(t, err) + require.Len(t, cursor.Data, 1) } func TestLedgerUpdateMetadata(t *testing.T) { t.Parallel() ctx := logging.TestingContext() - storageDriver := newStorageDriver(t) + + ctrl := gomock.NewController(t) + ledgerStoreFactory := driver.NewLedgerStoreFactory(ctrl) + bucketFactory := driver.NewBucketFactory(ctrl) + systemStore := driver.NewSystemStore(ctrl) + + d := driver.New( + ledgerStoreFactory, + systemStore, + bucketFactory, + ) l := ledger.MustNewWithDefault(uuid.NewString()) - _, err := storageDriver.CreateLedger(ctx, &l) - require.NoError(t, err) addedMetadata := metadata.Metadata{ "foo": "bar", } - err = storageDriver.UpdateLedgerMetadata(ctx, l.Name, addedMetadata) - require.NoError(t, err) + systemStore.EXPECT(). + UpdateLedgerMetadata(gomock.Any(), l.Name, addedMetadata). + Return(nil) - ledgerFromDB, err := storageDriver.GetLedger(ctx, l.Name) + err := d.UpdateLedgerMetadata(ctx, l.Name, addedMetadata) require.NoError(t, err) - require.Equal(t, addedMetadata, ledgerFromDB.Metadata) } func TestLedgerDeleteMetadata(t *testing.T) { t.Parallel() ctx := logging.TestingContext() - driver := newStorageDriver(t) + ctrl := gomock.NewController(t) + ledgerStoreFactory := driver.NewLedgerStoreFactory(ctrl) + bucketFactory := driver.NewBucketFactory(ctrl) + systemStore := driver.NewSystemStore(ctrl) + + d := driver.New( + ledgerStoreFactory, + systemStore, + bucketFactory, + ) l := ledger.MustNewWithDefault(uuid.NewString()).WithMetadata(metadata.Metadata{ "foo": "bar", }) - _, err := driver.CreateLedger(ctx, &l) - require.NoError(t, err) - - err = driver.DeleteLedgerMetadata(ctx, l.Name, "foo") - require.NoError(t, err) + systemStore.EXPECT(). + DeleteLedgerMetadata(gomock.Any(), l.Name, "foo"). + Return(nil) - ledgerFromDB, err := driver.GetLedger(ctx, l.Name) + err := d.DeleteLedgerMetadata(ctx, l.Name, "foo") require.NoError(t, err) - require.Equal(t, metadata.Metadata{}, ledgerFromDB.Metadata) -} - -func newStorageDriver(t docker.T, driverOptions ...driver.Option) *driver.Driver { - t.Helper() - - ctx := logging.TestingContext() - pgDatabase := srv.NewDatabase(t) - - hooks := make([]bun.QueryHook, 0) - if os.Getenv("DEBUG") == "true" { - hooks = append(hooks, bundebug.NewQueryHook()) - } - db, err := bunconnect.OpenSQLDB(ctx, pgDatabase.ConnectionOptions(), hooks...) - require.NoError(t, err) - - d := driver.New( - ledgerstore.NewFactory(db), - systemstore.New(db), - bucket.NewDefaultFactory(db), - driverOptions..., - ) - - require.NoError(t, d.Initialize(logging.TestingContext())) - - return d } diff --git a/internal/storage/driver/ledger_generated_test.go b/internal/storage/driver/ledger_generated_test.go new file mode 100644 index 000000000..fb2f8a6ab --- /dev/null +++ b/internal/storage/driver/ledger_generated_test.go @@ -0,0 +1,52 @@ +// Code generated by MockGen. DO NOT EDIT. +// +// Generated by this command: +// +// mockgen -write_source_comment=false -write_package_comment=false -source ../ledger/factory.go -destination ledger_generated_test.go -package driver --mock_names Factory=LedgerStoreFactory . Factory +package driver + +import ( + reflect "reflect" + + ledger "github.com/formancehq/ledger/internal" + bucket "github.com/formancehq/ledger/internal/storage/bucket" + ledger0 "github.com/formancehq/ledger/internal/storage/ledger" + gomock "go.uber.org/mock/gomock" +) + +// LedgerStoreFactory is a mock of Factory interface. +type LedgerStoreFactory struct { + ctrl *gomock.Controller + recorder *LedgerStoreFactoryMockRecorder +} + +// LedgerStoreFactoryMockRecorder is the mock recorder for LedgerStoreFactory. +type LedgerStoreFactoryMockRecorder struct { + mock *LedgerStoreFactory +} + +// NewLedgerStoreFactory creates a new mock instance. +func NewLedgerStoreFactory(ctrl *gomock.Controller) *LedgerStoreFactory { + mock := &LedgerStoreFactory{ctrl: ctrl} + mock.recorder = &LedgerStoreFactoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *LedgerStoreFactory) EXPECT() *LedgerStoreFactoryMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *LedgerStoreFactory) Create(arg0 bucket.Bucket, arg1 ledger.Ledger) *ledger0.Store { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", arg0, arg1) + ret0, _ := ret[0].(*ledger0.Store) + return ret0 +} + +// Create indicates an expected call of Create. +func (mr *LedgerStoreFactoryMockRecorder) Create(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*LedgerStoreFactory)(nil).Create), arg0, arg1) +} diff --git a/internal/storage/driver/main_test.go b/internal/storage/driver/main_test.go deleted file mode 100644 index f07b55c5b..000000000 --- a/internal/storage/driver/main_test.go +++ /dev/null @@ -1,24 +0,0 @@ -//go:build it - -package driver_test - -import ( - "testing" - - "github.com/formancehq/go-libs/v2/testing/docker" - "github.com/formancehq/go-libs/v2/testing/utils" - - "github.com/formancehq/go-libs/v2/logging" - "github.com/formancehq/go-libs/v2/testing/platform/pgtesting" -) - -var srv *pgtesting.PostgresServer - -func TestMain(m *testing.M) { - utils.WithTestMain(func(t *utils.TestingTForMain) int { - srv = pgtesting.CreatePostgresServer(t, docker.NewPool(t, logging.Testing())) - - - return m.Run() - }) -} diff --git a/internal/storage/driver/mocks.go b/internal/storage/driver/mocks.go index c83ea64d1..ef937ef5f 100644 --- a/internal/storage/driver/mocks.go +++ b/internal/storage/driver/mocks.go @@ -1,4 +1,6 @@ -//go:generate mockgen -write_source_comment=false -write_package_comment=false -source ../bucket/bucket.go -destination bucket_generated_test.go -package driver . Bucket -//go:generate mockgen -write_source_comment=false -write_package_comment=false -source ../bucket/bucket.go -destination bucket_generated_test.go -package driver . Factory +//go:generate mockgen -write_source_comment=false -write_package_comment=false -source ../bucket/bucket.go -destination buckets_generated_test.go -package driver . Bucket +//go:generate mockgen -write_source_comment=false -write_package_comment=false -source ../bucket/bucket.go -destination buckets_generated_test.go -package driver --mock_names Factory=BucketFactory . Factory +//go:generate mockgen -write_source_comment=false -write_package_comment=false -source ../ledger/factory.go -destination ledger_generated_test.go -package driver --mock_names Factory=LedgerStoreFactory . Factory +//go:generate mockgen -write_source_comment=false -write_package_comment=false -source ../system/store.go -destination system_generated_test.go -package driver --mock_names Store=SystemStore . Store package driver diff --git a/internal/storage/driver/system_generated_test.go b/internal/storage/driver/system_generated_test.go new file mode 100644 index 000000000..e646d8a75 --- /dev/null +++ b/internal/storage/driver/system_generated_test.go @@ -0,0 +1,165 @@ +// Code generated by MockGen. DO NOT EDIT. +// +// Generated by this command: +// +// mockgen -write_source_comment=false -write_package_comment=false -source ../system/store.go -destination system_generated_test.go -package driver --mock_names Store=SystemStore . Store +package driver + +import ( + context "context" + reflect "reflect" + + bunpaginate "github.com/formancehq/go-libs/v2/bun/bunpaginate" + metadata "github.com/formancehq/go-libs/v2/metadata" + migrations "github.com/formancehq/go-libs/v2/migrations" + ledger "github.com/formancehq/ledger/internal" + ledger0 "github.com/formancehq/ledger/internal/controller/ledger" + gomock "go.uber.org/mock/gomock" +) + +// SystemStore is a mock of Store interface. +type SystemStore struct { + ctrl *gomock.Controller + recorder *SystemStoreMockRecorder +} + +// SystemStoreMockRecorder is the mock recorder for SystemStore. +type SystemStoreMockRecorder struct { + mock *SystemStore +} + +// NewSystemStore creates a new mock instance. +func NewSystemStore(ctrl *gomock.Controller) *SystemStore { + mock := &SystemStore{ctrl: ctrl} + mock.recorder = &SystemStoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *SystemStore) EXPECT() *SystemStoreMockRecorder { + return m.recorder +} + +// CreateLedger mocks base method. +func (m *SystemStore) CreateLedger(ctx context.Context, l *ledger.Ledger) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateLedger", ctx, l) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreateLedger indicates an expected call of CreateLedger. +func (mr *SystemStoreMockRecorder) CreateLedger(ctx, l any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateLedger", reflect.TypeOf((*SystemStore)(nil).CreateLedger), ctx, l) +} + +// DeleteLedgerMetadata mocks base method. +func (m *SystemStore) DeleteLedgerMetadata(ctx context.Context, name, key string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteLedgerMetadata", ctx, name, key) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteLedgerMetadata indicates an expected call of DeleteLedgerMetadata. +func (mr *SystemStoreMockRecorder) DeleteLedgerMetadata(ctx, name, key any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLedgerMetadata", reflect.TypeOf((*SystemStore)(nil).DeleteLedgerMetadata), ctx, name, key) +} + +// GetDistinctBuckets mocks base method. +func (m *SystemStore) GetDistinctBuckets(ctx context.Context) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDistinctBuckets", ctx) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetDistinctBuckets indicates an expected call of GetDistinctBuckets. +func (mr *SystemStoreMockRecorder) GetDistinctBuckets(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDistinctBuckets", reflect.TypeOf((*SystemStore)(nil).GetDistinctBuckets), ctx) +} + +// GetLedger mocks base method. +func (m *SystemStore) GetLedger(ctx context.Context, name string) (*ledger.Ledger, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLedger", ctx, name) + ret0, _ := ret[0].(*ledger.Ledger) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetLedger indicates an expected call of GetLedger. +func (mr *SystemStoreMockRecorder) GetLedger(ctx, name any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLedger", reflect.TypeOf((*SystemStore)(nil).GetLedger), ctx, name) +} + +// GetMigrator mocks base method. +func (m *SystemStore) GetMigrator(options ...migrations.Option) *migrations.Migrator { + m.ctrl.T.Helper() + varargs := []any{} + for _, a := range options { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetMigrator", varargs...) + ret0, _ := ret[0].(*migrations.Migrator) + return ret0 +} + +// GetMigrator indicates an expected call of GetMigrator. +func (mr *SystemStoreMockRecorder) GetMigrator(options ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMigrator", reflect.TypeOf((*SystemStore)(nil).GetMigrator), options...) +} + +// ListLedgers mocks base method. +func (m *SystemStore) ListLedgers(ctx context.Context, q ledger0.ListLedgersQuery) (*bunpaginate.Cursor[ledger.Ledger], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListLedgers", ctx, q) + ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Ledger]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListLedgers indicates an expected call of ListLedgers. +func (mr *SystemStoreMockRecorder) ListLedgers(ctx, q any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLedgers", reflect.TypeOf((*SystemStore)(nil).ListLedgers), ctx, q) +} + +// Migrate mocks base method. +func (m *SystemStore) Migrate(ctx context.Context, options ...migrations.Option) error { + m.ctrl.T.Helper() + varargs := []any{ctx} + for _, a := range options { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Migrate", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Migrate indicates an expected call of Migrate. +func (mr *SystemStoreMockRecorder) Migrate(ctx any, options ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx}, options...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Migrate", reflect.TypeOf((*SystemStore)(nil).Migrate), varargs...) +} + +// UpdateLedgerMetadata mocks base method. +func (m_2 *SystemStore) UpdateLedgerMetadata(ctx context.Context, name string, m metadata.Metadata) error { + m_2.ctrl.T.Helper() + ret := m_2.ctrl.Call(m_2, "UpdateLedgerMetadata", ctx, name, m) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateLedgerMetadata indicates an expected call of UpdateLedgerMetadata. +func (mr *SystemStoreMockRecorder) UpdateLedgerMetadata(ctx, name, m any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateLedgerMetadata", reflect.TypeOf((*SystemStore)(nil).UpdateLedgerMetadata), ctx, name, m) +} diff --git a/internal/storage/system/main_test.go b/internal/storage/system/main_test.go index 017d038d0..0e27d35f9 100644 --- a/internal/storage/system/main_test.go +++ b/internal/storage/system/main_test.go @@ -1,6 +1,6 @@ //go:build it -package driver_test +package system import ( "testing" diff --git a/internal/storage/system/migrations.go b/internal/storage/system/migrations.go index d33f627d4..eed37430d 100644 --- a/internal/storage/system/migrations.go +++ b/internal/storage/system/migrations.go @@ -1,4 +1,4 @@ -package driver +package system import ( "context" diff --git a/internal/storage/system/migrations_test.go b/internal/storage/system/migrations_test.go index 4d5c98811..044fb533e 100644 --- a/internal/storage/system/migrations_test.go +++ b/internal/storage/system/migrations_test.go @@ -1,12 +1,11 @@ //go:build it -package driver_test +package system import ( "context" "fmt" "github.com/formancehq/go-libs/v2/testing/migrations" - "github.com/formancehq/ledger/internal/storage/system" "os" "testing" @@ -36,7 +35,7 @@ func TestMigrations(t *testing.T) { require.NoError(t, db.Close()) }) - test := migrations.NewMigrationTest(t, driver.GetMigrator(db), db) + test := migrations.NewMigrationTest(t, GetMigrator(db), db) test.Append(8, addIdOnLedgerTable) test.Run() } diff --git a/internal/storage/system/store.go b/internal/storage/system/store.go index 5b64c68ad..1e32bad2c 100644 --- a/internal/storage/system/store.go +++ b/internal/storage/system/store.go @@ -1,19 +1,16 @@ -package driver +package system import ( "context" "errors" "fmt" + "github.com/formancehq/go-libs/v2/bun/bunpaginate" "github.com/formancehq/go-libs/v2/metadata" "github.com/formancehq/go-libs/v2/migrations" "github.com/formancehq/go-libs/v2/platform/postgres" + ledger "github.com/formancehq/ledger/internal" ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" systemcontroller "github.com/formancehq/ledger/internal/controller/system" - "go.opentelemetry.io/otel/metric" - "go.opentelemetry.io/otel/trace" - - "github.com/formancehq/go-libs/v2/bun/bunpaginate" - ledger "github.com/formancehq/ledger/internal" "github.com/uptrace/bun" ) @@ -34,9 +31,7 @@ const ( ) type DefaultStore struct { - db *bun.DB - tracer trace.Tracer - meter metric.Meter + db *bun.DB } func (d *DefaultStore) GetDistinctBuckets(ctx context.Context) ([]string, error) { diff --git a/internal/storage/system/store_test.go b/internal/storage/system/store_test.go new file mode 100644 index 000000000..f8fc36f9a --- /dev/null +++ b/internal/storage/system/store_test.go @@ -0,0 +1,169 @@ +//go:build it + +package system + +import ( + "context" + "fmt" + "github.com/formancehq/go-libs/v2/bun/bunconnect" + "github.com/formancehq/go-libs/v2/bun/bundebug" + "github.com/formancehq/go-libs/v2/bun/bunpaginate" + "github.com/formancehq/go-libs/v2/metadata" + "github.com/formancehq/go-libs/v2/testing/docker" + ledger "github.com/formancehq/ledger/internal" + ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" + "github.com/google/uuid" + "github.com/uptrace/bun" + "golang.org/x/sync/errgroup" + "os" + "slices" + "testing" + "time" + + "github.com/formancehq/go-libs/v2/logging" + "github.com/stretchr/testify/require" +) + +func TestLedgersCreate(t *testing.T) { + t.Parallel() + + ctx := logging.TestingContext() + store := newStore(t) + + const count = 30 + grp, ctx := errgroup.WithContext(ctx) + createdLedgersChan := make(chan ledger.Ledger, count) + + for i := range count { + grp.Go(func() error { + l := ledger.MustNewWithDefault(fmt.Sprintf("ledger%d", i)) + + ctx, cancel := context.WithDeadline(ctx, time.Now().Add(40*time.Second)) + defer cancel() + + err := store.CreateLedger(ctx, &l) + if err != nil { + return err + } + createdLedgersChan <- l + + return nil + }) + } + + require.NoError(t, grp.Wait()) + + close(createdLedgersChan) + + createdLedgers := make([]ledger.Ledger, 0) + for createdLedger := range createdLedgersChan { + createdLedgers = append(createdLedgers, createdLedger) + } + + slices.SortStableFunc(createdLedgers, func(a, b ledger.Ledger) int { + return a.ID - b.ID + }) + + for i, createdLedger := range createdLedgers { + require.Equal(t, i+1, createdLedger.ID) + require.NotEmpty(t, createdLedger.AddedAt) + } +} + +func TestLedgersList(t *testing.T) { + t.Parallel() + + ctx := logging.TestingContext() + store := newStore(t) + + ledgers := make([]ledger.Ledger, 0) + pageSize := uint64(2) + count := uint64(10) + for i := uint64(0); i < count; i++ { + m := metadata.Metadata{} + if i%2 == 0 { + m["foo"] = "bar" + } + l := ledger.MustNewWithDefault(fmt.Sprintf("ledger%d", i)).WithMetadata(m) + err := store.CreateLedger(ctx, &l) + require.NoError(t, err) + + ledgers = append(ledgers, l) + } + + cursor, err := store.ListLedgers(ctx, ledgercontroller.NewListLedgersQuery(pageSize)) + require.NoError(t, err) + require.Len(t, cursor.Data, int(pageSize)) + require.Equal(t, ledgers[:pageSize], cursor.Data) + + for i := pageSize; i < count; i += pageSize { + query := ledgercontroller.ListLedgersQuery{} + require.NoError(t, bunpaginate.UnmarshalCursor(cursor.Next, &query)) + + cursor, err = store.ListLedgers(ctx, query) + require.NoError(t, err) + require.Len(t, cursor.Data, 2) + require.Equal(t, ledgers[i:i+pageSize], cursor.Data) + } +} + +func TestLedgerUpdateMetadata(t *testing.T) { + t.Parallel() + + ctx := logging.TestingContext() + store := newStore(t) + + l := ledger.MustNewWithDefault(uuid.NewString()) + err := store.CreateLedger(ctx, &l) + require.NoError(t, err) + + addedMetadata := metadata.Metadata{ + "foo": "bar", + } + err = store.UpdateLedgerMetadata(ctx, l.Name, addedMetadata) + require.NoError(t, err) + + ledgerFromDB, err := store.GetLedger(ctx, l.Name) + require.NoError(t, err) + require.Equal(t, addedMetadata, ledgerFromDB.Metadata) +} + +func TestLedgerDeleteMetadata(t *testing.T) { + t.Parallel() + + ctx := logging.TestingContext() + store := newStore(t) + + l := ledger.MustNewWithDefault(uuid.NewString()).WithMetadata(metadata.Metadata{ + "foo": "bar", + }) + + err := store.CreateLedger(ctx, &l) + require.NoError(t, err) + + err = store.DeleteLedgerMetadata(ctx, l.Name, "foo") + require.NoError(t, err) + + ledgerFromDB, err := store.GetLedger(ctx, l.Name) + require.NoError(t, err) + require.Equal(t, metadata.Metadata{}, ledgerFromDB.Metadata) +} + +func newStore(t docker.T) Store { + t.Helper() + + ctx := logging.TestingContext() + pgDatabase := srv.NewDatabase(t) + + hooks := make([]bun.QueryHook, 0) + if os.Getenv("DEBUG") == "true" { + hooks = append(hooks, bundebug.NewQueryHook()) + } + db, err := bunconnect.OpenSQLDB(ctx, pgDatabase.ConnectionOptions(), hooks...) + require.NoError(t, err) + + ret := New(db) + require.NoError(t, ret.Migrate(ctx)) + + return ret +} diff --git a/test/e2e/app_lifecycle_test.go b/test/e2e/app_lifecycle_test.go index badcb6bc5..2fdfcda81 100644 --- a/test/e2e/app_lifecycle_test.go +++ b/test/e2e/app_lifecycle_test.go @@ -179,7 +179,7 @@ var _ = Context("Ledger application lifecycle tests", func() { bunDB, err := bunconnect.OpenSQLDB(ctx, db.GetValue().ConnectionOptions()) Expect(err).To(BeNil()) - Expect(driver.Migrate(ctx, bunDB)).To(BeNil()) + Expect(system.Migrate(ctx, bunDB)).To(BeNil()) _, err = bunDB.NewInsert(). Model(pointer.For(ledger.MustNewWithDefault(ledgerName))). diff --git a/test/e2e/suite_test.go b/test/e2e/suite_test.go index 329004535..f3286efeb 100644 --- a/test/e2e/suite_test.go +++ b/test/e2e/suite_test.go @@ -60,7 +60,7 @@ var _ = SynchronizedBeforeSuite(func() []byte { bunDB, err := bunconnect.OpenSQLDB(context.Background(), templateDatabase.ConnectionOptions()) Expect(err).To(BeNil()) - err = driver.Migrate(context.Background(), bunDB) + err = system.Migrate(context.Background(), bunDB) Expect(err).To(BeNil()) // Initialize the _default bucket on the default database From d0f69789a371d979cf0253d7ece33f9b093a3167 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 21 Nov 2024 20:34:28 +0100 Subject: [PATCH 29/71] feat: make buckets migration resilient regarding a bucket failure --- Earthfile | 8 ++- go.mod | 6 +- go.sum | 8 +-- internal/storage/driver/driver.go | 81 ++++++++++++++++---------- internal/storage/driver/driver_test.go | 43 +++++++++----- internal/storage/module.go | 35 ++--------- test/rolling-upgrades/go.mod | 2 +- test/rolling-upgrades/go.sum | 4 +- tools/generator/go.mod | 2 +- tools/generator/go.sum | 10 ++-- 10 files changed, 104 insertions(+), 95 deletions(-) diff --git a/Earthfile b/Earthfile index 43deeaa4d..3814b394c 100644 --- a/Earthfile +++ b/Earthfile @@ -66,15 +66,17 @@ tests: CACHE --id go-mod-cache /go/pkg/mod CACHE --id go-cache /root/.cache/go-build RUN go install github.com/onsi/ginkgo/v2/ginkgo@latest + RUN apk add gcc musl-dev + COPY --dir --pass-args (+generate/*) . ARG includeIntegrationTests="true" ARG coverage="" ARG debug=false + ARG additionalArgs="" ENV DEBUG=$debug ENV CGO_ENABLED=1 # required for -race - RUN apk add gcc musl-dev LET goFlags="-race" IF [ "$coverage" = "true" ] @@ -90,10 +92,10 @@ tests: IF [ "$includeIntegrationTests" = "true" ] SET goFlags="$goFlags -tags it" WITH DOCKER --load=postgres:15-alpine=+postgres - RUN go test $goFlags ./... + RUN go test $goFlags $additionalArgs ./... END ELSE - RUN go test $goFlags ./... + RUN go test $goFlags $additionalArgs ./... END IF [ "$coverage" = "true" ] # as special case, exclude files suffixed by debug.go diff --git a/go.mod b/go.mod index b3e8e4963..52290d3f2 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 github.com/bluele/gcache v0.0.2 github.com/dop251/goja v0.0.0-20241009100908-5f46f2705ca3 - github.com/formancehq/go-libs/v2 v2.0.1-0.20241121113152-18a3fc7aa771 + github.com/formancehq/go-libs/v2 v2.0.1-0.20241121194732-b79c48b683f2 github.com/formancehq/ledger/pkg/client v0.0.0-00010101000000-000000000000 github.com/go-chi/chi/v5 v5.1.0 github.com/go-chi/cors v1.2.1 @@ -25,7 +25,7 @@ require ( github.com/jamiealquiza/tachymeter v2.0.0+incompatible github.com/logrusorgru/aurora v2.0.3+incompatible github.com/nats-io/nats.go v1.37.0 - github.com/onsi/ginkgo/v2 v2.21.0 + github.com/onsi/ginkgo/v2 v2.22.0 github.com/onsi/gomega v1.35.1 github.com/ory/dockertest/v3 v3.11.0 github.com/pborman/uuid v1.2.1 @@ -161,7 +161,7 @@ require ( github.com/rs/cors v1.11.1 // indirect github.com/shirou/gopsutil/v4 v4.24.10 // indirect github.com/shomali11/util v0.0.0-20220717175126-f0771b70947f // indirect - github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sirupsen/logrus v1.9.3 github.com/tklauser/go-sysconf v0.3.14 // indirect github.com/tklauser/numcpus v0.9.0 // indirect github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect diff --git a/go.sum b/go.sum index b821cfba7..09b86c360 100644 --- a/go.sum +++ b/go.sum @@ -104,8 +104,8 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241121113152-18a3fc7aa771 h1:iv1nF1Q+zEkMtc5HhSxNdCRQGAT0s+oCpW5SzyXbnT0= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241121113152-18a3fc7aa771/go.mod h1:6vkHEfWEkDSPOv/G2o1Exxra3ouuYxRiCkznwKxTMHU= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241121194732-b79c48b683f2 h1:JsacSDe6MYGCm04ZY/VJyYTUAWg8jhOBZm6AGyErF/I= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241121194732-b79c48b683f2/go.mod h1:ZGHVcAC54ZbPgeoGKy/d31CHy94RMhHQlX+Vui15ST8= github.com/formancehq/numscript v0.0.9-0.20241009144012-1150c14a1417 h1:LOd5hxnXDIBcehFrpW1OnXk+VSs0yJXeu1iAOO+Hji4= github.com/formancehq/numscript v0.0.9-0.20241009144012-1150c14a1417/go.mod h1:btuSv05cYwi9BvLRxVs5zrunU+O1vTgigG1T6UsawcY= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= @@ -266,8 +266,8 @@ github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= -github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= +github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= diff --git a/internal/storage/driver/driver.go b/internal/storage/driver/driver.go index cc72eb5b6..52ca88cba 100644 --- a/internal/storage/driver/driver.go +++ b/internal/storage/driver/driver.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "github.com/alitto/pond" "github.com/formancehq/go-libs/v2/metadata" "github.com/formancehq/go-libs/v2/migrations" "github.com/formancehq/go-libs/v2/platform/postgres" @@ -13,7 +14,6 @@ import ( noopmetrics "go.opentelemetry.io/otel/metric/noop" "go.opentelemetry.io/otel/trace" nooptracer "go.opentelemetry.io/otel/trace/noop" - "golang.org/x/sync/errgroup" "time" ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" @@ -155,38 +155,58 @@ func (d *Driver) UpgradeAllBuckets(ctx context.Context, minimalVersionReached ch sem := make(chan struct{}, len(buckets)) - grp, ctx := errgroup.WithContext(ctx) + wp := pond.New(10, len(buckets), pond.Context(ctx)) + for _, bucketName := range buckets { - grp.Go(func() error { + wp.Submit(func() { logger := logging.FromContext(ctx).WithFields(map[string]any{ "bucket": bucketName, }) b := d.bucketFactory.Create(bucketName) - minimalVersionReached := make(chan struct{}) - - go func() { - select { - case <-ctx.Done(): - return - case <-minimalVersionReached: - logger.Infof("Reached minimal workable version") - sem <- struct{}{} + // copy semaphore to be able to nil it + sem := sem + + l: + for { + minimalVersionReached := make(chan struct{}) + errChan := make(chan error, 1) + go func() { + logger.Infof("Upgrading...") + errChan <- b.Migrate( + ctx, + minimalVersionReached, + migrations.WithLockRetryInterval(d.migratorLockRetryInterval), + ) + }() + + for { + logger.Infof("Waiting termination") + select { + case <-ctx.Done(): + return + case err := <-errChan: + if err != nil { + logger.Errorf("Error upgrading: %s", err) + continue l + } + if sem != nil { + logger.Infof("Reached minimal workable version") + sem <- struct{}{} + } + + logger.Info("Upgrade terminated") + return + case <-minimalVersionReached: + minimalVersionReached = nil + if sem != nil { + logger.Infof("Reached minimal workable version") + sem <- struct{}{} + sem = nil + } + } } - }() - - logger.Infof("Upgrading...") - if err := b.Migrate( - ctx, - minimalVersionReached, - migrations.WithLockRetryInterval(d.migratorLockRetryInterval), - ); err != nil { - logger.Errorf("Error upgrading: %s", err) - return err } - logging.Infof("Up to date") - - return nil }) } @@ -199,14 +219,11 @@ func (d *Driver) UpgradeAllBuckets(ctx context.Context, minimalVersionReached ch } logging.FromContext(ctx).Infof("All buckets have reached minimal workable version") - select { - case <-minimalVersionReached: - // already closed - default: - close(minimalVersionReached) - } + close(minimalVersionReached) + + wp.StopAndWait() - return grp.Wait() + return nil } func New( diff --git a/internal/storage/driver/driver_test.go b/internal/storage/driver/driver_test.go index 9d5dbc791..6c2578bf4 100644 --- a/internal/storage/driver/driver_test.go +++ b/internal/storage/driver/driver_test.go @@ -14,9 +14,11 @@ import ( "github.com/formancehq/ledger/internal/storage/driver" ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" "github.com/google/uuid" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" "testing" + "time" ) func TestUpgradeAllLedgers(t *testing.T) { @@ -41,7 +43,7 @@ func TestUpgradeAllLedgers(t *testing.T) { bucket := driver.NewMockBucket(ctrl) systemStore.EXPECT(). - GetDistinctBuckets(ctx). + GetDistinctBuckets(gomock.Any()). Return([]string{ledger.DefaultBucket}, nil) bucketFactory.EXPECT(). @@ -56,6 +58,9 @@ func TestUpgradeAllLedgers(t *testing.T) { return nil }) + ctx, cancel := context.WithTimeout(ctx, 2*time.Second) + t.Cleanup(cancel) + require.NoError(t, d.UpgradeAllBuckets(ctx, make(chan struct{}))) }) @@ -96,25 +101,28 @@ func TestUpgradeAllLedgers(t *testing.T) { } systemStore.EXPECT(). - GetDistinctBuckets(ctx). + GetDistinctBuckets(gomock.Any()). Return(bucketList, nil) + ctx, cancel := context.WithTimeout(ctx, 2*time.Second) + t.Cleanup(cancel) + require.NoError(t, d.UpgradeAllBuckets(ctx, make(chan struct{}))) }) t.Run("and error", func(t *testing.T) { t.Parallel() + //ctx := context.Background() + + ctx := logging.ContextWithLogger(ctx, logging.NewLogrus(logrus.New())) + ctrl := gomock.NewController(t) ledgerStoreFactory := driver.NewLedgerStoreFactory(ctrl) bucketFactory := driver.NewBucketFactory(ctrl) systemStore := driver.NewSystemStore(ctrl) - d := driver.New( - ledgerStoreFactory, - systemStore, - bucketFactory, - ) + d := driver.New(ledgerStoreFactory, systemStore, bucketFactory) bucket1 := driver.NewMockBucket(ctrl) bucket2 := driver.NewMockBucket(ctrl) @@ -141,11 +149,11 @@ func TestUpgradeAllLedgers(t *testing.T) { DoAndReturn(func(ctx context.Context, minimalVersionReached chan struct{}, opts ...migrations.Option) error { close(minimalVersionReached) close(bucket1MigrationStarted) - <-ctx.Done() - return ctx.Err() + return nil }) + firstCall := true bucket2.EXPECT(). Migrate(gomock.Any(), gomock.Any(), gomock.Any()). AnyTimes(). @@ -154,21 +162,26 @@ func TestUpgradeAllLedgers(t *testing.T) { case <-ctx.Done(): return ctx.Err() case <-bucket1MigrationStarted: - return errors.New("unknown error") + if firstCall { + firstCall = false + return errors.New("unknown error") + } + close(minimalVersionReached) + return nil } }) systemStore.EXPECT(). - GetDistinctBuckets(ctx). + GetDistinctBuckets(gomock.Any()). AnyTimes(). Return(bucketList, nil) - err := d.UpgradeAllBuckets(ctx, allBucketsMinimalVersionReached) - require.Error(t, err) + ctx, cancel := context.WithTimeout(ctx, 2*time.Second) + t.Cleanup(cancel) bucket1MigrationStarted = make(chan struct{}) - err = d.UpgradeAllBuckets(ctx, allBucketsMinimalVersionReached) - require.Error(t, err) + err := d.UpgradeAllBuckets(ctx, allBucketsMinimalVersionReached) + require.NoError(t, err) }) }) } diff --git a/internal/storage/module.go b/internal/storage/module.go index fd524c8f0..29597c847 100644 --- a/internal/storage/module.go +++ b/internal/storage/module.go @@ -2,7 +2,6 @@ package storage import ( "context" - "errors" "github.com/formancehq/go-libs/v2/logging" "github.com/formancehq/ledger/internal/storage/driver" "go.uber.org/fx" @@ -23,14 +22,13 @@ func NewFXModule(autoUpgrade bool) fx.Option { ) lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { - upgradeContext, cancelContext = context.WithCancel(logging.ContextWithLogger( - context.Background(), - logging.FromContext(ctx), - )) + upgradeContext, cancelContext = context.WithCancel(context.WithoutCancel(ctx)) go func() { defer close(upgradeStopped) - migrate(upgradeContext, driver, minimalVersionReached) + if err := driver.UpgradeAllBuckets(upgradeContext, minimalVersionReached); err != nil { + logging.FromContext(ctx).Errorf("failed to upgrade all buckets: %v", err) + } }() select { @@ -54,27 +52,4 @@ func NewFXModule(autoUpgrade bool) fx.Option { ) } return fx.Options(ret...) -} - -func migrate(ctx context.Context, driver *driver.Driver, minimalVersionReached chan struct{}) { - for { - select { - case <-ctx.Done(): - return - default: - logging.FromContext(ctx).Infof("Upgrading buckets...") - if err := driver.UpgradeAllBuckets(ctx, minimalVersionReached); err != nil { - // Long migrations can be cancelled (app rescheduled for example) - // before fully terminated, handle this gracefully, don't panic, - // the next start will try again. - if errors.Is(err, context.DeadlineExceeded) || - errors.Is(err, context.Canceled) { - return - } - logging.FromContext(ctx).Errorf("Upgrading buckets: %s", err) - continue - } - return - } - } -} +} \ No newline at end of file diff --git a/test/rolling-upgrades/go.mod b/test/rolling-upgrades/go.mod index 3c7e86cb2..e6fe130ca 100644 --- a/test/rolling-upgrades/go.mod +++ b/test/rolling-upgrades/go.mod @@ -9,7 +9,7 @@ replace github.com/formancehq/ledger/pkg/client => ../../pkg/client replace github.com/formancehq/ledger => ../.. require ( - github.com/formancehq/go-libs/v2 v2.0.1-0.20241121113152-18a3fc7aa771 + github.com/formancehq/go-libs/v2 v2.0.1-0.20241121194732-b79c48b683f2 github.com/formancehq/ledger v0.0.0-00010101000000-000000000000 github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.12.0 github.com/pulumi/pulumi/sdk/v3 v3.117.0 diff --git a/test/rolling-upgrades/go.sum b/test/rolling-upgrades/go.sum index 22d254e7e..189d81373 100644 --- a/test/rolling-upgrades/go.sum +++ b/test/rolling-upgrades/go.sum @@ -56,8 +56,8 @@ github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241121113152-18a3fc7aa771 h1:iv1nF1Q+zEkMtc5HhSxNdCRQGAT0s+oCpW5SzyXbnT0= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241121113152-18a3fc7aa771/go.mod h1:6vkHEfWEkDSPOv/G2o1Exxra3ouuYxRiCkznwKxTMHU= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241121194732-b79c48b683f2 h1:JsacSDe6MYGCm04ZY/VJyYTUAWg8jhOBZm6AGyErF/I= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241121194732-b79c48b683f2/go.mod h1:ZGHVcAC54ZbPgeoGKy/d31CHy94RMhHQlX+Vui15ST8= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= diff --git a/tools/generator/go.mod b/tools/generator/go.mod index 507ef38d4..149dbb3ec 100644 --- a/tools/generator/go.mod +++ b/tools/generator/go.mod @@ -9,7 +9,7 @@ replace github.com/formancehq/ledger => ../.. replace github.com/formancehq/ledger/pkg/client => ../../pkg/client require ( - github.com/formancehq/go-libs/v2 v2.0.1-0.20241121113152-18a3fc7aa771 + github.com/formancehq/go-libs/v2 v2.0.1-0.20241121194732-b79c48b683f2 github.com/formancehq/ledger v0.0.0-00010101000000-000000000000 github.com/formancehq/ledger/pkg/client v0.0.0-00010101000000-000000000000 github.com/spf13/cobra v1.8.1 diff --git a/tools/generator/go.sum b/tools/generator/go.sum index 085f8e789..67c8b8372 100644 --- a/tools/generator/go.sum +++ b/tools/generator/go.sum @@ -22,6 +22,8 @@ github.com/ThreeDotsLabs/watermill-nats/v2 v2.1.2 h1:9d7Vb2gepq73Rn/aKaAJWbBiJzS github.com/ThreeDotsLabs/watermill-nats/v2 v2.1.2/go.mod h1:stjbT+s4u/s5ime5jdIyvPyjBGwGeJewIN7jxH8gp4k= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/alitto/pond v1.9.2 h1:9Qb75z/scEZVCoSU+osVmQ0I0JOeLfdTDafrbcJ8CLs= +github.com/alitto/pond v1.9.2/go.mod h1:xQn3P/sHTYcU/1BR3i86IGIrilcrGC2LiS+E2+CJWsI= github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 h1:yL7+Jz0jTC6yykIK/Wh74gnTJnrGr5AyrNMXuA0gves= github.com/antlr/antlr4/runtime/Go/antlr v1.4.10/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= @@ -100,8 +102,8 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241121113152-18a3fc7aa771 h1:iv1nF1Q+zEkMtc5HhSxNdCRQGAT0s+oCpW5SzyXbnT0= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241121113152-18a3fc7aa771/go.mod h1:6vkHEfWEkDSPOv/G2o1Exxra3ouuYxRiCkznwKxTMHU= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241121194732-b79c48b683f2 h1:JsacSDe6MYGCm04ZY/VJyYTUAWg8jhOBZm6AGyErF/I= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241121194732-b79c48b683f2/go.mod h1:ZGHVcAC54ZbPgeoGKy/d31CHy94RMhHQlX+Vui15ST8= github.com/formancehq/numscript v0.0.9-0.20241009144012-1150c14a1417 h1:LOd5hxnXDIBcehFrpW1OnXk+VSs0yJXeu1iAOO+Hji4= github.com/formancehq/numscript v0.0.9-0.20241009144012-1150c14a1417/go.mod h1:btuSv05cYwi9BvLRxVs5zrunU+O1vTgigG1T6UsawcY= github.com/gkampitakis/ciinfo v0.3.0 h1:gWZlOC2+RYYttL0hBqcoQhM7h1qNkVqvRCV1fOvpAv8= @@ -240,8 +242,8 @@ github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= -github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= +github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= From d0f36bb65b5a9b67b20896bc6ff1f566c5c98085 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 20 Nov 2024 16:27:53 +0100 Subject: [PATCH 30/71] feat: add capability to create transaction at controller level --- Earthfile | 1 + docs/api/README.md | 2 + .../common/mocks_ledger_controller_test.go | 43 +++ .../api/v1/mocks_ledger_controller_test.go | 43 +++ internal/api/v2/controllers_bulk.go | 31 +- internal/api/v2/controllers_bulk_test.go | 69 +++- .../api/v2/mocks_ledger_controller_test.go | 43 +++ internal/controller/ledger/controller.go | 5 + .../controller/ledger/controller_default.go | 129 ++++--- .../ledger/controller_default_test.go | 86 +++-- .../ledger/controller_generated_test.go | 43 +++ ...ontroller_with_too_many_client_handling.go | 81 ++-- .../ledger/controller_with_traces.go | 19 + internal/controller/ledger/log_process.go | 48 ++- .../controller/ledger/log_process_test.go | 44 ++- .../controller/ledger/numscript_runtime.go | 14 +- .../numscript_runtime_generated_test.go | 2 +- internal/controller/ledger/store.go | 32 +- .../controller/ledger/store_generated_test.go | 361 ++++++++---------- internal/storage/ledger/legacy/adapters.go | 91 ++--- internal/storage/ledger/store.go | 77 +++- openapi.yaml | 12 + openapi/v2.yaml | 12 + pkg/client/.speakeasy/gen.lock | 6 +- pkg/client/.speakeasy/gen.yaml | 2 +- .../models/operations/v2createbulkrequest.md | 2 + pkg/client/docs/sdks/v2/README.md | 2 + pkg/client/formance.go | 4 +- pkg/client/models/operations/v2createbulk.go | 20 +- pkg/client/v2.go | 4 + test/e2e/api_bulk_test.go | 36 +- test/e2e/api_transactions_revert_test.go | 2 +- 32 files changed, 931 insertions(+), 435 deletions(-) diff --git a/Earthfile b/Earthfile index 3814b394c..ea6d1b265 100644 --- a/Earthfile +++ b/Earthfile @@ -35,6 +35,7 @@ generate: RUN go install github.com/princjef/gomarkdoc/cmd/gomarkdoc@latest COPY (+tidy/*) /src/ COPY --dir (+sources/src/*) /src/ + WORKDIR /src RUN go generate ./... SAVE ARTIFACT internal AS LOCAL internal diff --git a/docs/api/README.md b/docs/api/README.md index 4d6d4fa02..f8e344946 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -498,6 +498,8 @@ Accept: application/json |Name|In|Type|Required|Description| |---|---|---|---|---| |ledger|path|string|true|Name of the ledger.| +|continueOnFailure|query|boolean|false|Continue on failure| +|atomic|query|boolean|false|Make bulk atomic| |body|body|[V2Bulk](#schemav2bulk)|false|none| > Example responses diff --git a/internal/api/common/mocks_ledger_controller_test.go b/internal/api/common/mocks_ledger_controller_test.go index cfa5e8724..4e3c01be1 100644 --- a/internal/api/common/mocks_ledger_controller_test.go +++ b/internal/api/common/mocks_ledger_controller_test.go @@ -7,6 +7,7 @@ package common import ( context "context" + sql "database/sql" reflect "reflect" bunpaginate "github.com/formancehq/go-libs/v2/bun/bunpaginate" @@ -39,6 +40,34 @@ func (m *LedgerController) EXPECT() *LedgerControllerMockRecorder { return m.recorder } +// BeginTX mocks base method. +func (m *LedgerController) BeginTX(ctx context.Context, options *sql.TxOptions) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BeginTX", ctx, options) + ret0, _ := ret[0].(error) + return ret0 +} + +// BeginTX indicates an expected call of BeginTX. +func (mr *LedgerControllerMockRecorder) BeginTX(ctx, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BeginTX", reflect.TypeOf((*LedgerController)(nil).BeginTX), ctx, options) +} + +// Commit mocks base method. +func (m *LedgerController) Commit(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Commit", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// Commit indicates an expected call of Commit. +func (mr *LedgerControllerMockRecorder) Commit(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Commit", reflect.TypeOf((*LedgerController)(nil).Commit), ctx) +} + // CountAccounts mocks base method. func (m *LedgerController) CountAccounts(ctx context.Context, query ledger0.ListAccountsQuery) (int, error) { m.ctrl.T.Helper() @@ -309,6 +338,20 @@ func (mr *LedgerControllerMockRecorder) RevertTransaction(ctx, parameters any) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevertTransaction", reflect.TypeOf((*LedgerController)(nil).RevertTransaction), ctx, parameters) } +// Rollback mocks base method. +func (m *LedgerController) Rollback(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Rollback", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// Rollback indicates an expected call of Rollback. +func (mr *LedgerControllerMockRecorder) Rollback(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rollback", reflect.TypeOf((*LedgerController)(nil).Rollback), ctx) +} + // SaveAccountMetadata mocks base method. func (m *LedgerController) SaveAccountMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.SaveAccountMetadata]) (*ledger.Log, error) { m.ctrl.T.Helper() diff --git a/internal/api/v1/mocks_ledger_controller_test.go b/internal/api/v1/mocks_ledger_controller_test.go index a849cf030..adc36d2e3 100644 --- a/internal/api/v1/mocks_ledger_controller_test.go +++ b/internal/api/v1/mocks_ledger_controller_test.go @@ -7,6 +7,7 @@ package v1 import ( context "context" + sql "database/sql" reflect "reflect" bunpaginate "github.com/formancehq/go-libs/v2/bun/bunpaginate" @@ -39,6 +40,34 @@ func (m *LedgerController) EXPECT() *LedgerControllerMockRecorder { return m.recorder } +// BeginTX mocks base method. +func (m *LedgerController) BeginTX(ctx context.Context, options *sql.TxOptions) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BeginTX", ctx, options) + ret0, _ := ret[0].(error) + return ret0 +} + +// BeginTX indicates an expected call of BeginTX. +func (mr *LedgerControllerMockRecorder) BeginTX(ctx, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BeginTX", reflect.TypeOf((*LedgerController)(nil).BeginTX), ctx, options) +} + +// Commit mocks base method. +func (m *LedgerController) Commit(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Commit", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// Commit indicates an expected call of Commit. +func (mr *LedgerControllerMockRecorder) Commit(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Commit", reflect.TypeOf((*LedgerController)(nil).Commit), ctx) +} + // CountAccounts mocks base method. func (m *LedgerController) CountAccounts(ctx context.Context, query ledger0.ListAccountsQuery) (int, error) { m.ctrl.T.Helper() @@ -309,6 +338,20 @@ func (mr *LedgerControllerMockRecorder) RevertTransaction(ctx, parameters any) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevertTransaction", reflect.TypeOf((*LedgerController)(nil).RevertTransaction), ctx, parameters) } +// Rollback mocks base method. +func (m *LedgerController) Rollback(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Rollback", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// Rollback indicates an expected call of Rollback. +func (mr *LedgerControllerMockRecorder) Rollback(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rollback", reflect.TypeOf((*LedgerController)(nil).Rollback), ctx) +} + // SaveAccountMetadata mocks base method. func (m *LedgerController) SaveAccountMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.SaveAccountMetadata]) (*ledger.Log, error) { m.ctrl.T.Helper() diff --git a/internal/api/v2/controllers_bulk.go b/internal/api/v2/controllers_bulk.go index 9eb3ce18a..8b190d462 100644 --- a/internal/api/v2/controllers_bulk.go +++ b/internal/api/v2/controllers_bulk.go @@ -31,7 +31,13 @@ func bulkHandler(bulkMaxSize int) http.HandlerFunc { w.Header().Set("Content-Type", "application/json") - ret, errorsInBulk, err := ProcessBulk(r.Context(), common.LedgerFromContext(r.Context()), b, api.QueryParamBool(r, "continueOnFailure")) + ret, errorsInBulk, err := ProcessBulk( + r.Context(), + common.LedgerFromContext(r.Context()), + b, + api.QueryParamBool(r, "continueOnFailure"), + api.QueryParamBool(r, "atomic"), + ) if err != nil { api.InternalServerError(w, r, err) return @@ -122,12 +128,7 @@ func (req *TransactionRequest) ToRunScript(allowUnboundedOverdrafts bool) (*ledg }, nil } -func ProcessBulk( - ctx context.Context, - l ledgercontroller.Controller, - bulk Bulk, - continueOnFailure bool, -) ([]Result, bool, error) { +func ProcessBulk(ctx context.Context, l ledgercontroller.Controller, bulk Bulk, continueOnFailure bool, atomic bool) ([]Result, bool, error) { for i, element := range bulk { switch element.Action { @@ -166,6 +167,15 @@ func ProcessBulk( errorsInBulk = true } + if atomic { + if err := l.BeginTX(ctx, nil); err != nil { + return nil, errorsInBulk, fmt.Errorf("error starting transaction: %s", err) + } + defer func() { + _ = l.Rollback(ctx) + }() + } + for i, element := range bulk { switch element.Action { case ActionCreateTransaction: @@ -370,5 +380,12 @@ func ProcessBulk( } } } + + if atomic { + if err := l.Commit(ctx); err != nil { + return nil, errorsInBulk, fmt.Errorf("error committing transaction: %s", err) + } + } + return ret, errorsInBulk, nil } diff --git a/internal/api/v2/controllers_bulk_test.go b/internal/api/v2/controllers_bulk_test.go index 31f8e857d..a39f447b0 100644 --- a/internal/api/v2/controllers_bulk_test.go +++ b/internal/api/v2/controllers_bulk_test.go @@ -349,9 +349,76 @@ func TestBulk(t *testing.T) { }}, expectError: true, }, + { + name: "with atomic", + body: `[ + { + "action": "ADD_METADATA", + "data": { + "targetId": "world", + "targetType": "ACCOUNT", + "metadata": { + "foo": "bar" + } + } + }, + { + "action": "ADD_METADATA", + "data": { + "targetId": "world", + "targetType": "ACCOUNT", + "metadata": { + "foo2": "bar2" + } + } + } + ]`, + queryParams: map[string][]string{ + "atomic": {"true"}, + }, + expectations: func(mockLedger *LedgerController) { + mockLedger.EXPECT(). + BeginTX(gomock.Any(), nil). + Return(nil) + + mockLedger.EXPECT(). + SaveAccountMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.SaveAccountMetadata]{ + Input: ledgercontroller.SaveAccountMetadata{ + Address: "world", + Metadata: metadata.Metadata{ + "foo": "bar", + }, + }, + }). + Return(&ledger.Log{}, nil) + + mockLedger.EXPECT(). + SaveAccountMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.SaveAccountMetadata]{ + Input: ledgercontroller.SaveAccountMetadata{ + Address: "world", + Metadata: metadata.Metadata{ + "foo2": "bar2", + }, + }, + }). + Return(&ledger.Log{}, nil) + + mockLedger.EXPECT(). + Commit(gomock.Any()). + Return(nil) + + mockLedger.EXPECT(). + Rollback(gomock.Any()). + Return(nil) + }, + expectResults: []Result{{ + ResponseType: ActionAddMetadata, + }, { + ResponseType: ActionAddMetadata, + }}, + }, } for _, testCase := range testCases { - testCase := testCase t.Run(testCase.name, func(t *testing.T) { systemController, ledgerController := newTestingSystemController(t, true) diff --git a/internal/api/v2/mocks_ledger_controller_test.go b/internal/api/v2/mocks_ledger_controller_test.go index 0613e2e93..cc4bf5f39 100644 --- a/internal/api/v2/mocks_ledger_controller_test.go +++ b/internal/api/v2/mocks_ledger_controller_test.go @@ -7,6 +7,7 @@ package v2 import ( context "context" + sql "database/sql" reflect "reflect" bunpaginate "github.com/formancehq/go-libs/v2/bun/bunpaginate" @@ -39,6 +40,34 @@ func (m *LedgerController) EXPECT() *LedgerControllerMockRecorder { return m.recorder } +// BeginTX mocks base method. +func (m *LedgerController) BeginTX(ctx context.Context, options *sql.TxOptions) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BeginTX", ctx, options) + ret0, _ := ret[0].(error) + return ret0 +} + +// BeginTX indicates an expected call of BeginTX. +func (mr *LedgerControllerMockRecorder) BeginTX(ctx, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BeginTX", reflect.TypeOf((*LedgerController)(nil).BeginTX), ctx, options) +} + +// Commit mocks base method. +func (m *LedgerController) Commit(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Commit", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// Commit indicates an expected call of Commit. +func (mr *LedgerControllerMockRecorder) Commit(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Commit", reflect.TypeOf((*LedgerController)(nil).Commit), ctx) +} + // CountAccounts mocks base method. func (m *LedgerController) CountAccounts(ctx context.Context, query ledger0.ListAccountsQuery) (int, error) { m.ctrl.T.Helper() @@ -309,6 +338,20 @@ func (mr *LedgerControllerMockRecorder) RevertTransaction(ctx, parameters any) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevertTransaction", reflect.TypeOf((*LedgerController)(nil).RevertTransaction), ctx, parameters) } +// Rollback mocks base method. +func (m *LedgerController) Rollback(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Rollback", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// Rollback indicates an expected call of Rollback. +func (mr *LedgerControllerMockRecorder) Rollback(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rollback", reflect.TypeOf((*LedgerController)(nil).Rollback), ctx) +} + // SaveAccountMetadata mocks base method. func (m *LedgerController) SaveAccountMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.SaveAccountMetadata]) (*ledger.Log, error) { m.ctrl.T.Helper() diff --git a/internal/controller/ledger/controller.go b/internal/controller/ledger/controller.go index feb401262..d99375ec2 100644 --- a/internal/controller/ledger/controller.go +++ b/internal/controller/ledger/controller.go @@ -2,6 +2,7 @@ package ledger import ( "context" + "database/sql" "github.com/formancehq/go-libs/v2/metadata" "github.com/formancehq/ledger/internal/machine/vm" @@ -13,6 +14,10 @@ import ( //go:generate mockgen -write_source_comment=false -write_package_comment=false -source controller.go -destination controller_generated_test.go -package ledger . Controller type Controller interface { + BeginTX(ctx context.Context, options *sql.TxOptions) error + Commit(ctx context.Context) error + Rollback(ctx context.Context) error + // IsDatabaseUpToDate check if the ledger store is up to date, including the bucket and the ledger specifics // It returns true if up to date IsDatabaseUpToDate(ctx context.Context) (bool, error) diff --git a/internal/controller/ledger/controller_default.go b/internal/controller/ledger/controller_default.go index 133991779..c5d34ce51 100644 --- a/internal/controller/ledger/controller_default.go +++ b/internal/controller/ledger/controller_default.go @@ -51,6 +51,18 @@ type DefaultController struct { deleteAccountMetadataLp *logProcessor[DeleteAccountMetadata, ledger.DeletedMetadata] } +func (ctrl *DefaultController) BeginTX(ctx context.Context, options *sql.TxOptions) error { + return ctrl.store.BeginTX(ctx, options) +} + +func (ctrl *DefaultController) Commit(_ context.Context) error { + return ctrl.store.Commit() +} + +func (ctrl *DefaultController) Rollback(_ context.Context) error { + return ctrl.store.Rollback() +} + func NewDefaultController( l ledger.Ledger, store Store, @@ -127,57 +139,69 @@ func (ctrl *DefaultController) Import(ctx context.Context, stream chan ledger.Lo // Use serializable isolation level to ensure no concurrent request use the store. // If a concurrent transactions is made while we are importing some logs, the transaction importing logs will // be canceled with serialization error. - err := ctrl.store.WithTX(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable}, func(sqlTx TX) (bool, error) { - - // Due to the serializable isolation level, and since we explicitly ask for the ledger state in the sql transaction context - // if the state change, the sql transaction will be aborted with a serialization error - if err := sqlTx.LockLedger(ctx); err != nil { - return false, fmt.Errorf("failed to lock ledger: %w", err) - } + err := ctrl.store.BeginTX(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable}) + if err != nil { + return fmt.Errorf("failed to start transaction: %w", err) + } + defer func() { + _ = ctrl.store.Rollback() + }() + + // Due to the serializable isolation level, and since we explicitly ask for the ledger state in the sql transaction context + // if the state change, the sql transaction will be aborted with a serialization error + if err := ctrl.store.LockLedger(ctx); err != nil { + return fmt.Errorf("failed to lock ledger: %w", err) + } - // We can import only if the ledger is empty. - logs, err := sqlTx.ListLogs(ctx, NewListLogsQuery(PaginatedQueryOptions[any]{ - PageSize: 1, - })) - if err != nil { - return false, fmt.Errorf("error listing logs: %w", err) - } + // We can import only if the ledger is empty. + logs, err := ctrl.store.ListLogs(ctx, NewListLogsQuery(PaginatedQueryOptions[any]{ + PageSize: 1, + })) + if err != nil { + return fmt.Errorf("error listing logs: %w", err) + } - if len(logs.Data) > 0 { - return false, newErrImport(errors.New("ledger must be empty")) - } + if len(logs.Data) > 0 { + return newErrImport(errors.New("ledger must be empty")) + } - for log := range stream { - if err := ctrl.importLog(ctx, sqlTx, log); err != nil { - return false, fmt.Errorf("importing log %d: %w", log.ID, err) + for log := range stream { + if err := ctrl.importLog(ctx, log); err != nil { + if errors.Is(err, postgres.ErrSerialization) { + return newErrImport(errors.New("concurrent transaction occur" + + "red, cannot import the ledger")) } + return fmt.Errorf("importing log %d: %w", log.ID, err) } + } - return true, nil - }) - if err != nil { - if errors.Is(err, postgres.ErrSerialization) { - return newErrImport(errors.New("concurrent transaction occur" + - "red, cannot import the ledger")) - } + if err := ctrl.store.Commit(); err != nil { + return fmt.Errorf("failed to commit transaction: %w", err) } return err } -func (ctrl *DefaultController) importLog(ctx context.Context, sqlTx TX, log ledger.Log) error { +// todo: as ids are generated by the database, the exported ids can not match imported ones +// it can cause issues with actions referencing other objects +func (ctrl *DefaultController) importLog(ctx context.Context, log ledger.Log) error { switch payload := log.Data.(type) { case ledger.CreatedTransaction: - if err := sqlTx.CommitTransaction(ctx, &payload.Transaction); err != nil { + logging.FromContext(ctx).Debugf("Importing transaction %d", payload.Transaction.ID) + if err := ctrl.store.CommitTransaction(ctx, &payload.Transaction); err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } + logging.FromContext(ctx).Debugf("Imported transaction %d", payload.Transaction.ID) + if len(payload.AccountMetadata) > 0 { - if err := sqlTx.UpdateAccountsMetadata(ctx, payload.AccountMetadata); err != nil { + logging.FromContext(ctx).Debugf("Importing metadata of accounts '%s'", Keys(payload.AccountMetadata)) + if err := ctrl.store.UpdateAccountsMetadata(ctx, payload.AccountMetadata); err != nil { return fmt.Errorf("updating metadata of accounts '%s': %w", Keys(payload.AccountMetadata), err) } } case ledger.RevertedTransaction: - _, _, err := sqlTx.RevertTransaction( + logging.FromContext(ctx).Debugf("Reverting transaction %d", payload.RevertedTransaction.ID) + _, _, err := ctrl.store.RevertTransaction( ctx, payload.RevertedTransaction.ID, *payload.RevertedTransaction.RevertedAt, @@ -185,17 +209,19 @@ func (ctrl *DefaultController) importLog(ctx context.Context, sqlTx TX, log ledg if err != nil { return fmt.Errorf("failed to revert transaction: %w", err) } - if err := sqlTx.CommitTransaction(ctx, &payload.RevertTransaction); err != nil { + if err := ctrl.store.CommitTransaction(ctx, &payload.RevertTransaction); err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } case ledger.SavedMetadata: switch payload.TargetType { case ledger.MetaTargetTypeTransaction: - if _, _, err := sqlTx.UpdateTransactionMetadata(ctx, payload.TargetID.(int), payload.Metadata); err != nil { + logging.FromContext(ctx).Debugf("Saving metadata of transaction %d", payload.TargetID) + if _, _, err := ctrl.store.UpdateTransactionMetadata(ctx, payload.TargetID.(int), payload.Metadata); err != nil { return fmt.Errorf("failed to update transaction metadata: %w", err) } case ledger.MetaTargetTypeAccount: - if err := sqlTx.UpdateAccountsMetadata(ctx, ledger.AccountMetadata{ + logging.FromContext(ctx).Debugf("Saving metadata of account %s", payload.TargetID) + if err := ctrl.store.UpdateAccountsMetadata(ctx, ledger.AccountMetadata{ payload.TargetID.(string): payload.Metadata, }); err != nil { return fmt.Errorf("failed to update account metadata: %w", err) @@ -204,18 +230,21 @@ func (ctrl *DefaultController) importLog(ctx context.Context, sqlTx TX, log ledg case ledger.DeletedMetadata: switch payload.TargetType { case ledger.MetaTargetTypeTransaction: - if _, _, err := sqlTx.DeleteTransactionMetadata(ctx, payload.TargetID.(int), payload.Key); err != nil { + logging.FromContext(ctx).Debugf("Deleting metadata of transaction %d", payload.TargetID) + if _, _, err := ctrl.store.DeleteTransactionMetadata(ctx, payload.TargetID.(int), payload.Key); err != nil { return fmt.Errorf("failed to delete transaction metadata: %w", err) } case ledger.MetaTargetTypeAccount: - if err := sqlTx.DeleteAccountMetadata(ctx, payload.TargetID.(string), payload.Key); err != nil { + logging.FromContext(ctx).Debugf("Deleting metadata of account %s", payload.TargetID) + if err := ctrl.store.DeleteAccountMetadata(ctx, payload.TargetID.(string), payload.Key); err != nil { return fmt.Errorf("failed to delete account metadata: %w", err) } } } logCopy := log - if err := sqlTx.InsertLog(ctx, &log); err != nil { + logging.FromContext(ctx).Debugf("Inserting log %d", log.ID) + if err := ctrl.store.InsertLog(ctx, &log); err != nil { return fmt.Errorf("failed to insert log: %w", err) } @@ -255,7 +284,7 @@ func (ctrl *DefaultController) GetVolumesWithBalances(ctx context.Context, q Get return ctrl.store.GetVolumesWithBalances(ctx, q) } -func (ctrl *DefaultController) createTransaction(ctx context.Context, sqlTX TX, parameters Parameters[RunScript]) (*ledger.CreatedTransaction, error) { +func (ctrl *DefaultController) createTransaction(ctx context.Context, sqlTX Store, parameters Parameters[RunScript]) (*ledger.CreatedTransaction, error) { logger := logging.FromContext(ctx).WithField("req", uuid.NewString()[:8]) ctx = logging.ContextWithLogger(ctx, logger) @@ -319,12 +348,12 @@ func (ctrl *DefaultController) CreateTransaction(ctx context.Context, parameters return ctrl.createTransactionLp.forgeLog(ctx, ctrl.store, parameters, ctrl.createTransaction) } -func (ctrl *DefaultController) revertTransaction(ctx context.Context, sqlTX TX, parameters Parameters[RevertTransaction]) (*ledger.RevertedTransaction, error) { +func (ctrl *DefaultController) revertTransaction(ctx context.Context, store Store, parameters Parameters[RevertTransaction]) (*ledger.RevertedTransaction, error) { var ( hasBeenReverted bool err error ) - originalTransaction, hasBeenReverted, err := sqlTX.RevertTransaction(ctx, parameters.Input.TransactionID, time.Time{}) + originalTransaction, hasBeenReverted, err := store.RevertTransaction(ctx, parameters.Input.TransactionID, time.Time{}) if err != nil { return nil, err } @@ -334,7 +363,7 @@ func (ctrl *DefaultController) revertTransaction(ctx context.Context, sqlTX TX, bq := originalTransaction.InvolvedDestinations() - balances, err := sqlTX.GetBalances(ctx, bq) + balances, err := store.GetBalances(ctx, bq) if err != nil { return nil, fmt.Errorf("failed to get balances: %w", err) } @@ -366,7 +395,7 @@ func (ctrl *DefaultController) revertTransaction(ctx context.Context, sqlTX TX, } } - err = sqlTX.CommitTransaction(ctx, &reversedTx) + err = store.CommitTransaction(ctx, &reversedTx) if err != nil { return nil, fmt.Errorf("failed to insert transaction: %w", err) } @@ -381,8 +410,8 @@ func (ctrl *DefaultController) RevertTransaction(ctx context.Context, parameters return ctrl.revertTransactionLp.forgeLog(ctx, ctrl.store, parameters, ctrl.revertTransaction) } -func (ctrl *DefaultController) saveTransactionMetadata(ctx context.Context, sqlTX TX, parameters Parameters[SaveTransactionMetadata]) (*ledger.SavedMetadata, error) { - if _, _, err := sqlTX.UpdateTransactionMetadata(ctx, parameters.Input.TransactionID, parameters.Input.Metadata); err != nil { +func (ctrl *DefaultController) saveTransactionMetadata(ctx context.Context, store Store, parameters Parameters[SaveTransactionMetadata]) (*ledger.SavedMetadata, error) { + if _, _, err := store.UpdateTransactionMetadata(ctx, parameters.Input.TransactionID, parameters.Input.Metadata); err != nil { return nil, err } @@ -398,8 +427,8 @@ func (ctrl *DefaultController) SaveTransactionMetadata(ctx context.Context, para return log, err } -func (ctrl *DefaultController) saveAccountMetadata(ctx context.Context, sqlTX TX, parameters Parameters[SaveAccountMetadata]) (*ledger.SavedMetadata, error) { - if _, err := sqlTX.UpsertAccount(ctx, &ledger.Account{ +func (ctrl *DefaultController) saveAccountMetadata(ctx context.Context, store Store, parameters Parameters[SaveAccountMetadata]) (*ledger.SavedMetadata, error) { + if _, err := store.UpsertAccount(ctx, &ledger.Account{ Address: parameters.Input.Address, Metadata: parameters.Input.Metadata, }); err != nil { @@ -419,8 +448,8 @@ func (ctrl *DefaultController) SaveAccountMetadata(ctx context.Context, paramete return log, err } -func (ctrl *DefaultController) deleteTransactionMetadata(ctx context.Context, sqlTX TX, parameters Parameters[DeleteTransactionMetadata]) (*ledger.DeletedMetadata, error) { - _, modified, err := sqlTX.DeleteTransactionMetadata(ctx, parameters.Input.TransactionID, parameters.Input.Key) +func (ctrl *DefaultController) deleteTransactionMetadata(ctx context.Context, store Store, parameters Parameters[DeleteTransactionMetadata]) (*ledger.DeletedMetadata, error) { + _, modified, err := store.DeleteTransactionMetadata(ctx, parameters.Input.TransactionID, parameters.Input.Key) if err != nil { return nil, err } @@ -441,8 +470,8 @@ func (ctrl *DefaultController) DeleteTransactionMetadata(ctx context.Context, pa return log, err } -func (ctrl *DefaultController) deleteAccountMetadata(ctx context.Context, sqlTX TX, parameters Parameters[DeleteAccountMetadata]) (*ledger.DeletedMetadata, error) { - err := sqlTX.DeleteAccountMetadata(ctx, parameters.Input.Address, parameters.Input.Key) +func (ctrl *DefaultController) deleteAccountMetadata(ctx context.Context, store Store, parameters Parameters[DeleteAccountMetadata]) (*ledger.DeletedMetadata, error) { + err := store.DeleteAccountMetadata(ctx, parameters.Input.Address, parameters.Input.Key) if err != nil { return nil, err } diff --git a/internal/controller/ledger/controller_default_test.go b/internal/controller/ledger/controller_default_test.go index 58fa91329..0996f44ae 100644 --- a/internal/controller/ledger/controller_default_test.go +++ b/internal/controller/ledger/controller_default_test.go @@ -26,7 +26,6 @@ func TestCreateTransaction(t *testing.T) { store := NewMockStore(ctrl) numscriptRuntime := NewMockNumscriptRuntime(ctrl) parser := NewMockNumscriptParser(ctrl) - sqlTX := NewMockTX(ctrl) l := NewDefaultController(ledger.Ledger{}, store, parser) @@ -37,24 +36,29 @@ func TestCreateTransaction(t *testing.T) { Return(numscriptRuntime, nil) store.EXPECT(). - WithTX(gomock.Any(), nil, gomock.Any()). - DoAndReturn(func(_ context.Context, _ *sql.TxOptions, fn func(tx TX) (bool, error)) error { - _, err := fn(sqlTX) - return err - }) + BeginTX(gomock.Any(), nil). + Return(nil) + + store.EXPECT(). + Commit(). + Return(nil) + + store.EXPECT(). + Rollback(). + Return(sql.ErrTxDone) posting := ledger.NewPosting("world", "bank", "USD", big.NewInt(100)) numscriptRuntime.EXPECT(). - Execute(gomock.Any(), sqlTX, runScript.Vars). + Execute(gomock.Any(), store, runScript.Vars). Return(&NumscriptExecutionResult{ Postings: ledger.Postings{posting}, }, nil) - sqlTX.EXPECT(). + store.EXPECT(). CommitTransaction(gomock.Any(), gomock.Any()). Return(nil) - sqlTX.EXPECT(). + store.EXPECT(). InsertLog(gomock.Any(), gomock.Cond(func(x any) bool { return x.(*ledger.Log).Type == ledger.NewLogType })). @@ -74,35 +78,39 @@ func TestRevertTransaction(t *testing.T) { store := NewMockStore(ctrl) parser := NewMockNumscriptParser(ctrl) - sqlTX := NewMockTX(ctrl) ctx := logging.TestingContext() l := NewDefaultController(ledger.Ledger{}, store, parser) store.EXPECT(). - WithTX(gomock.Any(), nil, gomock.Any()). - DoAndReturn(func(_ context.Context, _ *sql.TxOptions, fn func(tx TX) (bool, error)) error { - _, err := fn(sqlTX) - return err - }) + BeginTX(gomock.Any(), nil). + Return(nil) + + store.EXPECT(). + Commit(). + Return(nil) + + store.EXPECT(). + Rollback(). + Return(sql.ErrTxDone) txToRevert := ledger.Transaction{} - sqlTX.EXPECT(). + store.EXPECT(). RevertTransaction(gomock.Any(), 1, time.Time{}). DoAndReturn(func(_ context.Context, _ int, _ time.Time) (*ledger.Transaction, bool, error) { txToRevert.RevertedAt = pointer.For(time.Now()) return &txToRevert, true, nil }) - sqlTX.EXPECT(). + store.EXPECT(). GetBalances(gomock.Any(), gomock.Any()). Return(map[string]map[string]*big.Int{}, nil) - sqlTX.EXPECT(). + store.EXPECT(). CommitTransaction(gomock.Any(), gomock.Any()). Return(nil) - sqlTX.EXPECT(). + store.EXPECT(). InsertLog(gomock.Any(), gomock.Cond(func(x any) bool { return x.(*ledger.Log).Type == ledger.RevertedTransactionLogType })). @@ -122,26 +130,30 @@ func TestSaveTransactionMetadata(t *testing.T) { store := NewMockStore(ctrl) parser := NewMockNumscriptParser(ctrl) - sqlTX := NewMockTX(ctrl) ctx := logging.TestingContext() l := NewDefaultController(ledger.Ledger{}, store, parser) store.EXPECT(). - WithTX(gomock.Any(), nil, gomock.Any()). - DoAndReturn(func(ctx context.Context, _ *sql.TxOptions, fn func(tx TX) (bool, error)) error { - _, err := fn(sqlTX) - return err - }) + BeginTX(gomock.Any(), nil). + Return(nil) + + store.EXPECT(). + Commit(). + Return(nil) + + store.EXPECT(). + Rollback(). + Return(sql.ErrTxDone) m := metadata.Metadata{ "foo": "bar", } - sqlTX.EXPECT(). + store.EXPECT(). UpdateTransactionMetadata(gomock.Any(), 1, m). Return(&ledger.Transaction{}, true, nil) - sqlTX.EXPECT(). + store.EXPECT(). InsertLog(gomock.Any(), gomock.Cond(func(x any) bool { return x.(*ledger.Log).Type == ledger.SetMetadataLogType })). @@ -162,23 +174,27 @@ func TestDeleteTransactionMetadata(t *testing.T) { store := NewMockStore(ctrl) parser := NewMockNumscriptParser(ctrl) - sqlTX := NewMockTX(ctrl) ctx := logging.TestingContext() l := NewDefaultController(ledger.Ledger{}, store, parser) store.EXPECT(). - WithTX(gomock.Any(), nil, gomock.Any()). - DoAndReturn(func(ctx context.Context, _ *sql.TxOptions, fn func(tx TX) (bool, error)) error { - _, err := fn(sqlTX) - return err - }) + BeginTX(gomock.Any(), nil). + Return(nil) + + store.EXPECT(). + Commit(). + Return(nil) - sqlTX.EXPECT(). + store.EXPECT(). + Rollback(). + Return(sql.ErrTxDone) + + store.EXPECT(). DeleteTransactionMetadata(gomock.Any(), 1, "foo"). Return(&ledger.Transaction{}, true, nil) - sqlTX.EXPECT(). + store.EXPECT(). InsertLog(gomock.Any(), gomock.Cond(func(x any) bool { return x.(*ledger.Log).Type == ledger.DeleteMetadataLogType })). diff --git a/internal/controller/ledger/controller_generated_test.go b/internal/controller/ledger/controller_generated_test.go index 7416ba889..e895d1656 100644 --- a/internal/controller/ledger/controller_generated_test.go +++ b/internal/controller/ledger/controller_generated_test.go @@ -7,6 +7,7 @@ package ledger import ( context "context" + sql "database/sql" reflect "reflect" bunpaginate "github.com/formancehq/go-libs/v2/bun/bunpaginate" @@ -38,6 +39,34 @@ func (m *MockController) EXPECT() *MockControllerMockRecorder { return m.recorder } +// BeginTX mocks base method. +func (m *MockController) BeginTX(ctx context.Context, options *sql.TxOptions) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BeginTX", ctx, options) + ret0, _ := ret[0].(error) + return ret0 +} + +// BeginTX indicates an expected call of BeginTX. +func (mr *MockControllerMockRecorder) BeginTX(ctx, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BeginTX", reflect.TypeOf((*MockController)(nil).BeginTX), ctx, options) +} + +// Commit mocks base method. +func (m *MockController) Commit(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Commit", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// Commit indicates an expected call of Commit. +func (mr *MockControllerMockRecorder) Commit(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Commit", reflect.TypeOf((*MockController)(nil).Commit), ctx) +} + // CountAccounts mocks base method. func (m *MockController) CountAccounts(ctx context.Context, query ListAccountsQuery) (int, error) { m.ctrl.T.Helper() @@ -308,6 +337,20 @@ func (mr *MockControllerMockRecorder) RevertTransaction(ctx, parameters any) *go return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevertTransaction", reflect.TypeOf((*MockController)(nil).RevertTransaction), ctx, parameters) } +// Rollback mocks base method. +func (m *MockController) Rollback(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Rollback", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// Rollback indicates an expected call of Rollback. +func (mr *MockControllerMockRecorder) Rollback(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rollback", reflect.TypeOf((*MockController)(nil).Rollback), ctx) +} + // SaveAccountMetadata mocks base method. func (m *MockController) SaveAccountMetadata(ctx context.Context, parameters Parameters[SaveAccountMetadata]) (*ledger.Log, error) { m.ctrl.T.Helper() diff --git a/internal/controller/ledger/controller_with_too_many_client_handling.go b/internal/controller/ledger/controller_with_too_many_client_handling.go index 01b864d63..8098103ee 100644 --- a/internal/controller/ledger/controller_with_too_many_client_handling.go +++ b/internal/controller/ledger/controller_with_too_many_client_handling.go @@ -39,76 +39,113 @@ func NewControllerWithTooManyClientHandling( } func (ctrl *ControllerWithTooManyClientHandling) CreateTransaction(ctx context.Context, parameters Parameters[RunScript]) (*ledger.Log, *ledger.CreatedTransaction, error) { - return handleRetry(ctx, ctrl.tracer, ctrl.delayCalculator, parameters, ctrl.Controller.CreateTransaction) + var ( + log *ledger.Log + createdTransaction *ledger.CreatedTransaction + err error + ) + err = handleRetry(ctx, ctrl.tracer, ctrl.delayCalculator, func(ctx context.Context) error { + log, createdTransaction, err = ctrl.Controller.CreateTransaction(ctx, parameters) + return err + }) + return log, createdTransaction, err } func (ctrl *ControllerWithTooManyClientHandling) RevertTransaction(ctx context.Context, parameters Parameters[RevertTransaction]) (*ledger.Log, *ledger.RevertedTransaction, error) { - return handleRetry(ctx, ctrl.tracer, ctrl.delayCalculator, parameters, ctrl.Controller.RevertTransaction) + var ( + log *ledger.Log + revertedTransaction *ledger.RevertedTransaction + err error + ) + err = handleRetry(ctx, ctrl.tracer, ctrl.delayCalculator, func(ctx context.Context) error { + log, revertedTransaction, err = ctrl.Controller.RevertTransaction(ctx, parameters) + return err + + }) + return log, revertedTransaction, err } func (ctrl *ControllerWithTooManyClientHandling) SaveTransactionMetadata(ctx context.Context, parameters Parameters[SaveTransactionMetadata]) (*ledger.Log, error) { - log, _, err := handleRetry(ctx, ctrl.tracer, ctrl.delayCalculator, parameters, func(ctx context.Context, parameters Parameters[SaveTransactionMetadata]) (*ledger.Log, *struct{}, error) { - log, err := ctrl.Controller.SaveTransactionMetadata(ctx, parameters) - return log, nil, err + var ( + log *ledger.Log + err error + ) + err = handleRetry(ctx, ctrl.tracer, ctrl.delayCalculator, func(ctx context.Context) error { + log, err = ctrl.Controller.SaveTransactionMetadata(ctx, parameters) + return err }) return log, err } func (ctrl *ControllerWithTooManyClientHandling) SaveAccountMetadata(ctx context.Context, parameters Parameters[SaveAccountMetadata]) (*ledger.Log, error) { - log, _, err := handleRetry(ctx, ctrl.tracer, ctrl.delayCalculator, parameters, func(ctx context.Context, parameters Parameters[SaveAccountMetadata]) (*ledger.Log, *struct{}, error) { - log, err := ctrl.Controller.SaveAccountMetadata(ctx, parameters) - return log, nil, err + var ( + log *ledger.Log + err error + ) + err = handleRetry(ctx, ctrl.tracer, ctrl.delayCalculator, func(ctx context.Context) error { + log, err = ctrl.Controller.SaveAccountMetadata(ctx, parameters) + return err }) + return log, err } func (ctrl *ControllerWithTooManyClientHandling) DeleteTransactionMetadata(ctx context.Context, parameters Parameters[DeleteTransactionMetadata]) (*ledger.Log, error) { - log, _, err := handleRetry(ctx, ctrl.tracer, ctrl.delayCalculator, parameters, func(ctx context.Context, parameters Parameters[DeleteTransactionMetadata]) (*ledger.Log, *struct{}, error) { - log, err := ctrl.Controller.DeleteTransactionMetadata(ctx, parameters) - return log, nil, err + var ( + log *ledger.Log + err error + ) + err = handleRetry(ctx, ctrl.tracer, ctrl.delayCalculator, func(ctx context.Context) error { + log, err = ctrl.Controller.DeleteTransactionMetadata(ctx, parameters) + return err }) + return log, err } func (ctrl *ControllerWithTooManyClientHandling) DeleteAccountMetadata(ctx context.Context, parameters Parameters[DeleteAccountMetadata]) (*ledger.Log, error) { - log, _, err := handleRetry(ctx, ctrl.tracer, ctrl.delayCalculator, parameters, func(ctx context.Context, parameters Parameters[DeleteAccountMetadata]) (*ledger.Log, *struct{}, error) { - log, err := ctrl.Controller.DeleteAccountMetadata(ctx, parameters) - return log, nil, err + var ( + log *ledger.Log + err error + ) + err = handleRetry(ctx, ctrl.tracer, ctrl.delayCalculator, func(ctx context.Context) error { + log, err = ctrl.Controller.DeleteAccountMetadata(ctx, parameters) + return err }) + return log, err } var _ Controller = (*ControllerWithTooManyClientHandling)(nil) -func handleRetry[INPUT, OUTPUT any]( +func handleRetry( ctx context.Context, tracer trace.Tracer, delayCalculator DelayCalculator, - parameters Parameters[INPUT], - fn func(ctx context.Context, parameters Parameters[INPUT]) (*ledger.Log, *OUTPUT, error), -) (*ledger.Log, *OUTPUT, error) { + fn func(ctx context.Context) error, +) error { ctx, span := tracer.Start(ctx, "TooManyClientRetrier") defer span.End() count := 0 for { - log, output, err := fn(ctx, parameters) + err := fn(ctx) if err != nil && errors.Is(err, postgres.ErrTooManyClient{}) { delay := delayCalculator.Next(count) if delay == 0 { - return nil, nil, err + return err } select { case <-ctx.Done(): - return nil, nil, ctx.Err() + return ctx.Err() case <-time.After(delay): count++ span.SetAttributes(attribute.Int("retry", count)) continue } } - return log, output, err + return err } } diff --git a/internal/controller/ledger/controller_with_traces.go b/internal/controller/ledger/controller_with_traces.go index 20b505323..39188b027 100644 --- a/internal/controller/ledger/controller_with_traces.go +++ b/internal/controller/ledger/controller_with_traces.go @@ -2,6 +2,7 @@ package ledger import ( "context" + "database/sql" "github.com/formancehq/go-libs/v2/migrations" "github.com/formancehq/ledger/internal/tracing" "go.opentelemetry.io/otel/trace" @@ -22,6 +23,24 @@ func NewControllerWithTraces(underlying Controller, tracer trace.Tracer) *Contro } } +func (ctrl *ControllerWithTraces) BeginTX(ctx context.Context, options *sql.TxOptions) error { + return tracing.SkipResult(tracing.Trace(ctx, ctrl.tracer, "BeginTX", tracing.NoResult(func(ctx context.Context) error { + return ctrl.underlying.BeginTX(ctx, options) + }))) +} + +func (ctrl *ControllerWithTraces) Commit(ctx context.Context) error { + return tracing.SkipResult(tracing.Trace(ctx, ctrl.tracer, "BeginTX", tracing.NoResult(func(ctx context.Context) error { + return ctrl.underlying.Commit(ctx) + }))) +} + +func (ctrl *ControllerWithTraces) Rollback(ctx context.Context) error { + return tracing.SkipResult(tracing.Trace(ctx, ctrl.tracer, "BeginTX", tracing.NoResult(func(ctx context.Context) error { + return ctrl.underlying.Rollback(ctx) + }))) +} + func (ctrl *ControllerWithTraces) GetMigrationsInfo(ctx context.Context) ([]migrations.Info, error) { return ctrl.underlying.GetMigrationsInfo(ctx) } diff --git a/internal/controller/ledger/log_process.go b/internal/controller/ledger/log_process.go index 6415a812d..a3811e861 100644 --- a/internal/controller/ledger/log_process.go +++ b/internal/controller/ledger/log_process.go @@ -29,33 +29,41 @@ func (lp *logProcessor[INPUT, OUTPUT]) runTx( ctx context.Context, store Store, parameters Parameters[INPUT], - fn func(ctx context.Context, sqlTX TX, parameters Parameters[INPUT]) (*OUTPUT, error), + fn func(ctx context.Context, sqlTX Store, parameters Parameters[INPUT]) (*OUTPUT, error), ) (*ledger.Log, *OUTPUT, error) { var ( output *OUTPUT log ledger.Log ) - err := store.WithTX(ctx, nil, func(tx TX) (commit bool, err error) { - output, err = fn(ctx, tx, parameters) - if err != nil { - return false, err - } - log = ledger.NewLog(*output) - log.IdempotencyKey = parameters.IdempotencyKey - log.IdempotencyHash = ledger.ComputeIdempotencyHash(parameters.Input) + if err := store.BeginTX(ctx, nil); err != nil { + return nil, nil, fmt.Errorf("failed to start transaction: %w", err) + } + defer func() { + _ = store.Rollback() + }() - err = tx.InsertLog(ctx, &log) - if err != nil { - return false, fmt.Errorf("failed to insert log: %w", err) - } - logging.FromContext(ctx).Debugf("log inserted with id %d", log.ID) + output, err := fn(ctx, store, parameters) + if err != nil { + return nil, nil, err + } + log = ledger.NewLog(*output) + log.IdempotencyKey = parameters.IdempotencyKey + log.IdempotencyHash = ledger.ComputeIdempotencyHash(parameters.Input) - if parameters.DryRun { - return false, nil - } + err = store.InsertLog(ctx, &log) + if err != nil { + return nil, nil, fmt.Errorf("failed to insert log: %w", err) + } + logging.FromContext(ctx).Debugf("log inserted with id %d", log.ID) + + if parameters.DryRun { + return &log, output, nil + } + + if err := store.Commit(); err != nil { + return nil, nil, fmt.Errorf("failed to commit transaction: %w", err) + } - return true, nil - }) return &log, output, err } @@ -63,7 +71,7 @@ func (lp *logProcessor[INPUT, OUTPUT]) forgeLog( ctx context.Context, store Store, parameters Parameters[INPUT], - fn func(ctx context.Context, sqlTX TX, parameters Parameters[INPUT]) (*OUTPUT, error), + fn func(ctx context.Context, store Store, parameters Parameters[INPUT]) (*OUTPUT, error), ) (*ledger.Log, *OUTPUT, error) { if parameters.IdempotencyKey != "" { log, output, err := lp.fetchLogWithIK(ctx, store, parameters) diff --git a/internal/controller/ledger/log_process_test.go b/internal/controller/ledger/log_process_test.go index 544b42848..c62a58003 100644 --- a/internal/controller/ledger/log_process_test.go +++ b/internal/controller/ledger/log_process_test.go @@ -1,6 +1,7 @@ package ledger import ( + "context" "github.com/formancehq/go-libs/v2/logging" "github.com/formancehq/go-libs/v2/platform/postgres" ledger "github.com/formancehq/ledger/internal" @@ -22,8 +23,12 @@ func TestForgeLogWithIKConflict(t *testing.T) { Return(nil, postgres.ErrNotFound) store.EXPECT(). - WithTX(gomock.Any(), gomock.Any(), gomock.Any()). - Return(ErrIdempotencyKeyConflict{}) + BeginTX(gomock.Any(), gomock.Any()). + Return(nil) + + store.EXPECT(). + Rollback(). + Return(nil) store.EXPECT(). ReadLogWithIdempotencyKey(gomock.Any(), "foo"). @@ -34,7 +39,9 @@ func TestForgeLogWithIKConflict(t *testing.T) { lp := newLogProcessor[RunScript, ledger.CreatedTransaction]("foo", noop.Int64Counter{}) _, _, err := lp.forgeLog(ctx, store, Parameters[RunScript]{ IdempotencyKey: "foo", - }, nil) + }, func(ctx context.Context, store Store, parameters Parameters[RunScript]) (*ledger.CreatedTransaction, error) { + return nil, NewErrIdempotencyKeyConflict("foo") + }) require.NoError(t, err) } @@ -47,15 +54,38 @@ func TestForgeLogWithDeadlock(t *testing.T) { // First call returns a deadlock store.EXPECT(). - WithTX(gomock.Any(), gomock.Any(), gomock.Any()). - Return(postgres.ErrDeadlockDetected) + BeginTX(gomock.Any(), gomock.Any()). + Return(nil) + + store.EXPECT(). + Rollback(). + Return(nil) // Second call is ok store.EXPECT(). - WithTX(gomock.Any(), gomock.Any(), gomock.Any()). + BeginTX(gomock.Any(), gomock.Any()). + Return(nil) + + store.EXPECT(). + InsertLog(gomock.Any(), gomock.Any()). + Return(nil) + + store.EXPECT(). + Commit(). + Return(nil) + + store.EXPECT(). + Rollback(). Return(nil) + firstCall := true lp := newLogProcessor[RunScript, ledger.CreatedTransaction]("foo", noop.Int64Counter{}) - _, _, err := lp.forgeLog(ctx, store, Parameters[RunScript]{}, nil) + _, _, err := lp.forgeLog(ctx, store, Parameters[RunScript]{}, func(ctx context.Context, store Store, parameters Parameters[RunScript]) (*ledger.CreatedTransaction, error) { + if firstCall { + firstCall = false + return nil, postgres.ErrDeadlockDetected + } + return &ledger.CreatedTransaction{}, nil + }) require.NoError(t, err) } diff --git a/internal/controller/ledger/numscript_runtime.go b/internal/controller/ledger/numscript_runtime.go index eec14e643..04d3fdec6 100644 --- a/internal/controller/ledger/numscript_runtime.go +++ b/internal/controller/ledger/numscript_runtime.go @@ -23,15 +23,15 @@ type NumscriptExecutionResult struct { //go:generate mockgen -write_source_comment=false -write_package_comment=false -source numscript_runtime.go -destination numscript_runtime_generated_test.go -package ledger . NumscriptRuntime type NumscriptRuntime interface { - Execute(context.Context, TX, map[string]string) (*NumscriptExecutionResult, error) + Execute(context.Context, Store, map[string]string) (*NumscriptExecutionResult, error) } type MachineNumscriptRuntimeAdapter struct { program program.Program } -func (d *MachineNumscriptRuntimeAdapter) Execute(ctx context.Context, tx TX, vars map[string]string) (*NumscriptExecutionResult, error) { - store := newVmStoreAdapter(tx) +func (d *MachineNumscriptRuntimeAdapter) Execute(ctx context.Context, store Store, vars map[string]string) (*NumscriptExecutionResult, error) { + storeAdapter := newVmStoreAdapter(store) machineInstance := vm.NewMachine(d.program) @@ -44,12 +44,12 @@ func (d *MachineNumscriptRuntimeAdapter) Execute(ctx context.Context, tx TX, var if err := machineInstance.SetVarsFromJSON(varsCopy); err != nil { return nil, fmt.Errorf("failed to set vars from JSON: %w", err) } - err := machineInstance.ResolveResources(ctx, store) + err := machineInstance.ResolveResources(ctx, storeAdapter) if err != nil { return nil, fmt.Errorf("failed to resolve resources: %w", err) } - if err := machineInstance.ResolveBalances(ctx, store); err != nil { + if err := machineInstance.ResolveBalances(ctx, storeAdapter); err != nil { return nil, fmt.Errorf("failed to resolve balances: %w", err) } @@ -99,8 +99,8 @@ func NewDefaultInterpreterMachineAdapter(parseResult numscript.ParseResult) *Def } } -func (d *DefaultInterpreterMachineAdapter) Execute(ctx context.Context, tx TX, vars map[string]string) (*NumscriptExecutionResult, error) { - execResult, err := d.parseResult.Run(ctx, vars, newNumscriptRewriteAdapter(tx)) +func (d *DefaultInterpreterMachineAdapter) Execute(ctx context.Context, store Store, vars map[string]string) (*NumscriptExecutionResult, error) { + execResult, err := d.parseResult.Run(ctx, vars, newNumscriptRewriteAdapter(store)) if err != nil { return nil, ErrRuntime{ Source: d.parseResult.GetSource(), diff --git a/internal/controller/ledger/numscript_runtime_generated_test.go b/internal/controller/ledger/numscript_runtime_generated_test.go index a116d7d89..254a78556 100644 --- a/internal/controller/ledger/numscript_runtime_generated_test.go +++ b/internal/controller/ledger/numscript_runtime_generated_test.go @@ -36,7 +36,7 @@ func (m *MockNumscriptRuntime) EXPECT() *MockNumscriptRuntimeMockRecorder { } // Execute mocks base method. -func (m *MockNumscriptRuntime) Execute(arg0 context.Context, arg1 TX, arg2 map[string]string) (*NumscriptExecutionResult, error) { +func (m *MockNumscriptRuntime) Execute(arg0 context.Context, arg1 Store, arg2 map[string]string) (*NumscriptExecutionResult, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Execute", arg0, arg1, arg2) ret0, _ := ret[0].(*NumscriptExecutionResult) diff --git a/internal/controller/ledger/store.go b/internal/controller/ledger/store.go index 7136b9344..dc10e1ac3 100644 --- a/internal/controller/ledger/store.go +++ b/internal/controller/ledger/store.go @@ -27,9 +27,12 @@ type Balance struct { type BalanceQuery = vm.BalanceQuery type Balances = vm.Balances -//go:generate mockgen -write_source_comment=false -write_package_comment=false -source store.go -destination store_generated_test.go -package ledger . TX -type TX interface { - GetAccount(ctx context.Context, query GetAccountQuery) (*ledger.Account, error) +//go:generate mockgen -write_source_comment=false -write_package_comment=false -source store.go -destination store_generated_test.go -package ledger . Store +type Store interface { + BeginTX(ctx context.Context, options *sql.TxOptions) error + Commit() error + Rollback() error + // GetBalances must returns balance and lock account until the end of the TX GetBalances(ctx context.Context, query BalanceQuery) (Balances, error) CommitTransaction(ctx context.Context, transaction *ledger.Transaction) error @@ -48,11 +51,7 @@ type TX interface { InsertLog(ctx context.Context, log *ledger.Log) error LockLedger(ctx context.Context) error - ListLogs(ctx context.Context, q GetLogsQuery) (*bunpaginate.Cursor[ledger.Log], error) -} -type Store interface { - WithTX(context.Context, *sql.TxOptions, func(TX) (bool, error)) error GetDB() bun.IDB ListLogs(ctx context.Context, q GetLogsQuery) (*bunpaginate.Cursor[ledger.Log], error) ReadLogWithIdempotencyKey(ctx context.Context, ik string) (*ledger.Log, error) @@ -272,11 +271,11 @@ func NewListLogsQuery(options PaginatedQueryOptions[any]) GetLogsQuery { } type vmStoreAdapter struct { - TX + Store } func (v *vmStoreAdapter) GetAccount(ctx context.Context, address string) (*ledger.Account, error) { - account, err := v.TX.GetAccount(ctx, NewGetAccountQuery(address)) + account, err := v.Store.GetAccount(ctx, NewGetAccountQuery(address)) if err != nil { return nil, err } @@ -285,9 +284,9 @@ func (v *vmStoreAdapter) GetAccount(ctx context.Context, address string) (*ledge var _ vm.Store = (*vmStoreAdapter)(nil) -func newVmStoreAdapter(tx TX) *vmStoreAdapter { +func newVmStoreAdapter(tx Store) *vmStoreAdapter { return &vmStoreAdapter{ - TX: tx, + Store: tx, } } @@ -303,21 +302,22 @@ func NewListLedgersQuery(pageSize uint64) ListLedgersQuery { var _ numscript.Store = (*numscriptRewriteAdapter)(nil) -func newNumscriptRewriteAdapter(tx TX) *numscriptRewriteAdapter { +func newNumscriptRewriteAdapter(store Store) *numscriptRewriteAdapter { return &numscriptRewriteAdapter{ - TX: tx, + Store: store, } } type numscriptRewriteAdapter struct { - TX TX + Store Store } func (s *numscriptRewriteAdapter) GetBalances(ctx context.Context, q numscript.BalanceQuery) (numscript.Balances, error) { - vmBalances, err := s.TX.GetBalances(ctx, BalanceQuery(q)) + vmBalances, err := s.Store.GetBalances(ctx, BalanceQuery(q)) if err != nil { return nil, err } + return numscript.Balances(vmBalances), nil } @@ -326,7 +326,7 @@ func (s *numscriptRewriteAdapter) GetAccountsMetadata(ctx context.Context, q num // we ignore the needed metadata values and just return all of them for address := range q { - v, err := s.TX.GetAccount(ctx, GetAccountQuery{ + v, err := s.Store.GetAccount(ctx, GetAccountQuery{ Addr: address, }) if err != nil { diff --git a/internal/controller/ledger/store_generated_test.go b/internal/controller/ledger/store_generated_test.go index 0d5200f3c..a6d3b4dca 100644 --- a/internal/controller/ledger/store_generated_test.go +++ b/internal/controller/ledger/store_generated_test.go @@ -2,7 +2,7 @@ // // Generated by this command: // -// mockgen -write_source_comment=false -write_package_comment=false -source store.go -destination store_generated_test.go -package ledger . TX +// mockgen -write_source_comment=false -write_package_comment=false -source store.go -destination store_generated_test.go -package ledger . Store package ledger import ( @@ -19,258 +19,129 @@ import ( gomock "go.uber.org/mock/gomock" ) -// MockTX is a mock of TX interface. -type MockTX struct { +// MockStore is a mock of Store interface. +type MockStore struct { ctrl *gomock.Controller - recorder *MockTXMockRecorder + recorder *MockStoreMockRecorder } -// MockTXMockRecorder is the mock recorder for MockTX. -type MockTXMockRecorder struct { - mock *MockTX +// MockStoreMockRecorder is the mock recorder for MockStore. +type MockStoreMockRecorder struct { + mock *MockStore } -// NewMockTX creates a new mock instance. -func NewMockTX(ctrl *gomock.Controller) *MockTX { - mock := &MockTX{ctrl: ctrl} - mock.recorder = &MockTXMockRecorder{mock} +// NewMockStore creates a new mock instance. +func NewMockStore(ctrl *gomock.Controller) *MockStore { + mock := &MockStore{ctrl: ctrl} + mock.recorder = &MockStoreMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockTX) EXPECT() *MockTXMockRecorder { +func (m *MockStore) EXPECT() *MockStoreMockRecorder { return m.recorder } -// CommitTransaction mocks base method. -func (m *MockTX) CommitTransaction(ctx context.Context, transaction *ledger.Transaction) error { +// BeginTX mocks base method. +func (m *MockStore) BeginTX(ctx context.Context, options *sql.TxOptions) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CommitTransaction", ctx, transaction) + ret := m.ctrl.Call(m, "BeginTX", ctx, options) ret0, _ := ret[0].(error) return ret0 } -// CommitTransaction indicates an expected call of CommitTransaction. -func (mr *MockTXMockRecorder) CommitTransaction(ctx, transaction any) *gomock.Call { +// BeginTX indicates an expected call of BeginTX. +func (mr *MockStoreMockRecorder) BeginTX(ctx, options any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CommitTransaction", reflect.TypeOf((*MockTX)(nil).CommitTransaction), ctx, transaction) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BeginTX", reflect.TypeOf((*MockStore)(nil).BeginTX), ctx, options) } -// DeleteAccountMetadata mocks base method. -func (m *MockTX) DeleteAccountMetadata(ctx context.Context, address, key string) error { +// Commit mocks base method. +func (m *MockStore) Commit() error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteAccountMetadata", ctx, address, key) + ret := m.ctrl.Call(m, "Commit") ret0, _ := ret[0].(error) return ret0 } -// DeleteAccountMetadata indicates an expected call of DeleteAccountMetadata. -func (mr *MockTXMockRecorder) DeleteAccountMetadata(ctx, address, key any) *gomock.Call { +// Commit indicates an expected call of Commit. +func (mr *MockStoreMockRecorder) Commit() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAccountMetadata", reflect.TypeOf((*MockTX)(nil).DeleteAccountMetadata), ctx, address, key) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Commit", reflect.TypeOf((*MockStore)(nil).Commit)) } -// DeleteTransactionMetadata mocks base method. -func (m *MockTX) DeleteTransactionMetadata(ctx context.Context, transactionID int, key string) (*ledger.Transaction, bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteTransactionMetadata", ctx, transactionID, key) - ret0, _ := ret[0].(*ledger.Transaction) - ret1, _ := ret[1].(bool) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// DeleteTransactionMetadata indicates an expected call of DeleteTransactionMetadata. -func (mr *MockTXMockRecorder) DeleteTransactionMetadata(ctx, transactionID, key any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTransactionMetadata", reflect.TypeOf((*MockTX)(nil).DeleteTransactionMetadata), ctx, transactionID, key) -} - -// GetAccount mocks base method. -func (m *MockTX) GetAccount(ctx context.Context, query GetAccountQuery) (*ledger.Account, error) { +// CommitTransaction mocks base method. +func (m *MockStore) CommitTransaction(ctx context.Context, transaction *ledger.Transaction) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAccount", ctx, query) - ret0, _ := ret[0].(*ledger.Account) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret := m.ctrl.Call(m, "CommitTransaction", ctx, transaction) + ret0, _ := ret[0].(error) + return ret0 } -// GetAccount indicates an expected call of GetAccount. -func (mr *MockTXMockRecorder) GetAccount(ctx, query any) *gomock.Call { +// CommitTransaction indicates an expected call of CommitTransaction. +func (mr *MockStoreMockRecorder) CommitTransaction(ctx, transaction any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccount", reflect.TypeOf((*MockTX)(nil).GetAccount), ctx, query) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CommitTransaction", reflect.TypeOf((*MockStore)(nil).CommitTransaction), ctx, transaction) } -// GetBalances mocks base method. -func (m *MockTX) GetBalances(ctx context.Context, query BalanceQuery) (Balances, error) { +// CountAccounts mocks base method. +func (m *MockStore) CountAccounts(ctx context.Context, a ListAccountsQuery) (int, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBalances", ctx, query) - ret0, _ := ret[0].(Balances) + ret := m.ctrl.Call(m, "CountAccounts", ctx, a) + ret0, _ := ret[0].(int) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetBalances indicates an expected call of GetBalances. -func (mr *MockTXMockRecorder) GetBalances(ctx, query any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBalances", reflect.TypeOf((*MockTX)(nil).GetBalances), ctx, query) -} - -// InsertLog mocks base method. -func (m *MockTX) InsertLog(ctx context.Context, log *ledger.Log) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InsertLog", ctx, log) - ret0, _ := ret[0].(error) - return ret0 -} - -// InsertLog indicates an expected call of InsertLog. -func (mr *MockTXMockRecorder) InsertLog(ctx, log any) *gomock.Call { +// CountAccounts indicates an expected call of CountAccounts. +func (mr *MockStoreMockRecorder) CountAccounts(ctx, a any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertLog", reflect.TypeOf((*MockTX)(nil).InsertLog), ctx, log) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountAccounts", reflect.TypeOf((*MockStore)(nil).CountAccounts), ctx, a) } -// ListLogs mocks base method. -func (m *MockTX) ListLogs(ctx context.Context, q GetLogsQuery) (*bunpaginate.Cursor[ledger.Log], error) { +// CountTransactions mocks base method. +func (m *MockStore) CountTransactions(ctx context.Context, q ListTransactionsQuery) (int, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListLogs", ctx, q) - ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Log]) + ret := m.ctrl.Call(m, "CountTransactions", ctx, q) + ret0, _ := ret[0].(int) ret1, _ := ret[1].(error) return ret0, ret1 } -// ListLogs indicates an expected call of ListLogs. -func (mr *MockTXMockRecorder) ListLogs(ctx, q any) *gomock.Call { +// CountTransactions indicates an expected call of CountTransactions. +func (mr *MockStoreMockRecorder) CountTransactions(ctx, q any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLogs", reflect.TypeOf((*MockTX)(nil).ListLogs), ctx, q) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountTransactions", reflect.TypeOf((*MockStore)(nil).CountTransactions), ctx, q) } -// LockLedger mocks base method. -func (m *MockTX) LockLedger(ctx context.Context) error { +// DeleteAccountMetadata mocks base method. +func (m *MockStore) DeleteAccountMetadata(ctx context.Context, address, key string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "LockLedger", ctx) + ret := m.ctrl.Call(m, "DeleteAccountMetadata", ctx, address, key) ret0, _ := ret[0].(error) return ret0 } -// LockLedger indicates an expected call of LockLedger. -func (mr *MockTXMockRecorder) LockLedger(ctx any) *gomock.Call { +// DeleteAccountMetadata indicates an expected call of DeleteAccountMetadata. +func (mr *MockStoreMockRecorder) DeleteAccountMetadata(ctx, address, key any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LockLedger", reflect.TypeOf((*MockTX)(nil).LockLedger), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAccountMetadata", reflect.TypeOf((*MockStore)(nil).DeleteAccountMetadata), ctx, address, key) } -// RevertTransaction mocks base method. -func (m *MockTX) RevertTransaction(ctx context.Context, id int, at time.Time) (*ledger.Transaction, bool, error) { +// DeleteTransactionMetadata mocks base method. +func (m *MockStore) DeleteTransactionMetadata(ctx context.Context, transactionID int, key string) (*ledger.Transaction, bool, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RevertTransaction", ctx, id, at) - ret0, _ := ret[0].(*ledger.Transaction) - ret1, _ := ret[1].(bool) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// RevertTransaction indicates an expected call of RevertTransaction. -func (mr *MockTXMockRecorder) RevertTransaction(ctx, id, at any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevertTransaction", reflect.TypeOf((*MockTX)(nil).RevertTransaction), ctx, id, at) -} - -// UpdateAccountsMetadata mocks base method. -func (m_2 *MockTX) UpdateAccountsMetadata(ctx context.Context, m map[string]metadata.Metadata) error { - m_2.ctrl.T.Helper() - ret := m_2.ctrl.Call(m_2, "UpdateAccountsMetadata", ctx, m) - ret0, _ := ret[0].(error) - return ret0 -} - -// UpdateAccountsMetadata indicates an expected call of UpdateAccountsMetadata. -func (mr *MockTXMockRecorder) UpdateAccountsMetadata(ctx, m any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAccountsMetadata", reflect.TypeOf((*MockTX)(nil).UpdateAccountsMetadata), ctx, m) -} - -// UpdateTransactionMetadata mocks base method. -func (m_2 *MockTX) UpdateTransactionMetadata(ctx context.Context, transactionID int, m metadata.Metadata) (*ledger.Transaction, bool, error) { - m_2.ctrl.T.Helper() - ret := m_2.ctrl.Call(m_2, "UpdateTransactionMetadata", ctx, transactionID, m) + ret := m.ctrl.Call(m, "DeleteTransactionMetadata", ctx, transactionID, key) ret0, _ := ret[0].(*ledger.Transaction) ret1, _ := ret[1].(bool) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } -// UpdateTransactionMetadata indicates an expected call of UpdateTransactionMetadata. -func (mr *MockTXMockRecorder) UpdateTransactionMetadata(ctx, transactionID, m any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTransactionMetadata", reflect.TypeOf((*MockTX)(nil).UpdateTransactionMetadata), ctx, transactionID, m) -} - -// UpsertAccount mocks base method. -func (m *MockTX) UpsertAccount(ctx context.Context, account *ledger.Account) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpsertAccount", ctx, account) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// UpsertAccount indicates an expected call of UpsertAccount. -func (mr *MockTXMockRecorder) UpsertAccount(ctx, account any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertAccount", reflect.TypeOf((*MockTX)(nil).UpsertAccount), ctx, account) -} - -// MockStore is a mock of Store interface. -type MockStore struct { - ctrl *gomock.Controller - recorder *MockStoreMockRecorder -} - -// MockStoreMockRecorder is the mock recorder for MockStore. -type MockStoreMockRecorder struct { - mock *MockStore -} - -// NewMockStore creates a new mock instance. -func NewMockStore(ctrl *gomock.Controller) *MockStore { - mock := &MockStore{ctrl: ctrl} - mock.recorder = &MockStoreMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockStore) EXPECT() *MockStoreMockRecorder { - return m.recorder -} - -// CountAccounts mocks base method. -func (m *MockStore) CountAccounts(ctx context.Context, a ListAccountsQuery) (int, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CountAccounts", ctx, a) - ret0, _ := ret[0].(int) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CountAccounts indicates an expected call of CountAccounts. -func (mr *MockStoreMockRecorder) CountAccounts(ctx, a any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountAccounts", reflect.TypeOf((*MockStore)(nil).CountAccounts), ctx, a) -} - -// CountTransactions mocks base method. -func (m *MockStore) CountTransactions(ctx context.Context, q ListTransactionsQuery) (int, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CountTransactions", ctx, q) - ret0, _ := ret[0].(int) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CountTransactions indicates an expected call of CountTransactions. -func (mr *MockStoreMockRecorder) CountTransactions(ctx, q any) *gomock.Call { +// DeleteTransactionMetadata indicates an expected call of DeleteTransactionMetadata. +func (mr *MockStoreMockRecorder) DeleteTransactionMetadata(ctx, transactionID, key any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountTransactions", reflect.TypeOf((*MockStore)(nil).CountTransactions), ctx, q) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTransactionMetadata", reflect.TypeOf((*MockStore)(nil).DeleteTransactionMetadata), ctx, transactionID, key) } // GetAccount mocks base method. @@ -303,6 +174,21 @@ func (mr *MockStoreMockRecorder) GetAggregatedBalances(ctx, q any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAggregatedBalances", reflect.TypeOf((*MockStore)(nil).GetAggregatedBalances), ctx, q) } +// GetBalances mocks base method. +func (m *MockStore) GetBalances(ctx context.Context, query BalanceQuery) (Balances, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBalances", ctx, query) + ret0, _ := ret[0].(Balances) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBalances indicates an expected call of GetBalances. +func (mr *MockStoreMockRecorder) GetBalances(ctx, query any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBalances", reflect.TypeOf((*MockStore)(nil).GetBalances), ctx, query) +} + // GetDB mocks base method. func (m *MockStore) GetDB() bun.IDB { m.ctrl.T.Helper() @@ -362,6 +248,20 @@ func (mr *MockStoreMockRecorder) GetVolumesWithBalances(ctx, q any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVolumesWithBalances", reflect.TypeOf((*MockStore)(nil).GetVolumesWithBalances), ctx, q) } +// InsertLog mocks base method. +func (m *MockStore) InsertLog(ctx context.Context, log *ledger.Log) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InsertLog", ctx, log) + ret0, _ := ret[0].(error) + return ret0 +} + +// InsertLog indicates an expected call of InsertLog. +func (mr *MockStoreMockRecorder) InsertLog(ctx, log any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertLog", reflect.TypeOf((*MockStore)(nil).InsertLog), ctx, log) +} + // IsUpToDate mocks base method. func (m *MockStore) IsUpToDate(ctx context.Context) (bool, error) { m.ctrl.T.Helper() @@ -422,6 +322,20 @@ func (mr *MockStoreMockRecorder) ListTransactions(ctx, q any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTransactions", reflect.TypeOf((*MockStore)(nil).ListTransactions), ctx, q) } +// LockLedger mocks base method. +func (m *MockStore) LockLedger(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LockLedger", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// LockLedger indicates an expected call of LockLedger. +func (mr *MockStoreMockRecorder) LockLedger(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LockLedger", reflect.TypeOf((*MockStore)(nil).LockLedger), ctx) +} + // ReadLogWithIdempotencyKey mocks base method. func (m *MockStore) ReadLogWithIdempotencyKey(ctx context.Context, ik string) (*ledger.Log, error) { m.ctrl.T.Helper() @@ -437,16 +351,77 @@ func (mr *MockStoreMockRecorder) ReadLogWithIdempotencyKey(ctx, ik any) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadLogWithIdempotencyKey", reflect.TypeOf((*MockStore)(nil).ReadLogWithIdempotencyKey), ctx, ik) } -// WithTX mocks base method. -func (m *MockStore) WithTX(arg0 context.Context, arg1 *sql.TxOptions, arg2 func(TX) (bool, error)) error { +// RevertTransaction mocks base method. +func (m *MockStore) RevertTransaction(ctx context.Context, id int, at time.Time) (*ledger.Transaction, bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RevertTransaction", ctx, id, at) + ret0, _ := ret[0].(*ledger.Transaction) + ret1, _ := ret[1].(bool) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// RevertTransaction indicates an expected call of RevertTransaction. +func (mr *MockStoreMockRecorder) RevertTransaction(ctx, id, at any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevertTransaction", reflect.TypeOf((*MockStore)(nil).RevertTransaction), ctx, id, at) +} + +// Rollback mocks base method. +func (m *MockStore) Rollback() error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "WithTX", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "Rollback") + ret0, _ := ret[0].(error) + return ret0 +} + +// Rollback indicates an expected call of Rollback. +func (mr *MockStoreMockRecorder) Rollback() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rollback", reflect.TypeOf((*MockStore)(nil).Rollback)) +} + +// UpdateAccountsMetadata mocks base method. +func (m_2 *MockStore) UpdateAccountsMetadata(ctx context.Context, m map[string]metadata.Metadata) error { + m_2.ctrl.T.Helper() + ret := m_2.ctrl.Call(m_2, "UpdateAccountsMetadata", ctx, m) ret0, _ := ret[0].(error) return ret0 } -// WithTX indicates an expected call of WithTX. -func (mr *MockStoreMockRecorder) WithTX(arg0, arg1, arg2 any) *gomock.Call { +// UpdateAccountsMetadata indicates an expected call of UpdateAccountsMetadata. +func (mr *MockStoreMockRecorder) UpdateAccountsMetadata(ctx, m any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAccountsMetadata", reflect.TypeOf((*MockStore)(nil).UpdateAccountsMetadata), ctx, m) +} + +// UpdateTransactionMetadata mocks base method. +func (m_2 *MockStore) UpdateTransactionMetadata(ctx context.Context, transactionID int, m metadata.Metadata) (*ledger.Transaction, bool, error) { + m_2.ctrl.T.Helper() + ret := m_2.ctrl.Call(m_2, "UpdateTransactionMetadata", ctx, transactionID, m) + ret0, _ := ret[0].(*ledger.Transaction) + ret1, _ := ret[1].(bool) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// UpdateTransactionMetadata indicates an expected call of UpdateTransactionMetadata. +func (mr *MockStoreMockRecorder) UpdateTransactionMetadata(ctx, transactionID, m any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTransactionMetadata", reflect.TypeOf((*MockStore)(nil).UpdateTransactionMetadata), ctx, transactionID, m) +} + +// UpsertAccount mocks base method. +func (m *MockStore) UpsertAccount(ctx context.Context, account *ledger.Account) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpsertAccount", ctx, account) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpsertAccount indicates an expected call of UpsertAccount. +func (mr *MockStoreMockRecorder) UpsertAccount(ctx, account any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithTX", reflect.TypeOf((*MockStore)(nil).WithTX), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertAccount", reflect.TypeOf((*MockStore)(nil).UpsertAccount), ctx, account) } diff --git a/internal/storage/ledger/legacy/adapters.go b/internal/storage/ledger/legacy/adapters.go index 19bb9723a..b7c225eec 100644 --- a/internal/storage/ledger/legacy/adapters.go +++ b/internal/storage/ledger/legacy/adapters.go @@ -13,67 +13,53 @@ import ( "github.com/uptrace/bun" ) -type TX struct { +type DefaultStoreAdapter struct { newStore *ledgerstore.Store legacyStore *Store - sqlTX bun.Tx -} - -func (tx TX) GetAccount(ctx context.Context, query ledgercontroller.GetAccountQuery) (*ledger.Account, error) { - return tx.legacyStore.GetAccountWithVolumes(ctx, query) } -func (tx TX) GetBalances(ctx context.Context, query ledgercontroller.BalanceQuery) (ledgercontroller.Balances, error) { - return tx.newStore.GetBalances(ctx, query) -} - -func (tx TX) CommitTransaction(ctx context.Context, transaction *ledger.Transaction) error { - return tx.newStore.CommitTransaction(ctx, transaction) +func (d *DefaultStoreAdapter) GetDB() bun.IDB { + return d.newStore.GetDB() } -func (tx TX) RevertTransaction(ctx context.Context, id int, at time.Time) (*ledger.Transaction, bool, error) { - return tx.newStore.RevertTransaction(ctx, id, at) +func (d *DefaultStoreAdapter) GetBalances(ctx context.Context, query ledgercontroller.BalanceQuery) (ledgercontroller.Balances, error) { + return d.newStore.GetBalances(ctx, query) } -func (tx TX) UpdateTransactionMetadata(ctx context.Context, transactionID int, m metadata.Metadata) (*ledger.Transaction, bool, error) { - return tx.newStore.UpdateTransactionMetadata(ctx, transactionID, m) +func (d *DefaultStoreAdapter) CommitTransaction(ctx context.Context, transaction *ledger.Transaction) error { + return d.newStore.CommitTransaction(ctx, transaction) } -func (tx TX) DeleteTransactionMetadata(ctx context.Context, transactionID int, key string) (*ledger.Transaction, bool, error) { - return tx.newStore.DeleteTransactionMetadata(ctx, transactionID, key) +func (d *DefaultStoreAdapter) RevertTransaction(ctx context.Context, id int, at time.Time) (*ledger.Transaction, bool, error) { + return d.newStore.RevertTransaction(ctx, id, at) } -func (tx TX) UpdateAccountsMetadata(ctx context.Context, m map[string]metadata.Metadata) error { - return tx.newStore.UpdateAccountsMetadata(ctx, m) +func (d *DefaultStoreAdapter) UpdateTransactionMetadata(ctx context.Context, transactionID int, m metadata.Metadata) (*ledger.Transaction, bool, error) { + return d.newStore.UpdateTransactionMetadata(ctx, transactionID, m) } -func (tx TX) UpsertAccount(ctx context.Context, account *ledger.Account) (bool, error) { - return tx.newStore.UpsertAccount(ctx, account) +func (d *DefaultStoreAdapter) DeleteTransactionMetadata(ctx context.Context, transactionID int, key string) (*ledger.Transaction, bool, error) { + return d.newStore.DeleteTransactionMetadata(ctx, transactionID, key) } -func (tx TX) DeleteAccountMetadata(ctx context.Context, address, key string) error { - return tx.newStore.DeleteAccountMetadata(ctx, address, key) +func (d *DefaultStoreAdapter) UpdateAccountsMetadata(ctx context.Context, m map[string]metadata.Metadata) error { + return d.newStore.UpdateAccountsMetadata(ctx, m) } -func (tx TX) InsertLog(ctx context.Context, log *ledger.Log) error { - return tx.newStore.InsertLog(ctx, log) +func (d *DefaultStoreAdapter) UpsertAccount(ctx context.Context, account *ledger.Account) (bool, error) { + return d.newStore.UpsertAccount(ctx, account) } -func (tx TX) LockLedger(ctx context.Context) error { - return tx.newStore.LockLedger(ctx) +func (d *DefaultStoreAdapter) DeleteAccountMetadata(ctx context.Context, address, key string) error { + return d.newStore.DeleteAccountMetadata(ctx, address, key) } -func (tx TX) ListLogs(ctx context.Context, q ledgercontroller.GetLogsQuery) (*bunpaginate.Cursor[ledger.Log], error) { - return tx.legacyStore.GetLogs(ctx, q) +func (d *DefaultStoreAdapter) InsertLog(ctx context.Context, log *ledger.Log) error { + return d.newStore.InsertLog(ctx, log) } -type DefaultStoreAdapter struct { - newStore *ledgerstore.Store - legacyStore *Store -} - -func (d *DefaultStoreAdapter) GetDB() bun.IDB { - return d.newStore.GetDB() +func (d *DefaultStoreAdapter) LockLedger(ctx context.Context) error { + return d.newStore.LockLedger(ctx) } func (d *DefaultStoreAdapter) ListLogs(ctx context.Context, q ledgercontroller.GetLogsQuery) (*bunpaginate.Cursor[ledger.Log], error) { @@ -124,32 +110,25 @@ func (d *DefaultStoreAdapter) GetMigrationsInfo(ctx context.Context) ([]migratio return d.newStore.GetMigrationsInfo(ctx) } -func (d *DefaultStoreAdapter) WithTX(ctx context.Context, opts *sql.TxOptions, f func(ledgercontroller.TX) (bool, error)) error { - if opts == nil { - opts = &sql.TxOptions{} - } - - tx, err := d.newStore.GetDB().BeginTx(ctx, opts) +func (d *DefaultStoreAdapter) BeginTX(ctx context.Context, opts *sql.TxOptions) error { + err := d.newStore.BeginTX(ctx, opts) if err != nil { return err } - defer func() { - _ = tx.Rollback() - }() - - if commit, err := f(&TX{ - newStore: d.newStore.WithDB(tx), - legacyStore: d.legacyStore.WithDB(tx), - sqlTX: tx, - }); err != nil { - return err - } else if commit { - return tx.Commit() - } + + d.legacyStore = d.legacyStore.WithDB(d.newStore.GetDB()) return nil } +func (d *DefaultStoreAdapter) Commit() error { + return d.newStore.Commit() +} + +func (d *DefaultStoreAdapter) Rollback() error { + return d.newStore.Rollback() +} + func NewDefaultStoreAdapter(store *ledgerstore.Store) *DefaultStoreAdapter { return &DefaultStoreAdapter{ newStore: store, diff --git a/internal/storage/ledger/store.go b/internal/storage/ledger/store.go index 3d332f797..89e79120c 100644 --- a/internal/storage/ledger/store.go +++ b/internal/storage/ledger/store.go @@ -2,6 +2,7 @@ package ledger import ( "context" + "database/sql" "fmt" "github.com/formancehq/go-libs/v2/migrations" "github.com/formancehq/go-libs/v2/platform/postgres" @@ -18,9 +19,10 @@ import ( ) type Store struct { - db bun.IDB - bucket bucket.Bucket - ledger ledger.Ledger + dbStack []bun.IDB + db bun.IDB + bucket bucket.Bucket + ledger ledger.Ledger tracer trace.Tracer meter metric.Meter @@ -48,6 +50,41 @@ type Store struct { listTransactionsHistogram metric.Int64Histogram } +func (s *Store) BeginTX(ctx context.Context, options *sql.TxOptions) error { + tx, err := s.db.BeginTx(ctx, options) + if err != nil { + return postgres.ResolveError(err) + } + s.dbStack = append(s.dbStack, s.db) + s.db = tx + + return nil +} + +func (s *Store) Commit() error { + switch db := s.db.(type) { + case bun.Tx: + err := db.Commit() + s.db = s.dbStack[len(s.dbStack)-1] + s.dbStack = s.dbStack[:len(s.dbStack)-1] + return err + default: + return errors.New("not in a transaction") + } +} + +func (s *Store) Rollback() error { + switch db := s.db.(type) { + case bun.Tx: + err := db.Rollback() + s.db = s.dbStack[len(s.dbStack)-1] + s.dbStack = s.dbStack[:len(s.dbStack)-1] + return err + default: + return errors.New("not in a transaction") + } +} + func (s *Store) GetLedger() ledger.Ledger { return s.ledger } @@ -60,12 +97,6 @@ func (s *Store) GetPrefixedRelationName(v string) string { return fmt.Sprintf(`"%s".%s`, s.ledger.Bucket, v) } -func (s *Store) WithDB(db bun.IDB) *Store { - ret := *s - ret.db = db - return &ret -} - func (s *Store) validateAddressFilter(operator string, value any) error { if operator != "$match" { return errors.New("'address' column can only be used with $match") @@ -99,86 +130,107 @@ func New(db bun.IDB, bucket bucket.Bucket, ledger ledger.Ledger, opts ...Option) if err != nil { panic(err) } + ret.checkBucketSchemaHistogram, err = ret.meter.Int64Histogram("store.checkBucketSchema") if err != nil { panic(err) } + ret.checkLedgerSchemaHistogram, err = ret.meter.Int64Histogram("store.checkLedgerSchema") if err != nil { panic(err) } + ret.getAccountHistogram, err = ret.meter.Int64Histogram("store.getAccount") if err != nil { panic(err) } + ret.countAccountsHistogram, err = ret.meter.Int64Histogram("store.countAccounts") if err != nil { panic(err) } + ret.updateAccountsMetadataHistogram, err = ret.meter.Int64Histogram("store.updateAccountsMetadata") if err != nil { panic(err) } + ret.deleteAccountMetadataHistogram, err = ret.meter.Int64Histogram("store.deleteAccountMetadata") if err != nil { panic(err) } + ret.upsertAccountHistogram, err = ret.meter.Int64Histogram("store.upsertAccount") if err != nil { panic(err) } + ret.getBalancesHistogram, err = ret.meter.Int64Histogram("store.getBalances") if err != nil { panic(err) } + ret.insertLogHistogram, err = ret.meter.Int64Histogram("store.insertLog") if err != nil { panic(err) } + ret.listLogsHistogram, err = ret.meter.Int64Histogram("store.listLogs") if err != nil { panic(err) } + ret.readLogWithIdempotencyKeyHistogram, err = ret.meter.Int64Histogram("store.readLogWithIdempotencyKey") if err != nil { panic(err) } + ret.insertMovesHistogram, err = ret.meter.Int64Histogram("store.insertMoves") if err != nil { panic(err) } + ret.countTransactionsHistogram, err = ret.meter.Int64Histogram("store.countTransactions") if err != nil { panic(err) } + ret.getTransactionHistogram, err = ret.meter.Int64Histogram("store.getTransaction") if err != nil { panic(err) } + ret.insertTransactionHistogram, err = ret.meter.Int64Histogram("store.insertTransaction") if err != nil { panic(err) } + ret.revertTransactionHistogram, err = ret.meter.Int64Histogram("store.revertTransaction") if err != nil { panic(err) } + ret.updateTransactionMetadataHistogram, err = ret.meter.Int64Histogram("store.updateTransactionMetadata") if err != nil { panic(err) } + ret.deleteTransactionMetadataHistogram, err = ret.meter.Int64Histogram("store.deleteTransactionMetadata") if err != nil { panic(err) } + ret.updateBalancesHistogram, err = ret.meter.Int64Histogram("store.updateBalances") if err != nil { panic(err) } + ret.getVolumesWithBalancesHistogram, err = ret.meter.Int64Histogram("store.getVolumesWithBalances") if err != nil { panic(err) } + ret.listTransactionsHistogram, err = ret.meter.Int64Histogram("store.listTransactions") if err != nil { panic(err) @@ -195,6 +247,13 @@ func (s *Store) GetMigrationsInfo(ctx context.Context) ([]migrations.Info, error return s.bucket.GetMigrationsInfo(ctx) } +func (s *Store) WithDB(db bun.IDB) *Store { + ret := *s + ret.db = db + + return &ret +} + type Option func(s *Store) func WithMeter(meter metric.Meter) Option { diff --git a/openapi.yaml b/openapi.yaml index 742570a37..9f8baf13b 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1448,6 +1448,18 @@ paths: schema: type: string example: ledger001 + - name: continueOnFailure + in: query + description: Continue on failure + schema: + type: boolean + example: true + - name: atomic + in: query + description: Make bulk atomic + schema: + type: boolean + example: true requestBody: content: application/json: diff --git a/openapi/v2.yaml b/openapi/v2.yaml index 71d17a9ac..13eb82791 100644 --- a/openapi/v2.yaml +++ b/openapi/v2.yaml @@ -279,6 +279,18 @@ paths: schema: type: string example: ledger001 + - name: continueOnFailure + in: query + description: Continue on failure + schema: + type: boolean + example: true + - name: atomic + in: query + description: Make bulk atomic + schema: + type: boolean + example: true requestBody: content: application/json: diff --git a/pkg/client/.speakeasy/gen.lock b/pkg/client/.speakeasy/gen.lock index c273af6cc..983ac76d8 100644 --- a/pkg/client/.speakeasy/gen.lock +++ b/pkg/client/.speakeasy/gen.lock @@ -1,12 +1,12 @@ lockVersion: 2.0.0 id: a9ac79e1-e429-4ee3-96c4-ec973f19bec3 management: - docChecksum: 25c171859e282a2487f6d9f9d3888d8a + docChecksum: 4a4a3929b808f3192cbb2f02351bc186 docVersion: v1 speakeasyVersion: 1.351.0 generationVersion: 2.384.1 - releaseVersion: 0.4.32 - configChecksum: 38c4107cc4da2c542bfe11bf874323aa + releaseVersion: 0.4.33 + configChecksum: 22f33d29f62599fa892d20fe5d13f4cc features: go: additionalDependencies: 0.1.0 diff --git a/pkg/client/.speakeasy/gen.yaml b/pkg/client/.speakeasy/gen.yaml index cb7842dbb..117836507 100644 --- a/pkg/client/.speakeasy/gen.yaml +++ b/pkg/client/.speakeasy/gen.yaml @@ -15,7 +15,7 @@ generation: auth: oAuth2ClientCredentialsEnabled: true go: - version: 0.4.32 + version: 0.4.33 additionalDependencies: {} allowUnknownFieldsInWeakUnions: false clientServerStatusCodesAsErrors: true diff --git a/pkg/client/docs/models/operations/v2createbulkrequest.md b/pkg/client/docs/models/operations/v2createbulkrequest.md index a3531bc22..078c42518 100644 --- a/pkg/client/docs/models/operations/v2createbulkrequest.md +++ b/pkg/client/docs/models/operations/v2createbulkrequest.md @@ -6,4 +6,6 @@ | Field | Type | Required | Description | Example | | ---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------- | | `Ledger` | *string* | :heavy_check_mark: | Name of the ledger. | ledger001 | +| `ContinueOnFailure` | **bool* | :heavy_minus_sign: | Continue on failure | true | +| `Atomic` | **bool* | :heavy_minus_sign: | Make bulk atomic | true | | `RequestBody` | [][components.V2BulkElement](../../models/components/v2bulkelement.md) | :heavy_minus_sign: | 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 04807c180..38dd2752e 100644 --- a/pkg/client/docs/sdks/v2/README.md +++ b/pkg/client/docs/sdks/v2/README.md @@ -396,6 +396,8 @@ func main() { ) request := operations.V2CreateBulkRequest{ Ledger: "ledger001", + ContinueOnFailure: client.Bool(true), + Atomic: client.Bool(true), RequestBody: []components.V2BulkElement{ components.CreateV2BulkElementV2BulkElementCreateTransaction( components.V2BulkElementCreateTransaction{ diff --git a/pkg/client/formance.go b/pkg/client/formance.go index 11fd71ddc..f6e207f06 100644 --- a/pkg/client/formance.go +++ b/pkg/client/formance.go @@ -143,9 +143,9 @@ func New(opts ...SDKOption) *Formance { sdkConfiguration: sdkConfiguration{ Language: "go", OpenAPIDocVersion: "v1", - SDKVersion: "0.4.32", + SDKVersion: "0.4.33", GenVersion: "2.384.1", - UserAgent: "speakeasy-sdk/go 0.4.32 2.384.1 v1 github.com/formancehq/ledger/pkg/client", + UserAgent: "speakeasy-sdk/go 0.4.33 2.384.1 v1 github.com/formancehq/ledger/pkg/client", Hooks: hooks.New(), }, } diff --git a/pkg/client/models/operations/v2createbulk.go b/pkg/client/models/operations/v2createbulk.go index 444d81c26..eb3819184 100644 --- a/pkg/client/models/operations/v2createbulk.go +++ b/pkg/client/models/operations/v2createbulk.go @@ -8,7 +8,11 @@ import ( type V2CreateBulkRequest struct { // Name of the ledger. - Ledger string `pathParam:"style=simple,explode=false,name=ledger"` + Ledger string `pathParam:"style=simple,explode=false,name=ledger"` + // Continue on failure + ContinueOnFailure *bool `queryParam:"style=form,explode=true,name=continueOnFailure"` + // Make bulk atomic + Atomic *bool `queryParam:"style=form,explode=true,name=atomic"` RequestBody []components.V2BulkElement `request:"mediaType=application/json"` } @@ -19,6 +23,20 @@ func (o *V2CreateBulkRequest) GetLedger() string { return o.Ledger } +func (o *V2CreateBulkRequest) GetContinueOnFailure() *bool { + if o == nil { + return nil + } + return o.ContinueOnFailure +} + +func (o *V2CreateBulkRequest) GetAtomic() *bool { + if o == nil { + return nil + } + return o.Atomic +} + func (o *V2CreateBulkRequest) GetRequestBody() []components.V2BulkElement { if o == nil { return nil diff --git a/pkg/client/v2.go b/pkg/client/v2.go index 3f4800466..4d978414e 100644 --- a/pkg/client/v2.go +++ b/pkg/client/v2.go @@ -1147,6 +1147,10 @@ func (s *V2) CreateBulk(ctx context.Context, request operations.V2CreateBulkRequ req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) req.Header.Set("Content-Type", reqContentType) + if err := utils.PopulateQueryParams(ctx, req, request, nil); err != nil { + return nil, fmt.Errorf("error populating query params: %w", err) + } + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { return nil, err } diff --git a/test/e2e/api_bulk_test.go b/test/e2e/api_bulk_test.go index b46cb6779..f55ac6aff 100644 --- a/test/e2e/api_bulk_test.go +++ b/test/e2e/api_bulk_test.go @@ -3,6 +3,7 @@ package test_suite import ( + "github.com/formancehq/go-libs/v2/pointer" "math/big" "time" @@ -159,9 +160,11 @@ var _ = Context("Ledger engine tests", func() { now = time.Now().Round(time.Microsecond).UTC() err error bulkResponse []components.V2BulkElementResult + atomic bool ) - BeforeEach(func() { + JustBeforeEach(func() { bulkResponse, err = CreateBulk(ctx, testServer.GetValue(), operations.V2CreateBulkRequest{ + Atomic: pointer.For(atomic), RequestBody: []components.V2BulkElement{ components.CreateV2BulkElementCreateTransaction(components.V2BulkElementCreateTransaction{ Data: &components.V2PostTransaction{ @@ -192,8 +195,11 @@ var _ = Context("Ledger engine tests", func() { }) Expect(err).To(Succeed()) }) - It("should respond with an error", func() { + shouldRespondWithAnError := func() { + GinkgoHelper() + var expectedErr string + // todo: must be fixed before switch to the new implementation if data.numscriptRewrite { expectedErr = "INTERPRETER_RUNTIME" } else { @@ -201,9 +207,33 @@ var _ = Context("Ledger engine tests", func() { } Expect(bulkResponse[1].Type).To(Equal(components.V2BulkElementResultType("ERROR"))) Expect(bulkResponse[1].V2BulkElementResultError.ErrorCode).To(Equal(expectedErr)) + } + It("should respond with an error", func() { + shouldRespondWithAnError() + + By("should have created the first item", func() { + txs, err := ListTransactions(ctx, testServer.GetValue(), operations.V2ListTransactionsRequest{ + Ledger: "default", + }) + Expect(err).To(Succeed()) + Expect(txs.Data).To(HaveLen(1)) + }) + }) + Context("with atomic", func() { + BeforeEach(func() { + atomic = true + }) + It("should not commit anything", func() { + shouldRespondWithAnError() + + txs, err := ListTransactions(ctx, testServer.GetValue(), operations.V2ListTransactionsRequest{ + Ledger: "default", + }) + Expect(err).To(Succeed()) + Expect(txs.Data).To(HaveLen(0)) + }) }) }) }) } - }) diff --git a/test/e2e/api_transactions_revert_test.go b/test/e2e/api_transactions_revert_test.go index 2f39c80aa..2c135f698 100644 --- a/test/e2e/api_transactions_revert_test.go +++ b/test/e2e/api_transactions_revert_test.go @@ -19,7 +19,7 @@ import ( ledgerevents "github.com/formancehq/ledger/pkg/events" ) -var _ = Context("Ledger accounts list API tests", func() { +var _ = Context("Ledger revert transactions API tests", func() { var ( db = UseTemplatedDatabase() ctx = logging.TestingContext() From 50a5eb730987d4ed53228f73dde54f80582ce2ca Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 21 Nov 2024 14:16:20 +0100 Subject: [PATCH 31/71] refacto: add bulker --- .../common/mocks_ledger_controller_test.go | 7 +- .../api/v1/controllers_transactions_create.go | 2 +- .../controllers_transactions_create_test.go | 5 +- .../api/v1/mocks_ledger_controller_test.go | 7 +- internal/api/v2/controllers_bulk.go | 384 +++--------------- internal/api/v2/controllers_bulk_test.go | 29 +- .../api/v2/controllers_transactions_create.go | 2 +- .../controllers_transactions_create_test.go | 35 +- .../api/v2/mocks_ledger_controller_test.go | 7 +- internal/controller/ledger/bulker.go | 331 +++++++++++++++ internal/controller/ledger/controller.go | 2 +- .../controller/ledger/controller_default.go | 70 ++-- .../ledger/controller_default_test.go | 25 +- .../ledger/controller_generated_test.go | 7 +- .../ledger/controller_with_cache.go | 14 + .../ledger/controller_with_events.go | 14 + ...ontroller_with_too_many_client_handling.go | 50 ++- .../ledger/controller_with_traces.go | 146 +++---- internal/controller/ledger/log_process.go | 49 ++- .../controller/ledger/log_process_test.go | 10 +- .../common => controller/ledger}/numscript.go | 9 +- internal/controller/ledger/store.go | 2 +- .../controller/ledger/store_generated_test.go | 7 +- internal/storage/ledger/legacy/adapters.go | 13 +- internal/storage/ledger/store.go | 31 +- pkg/generate/generator.go | 22 +- test/e2e/api_bulk_test.go | 2 +- tools/generator/go.mod | 30 +- tools/generator/go.sum | 6 - 29 files changed, 688 insertions(+), 630 deletions(-) create mode 100644 internal/controller/ledger/bulker.go rename internal/{api/common => controller/ledger}/numscript.go (93%) diff --git a/internal/api/common/mocks_ledger_controller_test.go b/internal/api/common/mocks_ledger_controller_test.go index 4e3c01be1..85e72e1b8 100644 --- a/internal/api/common/mocks_ledger_controller_test.go +++ b/internal/api/common/mocks_ledger_controller_test.go @@ -41,11 +41,12 @@ func (m *LedgerController) EXPECT() *LedgerControllerMockRecorder { } // BeginTX mocks base method. -func (m *LedgerController) BeginTX(ctx context.Context, options *sql.TxOptions) error { +func (m *LedgerController) BeginTX(ctx context.Context, options *sql.TxOptions) (ledger0.Controller, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "BeginTX", ctx, options) - ret0, _ := ret[0].(error) - return ret0 + ret0, _ := ret[0].(ledger0.Controller) + ret1, _ := ret[1].(error) + return ret0, ret1 } // BeginTX indicates an expected call of BeginTX. diff --git a/internal/api/v1/controllers_transactions_create.go b/internal/api/v1/controllers_transactions_create.go index e1acb5e32..2e6d89cce 100644 --- a/internal/api/v1/controllers_transactions_create.go +++ b/internal/api/v1/controllers_transactions_create.go @@ -83,7 +83,7 @@ func createTransaction(w http.ResponseWriter, r *http.Request) { Metadata: payload.Metadata, } - _, res, err := l.CreateTransaction(r.Context(), getCommandParameters(r, common.TxToScriptData(txData, false))) + _, res, err := l.CreateTransaction(r.Context(), getCommandParameters(r, ledgercontroller.TxToScriptData(txData, false))) if err != nil { switch { case errors.Is(err, &ledgercontroller.ErrInsufficientFunds{}): diff --git a/internal/api/v1/controllers_transactions_create_test.go b/internal/api/v1/controllers_transactions_create_test.go index f90ed338f..caea2047f 100644 --- a/internal/api/v1/controllers_transactions_create_test.go +++ b/internal/api/v1/controllers_transactions_create_test.go @@ -2,7 +2,6 @@ package v1 import ( "encoding/json" - "github.com/formancehq/ledger/internal/api/common" "math/big" "net/http" "net/http/httptest" @@ -154,7 +153,7 @@ func TestTransactionsCreate(t *testing.T) { ledger.NewPosting("world", "bank", "USD", big.NewInt(100)), }, }, - expectedRunScript: common.TxToScriptData(ledger.NewTransactionData().WithPostings( + expectedRunScript: ledgercontroller.TxToScriptData(ledger.NewTransactionData().WithPostings( ledger.NewPosting("world", "bank", "USD", big.NewInt(100)), ), false), }, @@ -169,7 +168,7 @@ func TestTransactionsCreate(t *testing.T) { }, }, expectedPreview: true, - expectedRunScript: common.TxToScriptData(ledger.NewTransactionData().WithPostings( + expectedRunScript: ledgercontroller.TxToScriptData(ledger.NewTransactionData().WithPostings( ledger.NewPosting("world", "bank", "USD", big.NewInt(100)), ), false), }, diff --git a/internal/api/v1/mocks_ledger_controller_test.go b/internal/api/v1/mocks_ledger_controller_test.go index adc36d2e3..e619609c9 100644 --- a/internal/api/v1/mocks_ledger_controller_test.go +++ b/internal/api/v1/mocks_ledger_controller_test.go @@ -41,11 +41,12 @@ func (m *LedgerController) EXPECT() *LedgerControllerMockRecorder { } // BeginTX mocks base method. -func (m *LedgerController) BeginTX(ctx context.Context, options *sql.TxOptions) error { +func (m *LedgerController) BeginTX(ctx context.Context, options *sql.TxOptions) (ledger0.Controller, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "BeginTX", ctx, options) - ret0, _ := ret[0].(error) - return ret0 + ret0, _ := ret[0].(ledger0.Controller) + ret1, _ := ret[1].(error) + return ret0, ret1 } // BeginTX indicates an expected call of BeginTX. diff --git a/internal/api/v2/controllers_bulk.go b/internal/api/v2/controllers_bulk.go index 8b190d462..b3a66151d 100644 --- a/internal/api/v2/controllers_bulk.go +++ b/internal/api/v2/controllers_bulk.go @@ -1,24 +1,20 @@ package v2 import ( - "context" "encoding/json" "fmt" "github.com/formancehq/go-libs/v2/pointer" - "github.com/formancehq/go-libs/v2/time" "net/http" "errors" "github.com/formancehq/go-libs/v2/api" - "github.com/formancehq/go-libs/v2/metadata" - ledger "github.com/formancehq/ledger/internal" "github.com/formancehq/ledger/internal/api/common" ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" ) func bulkHandler(bulkMaxSize int) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - b := Bulk{} + b := ledgercontroller.Bulk{} if err := json.NewDecoder(r.Body).Decode(&b); err != nil { api.BadRequest(w, ErrValidation, err) return @@ -31,9 +27,9 @@ func bulkHandler(bulkMaxSize int) http.HandlerFunc { w.Header().Set("Content-Type", "application/json") - ret, errorsInBulk, err := ProcessBulk( - r.Context(), - common.LedgerFromContext(r.Context()), + ledgerController := common.LedgerFromContext(r.Context()) + bulker := ledgercontroller.NewBulker(ledgerController) + results, err := bulker.Run(r.Context(), b, api.QueryParamBool(r, "continueOnFailure"), api.QueryParamBool(r, "atomic"), @@ -42,350 +38,62 @@ func bulkHandler(bulkMaxSize int) http.HandlerFunc { api.InternalServerError(w, r, err) return } - if errorsInBulk { + if results.HasErrors() { w.WriteHeader(http.StatusBadRequest) } - if err := json.NewEncoder(w).Encode(api.BaseResponse[[]Result]{ - Data: &ret, - }); err != nil { - panic(err) - } - } -} - -const ( - ActionCreateTransaction = "CREATE_TRANSACTION" - ActionAddMetadata = "ADD_METADATA" - ActionRevertTransaction = "REVERT_TRANSACTION" - ActionDeleteMetadata = "DELETE_METADATA" -) - -type Bulk []BulkElement - -type BulkElement struct { - Action string `json:"action"` - IdempotencyKey string `json:"ik"` - Data json.RawMessage `json:"data"` -} - -type Result struct { - ErrorCode string `json:"errorCode,omitempty"` - ErrorDescription string `json:"errorDescription,omitempty"` - Data any `json:"data,omitempty"` - ResponseType string `json:"responseType"` // Added for sdk generation (discriminator in oneOf) - LogID int `json:"logID"` -} - -type AddMetadataRequest struct { - TargetType string `json:"targetType"` - TargetID json.RawMessage `json:"targetId"` - Metadata metadata.Metadata `json:"metadata"` -} - -type RevertTransactionRequest struct { - ID int `json:"id"` - Force bool `json:"force"` - AtEffectiveDate bool `json:"atEffectiveDate"` -} - -type DeleteMetadataRequest struct { - TargetType string `json:"targetType"` - TargetID json.RawMessage `json:"targetId"` - Key string `json:"key"` -} - -type TransactionRequest struct { - Postings ledger.Postings `json:"postings"` - Script ledgercontroller.ScriptV1 `json:"script"` - Timestamp time.Time `json:"timestamp"` - Reference string `json:"reference"` - Metadata metadata.Metadata `json:"metadata" swaggertype:"object"` -} - -func (req *TransactionRequest) ToRunScript(allowUnboundedOverdrafts bool) (*ledgercontroller.RunScript, error) { - - if _, err := req.Postings.Validate(); err != nil { - return nil, err - } - - if len(req.Postings) > 0 { - txData := ledger.TransactionData{ - Postings: req.Postings, - Timestamp: req.Timestamp, - Reference: req.Reference, - Metadata: req.Metadata, - } - - return pointer.For(common.TxToScriptData(txData, allowUnboundedOverdrafts)), nil - } - - return &ledgercontroller.RunScript{ - Script: req.Script.ToCore(), - Timestamp: req.Timestamp, - Reference: req.Reference, - Metadata: req.Metadata, - }, nil -} - -func ProcessBulk(ctx context.Context, l ledgercontroller.Controller, bulk Bulk, continueOnFailure bool, atomic bool) ([]Result, bool, error) { - - for i, element := range bulk { - switch element.Action { - case ActionCreateTransaction: - req := &TransactionRequest{} - if err := json.Unmarshal(element.Data, req); err != nil { - return nil, false, fmt.Errorf("error parsing element %d: %s", i, err) - } - case ActionAddMetadata: - req := &AddMetadataRequest{} - if err := json.Unmarshal(element.Data, req); err != nil { - return nil, false, fmt.Errorf("error parsing element %d: %s", i, err) - } - case ActionRevertTransaction: - req := &RevertTransactionRequest{} - if err := json.Unmarshal(element.Data, req); err != nil { - return nil, false, fmt.Errorf("error parsing element %d: %s", i, err) - } - case ActionDeleteMetadata: - req := &DeleteMetadataRequest{} - if err := json.Unmarshal(element.Data, req); err != nil { - return nil, false, fmt.Errorf("error parsing element %d: %s", i, err) - } - } - } - - ret := make([]Result, 0, len(bulk)) - - errorsInBulk := false - var bulkError = func(action, code string, err error) { - ret = append(ret, Result{ - ErrorCode: code, - ErrorDescription: err.Error(), - ResponseType: "ERROR", - }) - errorsInBulk = true - } - - if atomic { - if err := l.BeginTX(ctx, nil); err != nil { - return nil, errorsInBulk, fmt.Errorf("error starting transaction: %s", err) - } - defer func() { - _ = l.Rollback(ctx) - }() - } - - for i, element := range bulk { - switch element.Action { - case ActionCreateTransaction: - req := &TransactionRequest{} - if err := json.Unmarshal(element.Data, req); err != nil { - return nil, errorsInBulk, fmt.Errorf("error parsing element %d: %s", i, err) - } - rs, err := req.ToRunScript(false) - if err != nil { - return nil, errorsInBulk, fmt.Errorf("error parsing element %d: %s", i, err) - } - - log, createTransactionResult, err := l.CreateTransaction(ctx, ledgercontroller.Parameters[ledgercontroller.RunScript]{ - DryRun: false, - IdempotencyKey: element.IdempotencyKey, - Input: *rs, - }) - if err != nil { - var code string - - switch { - case errors.Is(err, &ledgercontroller.ErrInsufficientFunds{}): - code = ErrInsufficientFund - case errors.Is(err, &ledgercontroller.ErrInvalidVars{}) || errors.Is(err, ledgercontroller.ErrCompilationFailed{}): - code = ErrCompilationFailed - case errors.Is(err, &ledgercontroller.ErrMetadataOverride{}): - code = ErrMetadataOverride - case errors.Is(err, ledgercontroller.ErrNoPostings): - code = ErrNoPostings - case errors.Is(err, ledgercontroller.ErrTransactionReferenceConflict{}): - code = ErrConflict - case errors.Is(err, ledgercontroller.ErrParsing{}): - code = ErrInterpreterParse - case errors.Is(err, ledgercontroller.ErrRuntime{}): - code = ErrInterpreterRuntime - default: - code = api.ErrorInternal - } - - bulkError(element.Action, code, err) - if !continueOnFailure { - return ret, errorsInBulk, nil - } - } else { - ret = append(ret, Result{ - Data: createTransactionResult.Transaction, - ResponseType: element.Action, - LogID: log.ID, - }) - } - case ActionAddMetadata: - req := &AddMetadataRequest{} - if err := json.Unmarshal(element.Data, req); err != nil { - return nil, errorsInBulk, fmt.Errorf("error parsing element %d: %s", i, err) - } - + mappedResults := make([]Result, 0) + for ind, result := range results { var ( - log *ledger.Log - err error + errorCode string + errorDescription string + responseType = b[ind].Action ) - switch req.TargetType { - case ledger.MetaTargetTypeAccount: - address := "" - if err := json.Unmarshal(req.TargetID, &address); err != nil { - return nil, errorsInBulk, err - } - log, err = l.SaveAccountMetadata(ctx, ledgercontroller.Parameters[ledgercontroller.SaveAccountMetadata]{ - DryRun: false, - IdempotencyKey: element.IdempotencyKey, - Input: ledgercontroller.SaveAccountMetadata{ - Address: address, - Metadata: req.Metadata, - }, - }) - case ledger.MetaTargetTypeTransaction: - transactionID := 0 - if err := json.Unmarshal(req.TargetID, &transactionID); err != nil { - return nil, errorsInBulk, err - } - log, err = l.SaveTransactionMetadata(ctx, ledgercontroller.Parameters[ledgercontroller.SaveTransactionMetadata]{ - DryRun: false, - IdempotencyKey: element.IdempotencyKey, - Input: ledgercontroller.SaveTransactionMetadata{ - TransactionID: transactionID, - Metadata: req.Metadata, - }, - }) - default: - return nil, errorsInBulk, fmt.Errorf("invalid target type: %s", req.TargetType) - } - if err != nil { - var code string - switch { - case errors.Is(err, ledgercontroller.ErrNotFound): - code = api.ErrorCodeNotFound - default: - code = api.ErrorInternal - } - bulkError(element.Action, code, err) - if !continueOnFailure { - return ret, errorsInBulk, nil - } - } else { - ret = append(ret, Result{ - ResponseType: element.Action, - LogID: log.ID, - }) - } - case ActionRevertTransaction: - req := &RevertTransactionRequest{} - if err := json.Unmarshal(element.Data, req); err != nil { - return nil, errorsInBulk, fmt.Errorf("error parsing element %d: %s", i, err) - } - log, revertTransactionResult, err := l.RevertTransaction(ctx, ledgercontroller.Parameters[ledgercontroller.RevertTransaction]{ - DryRun: false, - IdempotencyKey: element.IdempotencyKey, - Input: ledgercontroller.RevertTransaction{ - Force: req.Force, - AtEffectiveDate: req.AtEffectiveDate, - TransactionID: req.ID, - }, - }) - if err != nil { - var code string + if result.Error != nil { switch { - case errors.Is(err, ledgercontroller.ErrNotFound): - code = api.ErrorCodeNotFound + case errors.Is(result.Error, &ledgercontroller.ErrInsufficientFunds{}): + errorCode = ErrInsufficientFund + case errors.Is(result.Error, &ledgercontroller.ErrInvalidVars{}) || errors.Is(err, ledgercontroller.ErrCompilationFailed{}): + errorCode = ErrCompilationFailed + case errors.Is(result.Error, &ledgercontroller.ErrMetadataOverride{}): + errorCode = ErrMetadataOverride + case errors.Is(result.Error, ledgercontroller.ErrNoPostings): + errorCode = ErrNoPostings + case errors.Is(result.Error, ledgercontroller.ErrTransactionReferenceConflict{}): + errorCode = ErrConflict + case errors.Is(result.Error, ledgercontroller.ErrParsing{}): + errorCode = ErrInterpreterParse + case errors.Is(result.Error, ledgercontroller.ErrRuntime{}): + errorCode = ErrInterpreterRuntime default: - code = api.ErrorInternal - } - bulkError(element.Action, code, err) - if !continueOnFailure { - return ret, errorsInBulk, nil + errorCode = api.ErrorInternal } - } else { - ret = append(ret, Result{ - Data: revertTransactionResult.RevertTransaction, - ResponseType: element.Action, - LogID: log.ID, - }) + errorDescription = result.Error.Error() + responseType = "ERROR" } - case ActionDeleteMetadata: - req := &DeleteMetadataRequest{} - if err := json.Unmarshal(element.Data, req); err != nil { - return nil, errorsInBulk, fmt.Errorf("error parsing element %d: %s", i, err) - } - - var ( - log *ledger.Log - err error - ) - switch req.TargetType { - case ledger.MetaTargetTypeAccount: - address := "" - if err := json.Unmarshal(req.TargetID, &address); err != nil { - return nil, errorsInBulk, err - } - - log, err = l.DeleteAccountMetadata(ctx, ledgercontroller.Parameters[ledgercontroller.DeleteAccountMetadata]{ - DryRun: false, - IdempotencyKey: element.IdempotencyKey, - Input: ledgercontroller.DeleteAccountMetadata{ - Address: address, - Key: req.Key, - }, - }) - case ledger.MetaTargetTypeTransaction: - transactionID := 0 - if err := json.Unmarshal(req.TargetID, &transactionID); err != nil { - return nil, errorsInBulk, err - } - log, err = l.DeleteTransactionMetadata(ctx, ledgercontroller.Parameters[ledgercontroller.DeleteTransactionMetadata]{ - DryRun: false, - IdempotencyKey: element.IdempotencyKey, - Input: ledgercontroller.DeleteTransactionMetadata{ - TransactionID: transactionID, - Key: req.Key, - }, - }) - default: - return nil, errorsInBulk, fmt.Errorf("unsupported target type: %s", req.TargetType) - } - if err != nil { - var code string - switch { - case errors.Is(err, ledgercontroller.ErrNotFound): - code = api.ErrorCodeNotFound - default: - code = api.ErrorInternal - } - bulkError(element.Action, code, err) - if !continueOnFailure { - return ret, errorsInBulk, nil - } - } else { - ret = append(ret, Result{ - ResponseType: element.Action, - LogID: log.ID, - }) - } + mappedResults = append(mappedResults, Result{ + ErrorCode: errorCode, + ErrorDescription: errorDescription, + Data: result.Data, + ResponseType: responseType, + LogID: result.LogID, + }) } - } - if atomic { - if err := l.Commit(ctx); err != nil { - return nil, errorsInBulk, fmt.Errorf("error committing transaction: %s", err) + if err := json.NewEncoder(w).Encode(api.BaseResponse[[]Result]{ + Data: pointer.For(mappedResults), + }); err != nil { + panic(err) } } +} - return ret, errorsInBulk, nil +type Result struct { + ErrorCode string `json:"errorCode,omitempty"` + ErrorDescription string `json:"errorDescription,omitempty"` + Data any `json:"data,omitempty"` + ResponseType string `json:"responseType"` // Added for sdk generation (discriminator in oneOf) + LogID int `json:"logID"` } diff --git a/internal/api/v2/controllers_bulk_test.go b/internal/api/v2/controllers_bulk_test.go index a39f447b0..b94eae76f 100644 --- a/internal/api/v2/controllers_bulk_test.go +++ b/internal/api/v2/controllers_bulk_test.go @@ -3,7 +3,6 @@ package v2 import ( "bytes" "fmt" - "github.com/formancehq/ledger/internal/api/common" "math/big" "net/http" "net/http/httptest" @@ -63,7 +62,7 @@ func TestBulk(t *testing.T) { }} mockLedger.EXPECT(). CreateTransaction(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.RunScript]{ - Input: common.TxToScriptData(ledger.TransactionData{ + Input: ledgercontroller.TxToScriptData(ledger.TransactionData{ Postings: postings, Timestamp: now, }, false), @@ -93,7 +92,7 @@ func TestBulk(t *testing.T) { "reverted": false, "id": float64(0), }, - ResponseType: ActionCreateTransaction, + ResponseType: ledgercontroller.ActionCreateTransaction, }}, }, { @@ -121,7 +120,7 @@ func TestBulk(t *testing.T) { Return(&ledger.Log{}, nil) }, expectResults: []Result{{ - ResponseType: ActionAddMetadata, + ResponseType: ledgercontroller.ActionAddMetadata, }}, }, { @@ -149,7 +148,7 @@ func TestBulk(t *testing.T) { Return(&ledger.Log{}, nil) }, expectResults: []Result{{ - ResponseType: ActionAddMetadata, + ResponseType: ledgercontroller.ActionAddMetadata, }}, }, { @@ -177,7 +176,7 @@ func TestBulk(t *testing.T) { "reverted": false, "timestamp": "0001-01-01T00:00:00Z", }, - ResponseType: ActionRevertTransaction, + ResponseType: ledgercontroller.ActionRevertTransaction, }}, }, { @@ -201,7 +200,7 @@ func TestBulk(t *testing.T) { Return(&ledger.Log{}, nil) }, expectResults: []Result{{ - ResponseType: ActionDeleteMetadata, + ResponseType: ledgercontroller.ActionDeleteMetadata, }}, }, { @@ -261,7 +260,7 @@ func TestBulk(t *testing.T) { Return(nil, errors.New("unexpected error")) }, expectResults: []Result{{ - ResponseType: ActionAddMetadata, + ResponseType: ledgercontroller.ActionAddMetadata, }, { ErrorCode: api.ErrorInternal, ErrorDescription: "unexpected error", @@ -339,13 +338,13 @@ func TestBulk(t *testing.T) { Return(&ledger.Log{}, nil) }, expectResults: []Result{{ - ResponseType: ActionAddMetadata, + ResponseType: ledgercontroller.ActionAddMetadata, }, { ResponseType: "ERROR", ErrorCode: api.ErrorInternal, ErrorDescription: "unexpected error", }, { - ResponseType: ActionAddMetadata, + ResponseType: ledgercontroller.ActionAddMetadata, }}, expectError: true, }, @@ -379,7 +378,7 @@ func TestBulk(t *testing.T) { expectations: func(mockLedger *LedgerController) { mockLedger.EXPECT(). BeginTX(gomock.Any(), nil). - Return(nil) + Return(mockLedger, nil) mockLedger.EXPECT(). SaveAccountMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.SaveAccountMetadata]{ @@ -406,15 +405,11 @@ func TestBulk(t *testing.T) { mockLedger.EXPECT(). Commit(gomock.Any()). Return(nil) - - mockLedger.EXPECT(). - Rollback(gomock.Any()). - Return(nil) }, expectResults: []Result{{ - ResponseType: ActionAddMetadata, + ResponseType: ledgercontroller.ActionAddMetadata, }, { - ResponseType: ActionAddMetadata, + ResponseType: ledgercontroller.ActionAddMetadata, }}, }, } diff --git a/internal/api/v2/controllers_transactions_create.go b/internal/api/v2/controllers_transactions_create.go index 2147d146e..523882fbc 100644 --- a/internal/api/v2/controllers_transactions_create.go +++ b/internal/api/v2/controllers_transactions_create.go @@ -16,7 +16,7 @@ import ( func createTransaction(w http.ResponseWriter, r *http.Request) { l := common.LedgerFromContext(r.Context()) - payload := TransactionRequest{} + payload := ledgercontroller.TransactionRequest{} if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { api.BadRequest(w, ErrValidation, errors.New("invalid transaction format")) return diff --git a/internal/api/v2/controllers_transactions_create_test.go b/internal/api/v2/controllers_transactions_create_test.go index e4870caf2..e6ada7517 100644 --- a/internal/api/v2/controllers_transactions_create_test.go +++ b/internal/api/v2/controllers_transactions_create_test.go @@ -1,7 +1,6 @@ package v2 import ( - "github.com/formancehq/ledger/internal/api/common" "math/big" "net/http" "net/http/httptest" @@ -36,7 +35,7 @@ func TestTransactionCreate(t *testing.T) { testCases := []testCase{ { name: "using plain numscript", - payload: TransactionRequest{ + payload: ledgercontroller.TransactionRequest{ Script: ledgercontroller.ScriptV1{ Script: ledgercontroller.Script{ Plain: `XXX`, @@ -53,7 +52,7 @@ func TestTransactionCreate(t *testing.T) { }, { name: "using plain numscript with variables", - payload: TransactionRequest{ + payload: ledgercontroller.TransactionRequest{ Script: ledgercontroller.ScriptV1{ Script: ledgercontroller.Script{ Plain: `vars { @@ -90,7 +89,7 @@ func TestTransactionCreate(t *testing.T) { { name: "using plain numscript with variables (legacy format)", expectControllerCall: true, - payload: TransactionRequest{ + payload: ledgercontroller.TransactionRequest{ Script: ledgercontroller.ScriptV1{ Script: ledgercontroller.Script{ Plain: `vars { @@ -129,7 +128,7 @@ func TestTransactionCreate(t *testing.T) { { name: "using plain numscript and dry run", expectControllerCall: true, - payload: TransactionRequest{ + payload: ledgercontroller.TransactionRequest{ Script: ledgercontroller.ScriptV1{ Script: ledgercontroller.Script{ Plain: `send ( @@ -156,12 +155,12 @@ func TestTransactionCreate(t *testing.T) { { name: "using JSON postings", expectControllerCall: true, - payload: TransactionRequest{ + payload: ledgercontroller.TransactionRequest{ Postings: []ledger.Posting{ ledger.NewPosting("world", "bank", "USD", big.NewInt(100)), }, }, - expectedRunScript: common.TxToScriptData(ledger.NewTransactionData().WithPostings( + expectedRunScript: ledgercontroller.TxToScriptData(ledger.NewTransactionData().WithPostings( ledger.NewPosting("world", "bank", "USD", big.NewInt(100)), ), false), }, @@ -171,19 +170,19 @@ func TestTransactionCreate(t *testing.T) { queryParams: url.Values{ "dryRun": []string{"true"}, }, - payload: TransactionRequest{ + payload: ledgercontroller.TransactionRequest{ Postings: []ledger.Posting{ ledger.NewPosting("world", "bank", "USD", big.NewInt(100)), }, }, expectedDryRun: true, - expectedRunScript: common.TxToScriptData(ledger.NewTransactionData().WithPostings( + expectedRunScript: ledgercontroller.TxToScriptData(ledger.NewTransactionData().WithPostings( ledger.NewPosting("world", "bank", "USD", big.NewInt(100)), ), false), }, { name: "no postings or script", - payload: TransactionRequest{ + payload: ledgercontroller.TransactionRequest{ Metadata: map[string]string{}, }, expectedStatusCode: http.StatusBadRequest, @@ -192,7 +191,7 @@ func TestTransactionCreate(t *testing.T) { }, { name: "postings and script", - payload: TransactionRequest{ + payload: ledgercontroller.TransactionRequest{ Postings: ledger.Postings{ { Source: "world", @@ -223,7 +222,7 @@ func TestTransactionCreate(t *testing.T) { { name: "with insufficient funds", expectControllerCall: true, - payload: TransactionRequest{ + payload: ledgercontroller.TransactionRequest{ Script: ledgercontroller.ScriptV1{ Script: ledgercontroller.Script{ Plain: `XXX`, @@ -242,7 +241,7 @@ func TestTransactionCreate(t *testing.T) { }, { name: "using JSON postings and negative amount", - payload: TransactionRequest{ + payload: ledgercontroller.TransactionRequest{ Postings: []ledger.Posting{ ledger.NewPosting("world", "bank", "USD", big.NewInt(-100)), }, @@ -253,7 +252,7 @@ func TestTransactionCreate(t *testing.T) { { expectControllerCall: true, name: "numscript and negative amount", - payload: TransactionRequest{ + payload: ledgercontroller.TransactionRequest{ Script: ledgercontroller.ScriptV1{ Script: ledgercontroller.Script{ Plain: `send [COIN -100] ( @@ -279,7 +278,7 @@ func TestTransactionCreate(t *testing.T) { { name: "numscript and compilation failed", expectControllerCall: true, - payload: TransactionRequest{ + payload: ledgercontroller.TransactionRequest{ Script: ledgercontroller.ScriptV1{ Script: ledgercontroller.Script{ Plain: `send [COIN XXX] ( @@ -305,7 +304,7 @@ func TestTransactionCreate(t *testing.T) { { name: "numscript and no postings", expectControllerCall: true, - payload: TransactionRequest{ + payload: ledgercontroller.TransactionRequest{ Script: ledgercontroller.ScriptV1{ Script: ledgercontroller.Script{ Plain: `vars {}`, @@ -325,7 +324,7 @@ func TestTransactionCreate(t *testing.T) { { name: "numscript and metadata override", expectControllerCall: true, - payload: TransactionRequest{ + payload: ledgercontroller.TransactionRequest{ Script: ledgercontroller.ScriptV1{ Script: ledgercontroller.Script{ Plain: `send [COIN 100] ( @@ -361,7 +360,7 @@ func TestTransactionCreate(t *testing.T) { { name: "unexpected error", expectControllerCall: true, - payload: TransactionRequest{ + payload: ledgercontroller.TransactionRequest{ Script: ledgercontroller.ScriptV1{ Script: ledgercontroller.Script{ Plain: `send [COIN 100] ( diff --git a/internal/api/v2/mocks_ledger_controller_test.go b/internal/api/v2/mocks_ledger_controller_test.go index cc4bf5f39..26775c2d7 100644 --- a/internal/api/v2/mocks_ledger_controller_test.go +++ b/internal/api/v2/mocks_ledger_controller_test.go @@ -41,11 +41,12 @@ func (m *LedgerController) EXPECT() *LedgerControllerMockRecorder { } // BeginTX mocks base method. -func (m *LedgerController) BeginTX(ctx context.Context, options *sql.TxOptions) error { +func (m *LedgerController) BeginTX(ctx context.Context, options *sql.TxOptions) (ledger0.Controller, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "BeginTX", ctx, options) - ret0, _ := ret[0].(error) - return ret0 + ret0, _ := ret[0].(ledger0.Controller) + ret1, _ := ret[1].(error) + return ret0, ret1 } // BeginTX indicates an expected call of BeginTX. diff --git a/internal/controller/ledger/bulker.go b/internal/controller/ledger/bulker.go new file mode 100644 index 000000000..eccb169db --- /dev/null +++ b/internal/controller/ledger/bulker.go @@ -0,0 +1,331 @@ +package ledger + +import ( + "context" + "encoding/json" + "fmt" + "github.com/formancehq/go-libs/v2/logging" + "github.com/formancehq/go-libs/v2/metadata" + "github.com/formancehq/go-libs/v2/pointer" + "github.com/formancehq/go-libs/v2/time" + ledger "github.com/formancehq/ledger/internal" +) + +const ( + ActionCreateTransaction = "CREATE_TRANSACTION" + ActionAddMetadata = "ADD_METADATA" + ActionRevertTransaction = "REVERT_TRANSACTION" + ActionDeleteMetadata = "DELETE_METADATA" +) + +type Bulk []BulkElement + +type BulkResult []BulkElementResult + +func (r BulkResult) HasErrors() bool { + for _, element := range r { + if element.Error != nil { + return true + } + } + + return false +} + +type BulkElement struct { + Action string `json:"action"` + IdempotencyKey string `json:"ik"` + Data json.RawMessage `json:"data"` +} + +type BulkElementResult struct { + Error error + Data any `json:"data,omitempty"` + LogID int `json:"logID"` +} + +type AddMetadataRequest struct { + TargetType string `json:"targetType"` + TargetID json.RawMessage `json:"targetId"` + Metadata metadata.Metadata `json:"metadata"` +} + +type RevertTransactionRequest struct { + ID int `json:"id"` + Force bool `json:"force"` + AtEffectiveDate bool `json:"atEffectiveDate"` +} + +type DeleteMetadataRequest struct { + TargetType string `json:"targetType"` + TargetID json.RawMessage `json:"targetId"` + Key string `json:"key"` +} + +type TransactionRequest struct { + Postings ledger.Postings `json:"postings"` + Script ScriptV1 `json:"script"` + Timestamp time.Time `json:"timestamp"` + Reference string `json:"reference"` + Metadata metadata.Metadata `json:"metadata" swaggertype:"object"` +} + +func (req *TransactionRequest) ToRunScript(allowUnboundedOverdrafts bool) (*RunScript, error) { + + if _, err := req.Postings.Validate(); err != nil { + return nil, err + } + + if len(req.Postings) > 0 { + txData := ledger.TransactionData{ + Postings: req.Postings, + Timestamp: req.Timestamp, + Reference: req.Reference, + Metadata: req.Metadata, + } + + return pointer.For(TxToScriptData(txData, allowUnboundedOverdrafts)), nil + } + + return &RunScript{ + Script: req.Script.ToCore(), + Timestamp: req.Timestamp, + Reference: req.Reference, + Metadata: req.Metadata, + }, nil +} + +type Bulker struct { + ctrl Controller +} + +func (b *Bulker) run(ctx context.Context, ctrl Controller, bulk Bulk, continueOnFailure bool) (BulkResult, error) { + for i, element := range bulk { + switch element.Action { + case ActionCreateTransaction: + req := &TransactionRequest{} + if err := json.Unmarshal(element.Data, req); err != nil { + return nil, fmt.Errorf("error parsing element %d: %s", i, err) + } + case ActionAddMetadata: + req := &AddMetadataRequest{} + if err := json.Unmarshal(element.Data, req); err != nil { + return nil, fmt.Errorf("error parsing element %d: %s", i, err) + } + case ActionRevertTransaction: + req := &RevertTransactionRequest{} + if err := json.Unmarshal(element.Data, req); err != nil { + return nil, fmt.Errorf("error parsing element %d: %s", i, err) + } + case ActionDeleteMetadata: + req := &DeleteMetadataRequest{} + if err := json.Unmarshal(element.Data, req); err != nil { + return nil, fmt.Errorf("error parsing element %d: %s", i, err) + } + } + } + + results := make([]BulkElementResult, 0, len(bulk)) + + for _, element := range bulk { + ret, logID, err := b.processElement(ctx, ctrl, element) + if err != nil { + results = append(results, BulkElementResult{ + Error: err, + }) + + if !continueOnFailure { + return results, nil + } + + continue + } + + results = append(results, BulkElementResult{ + Data: ret, + LogID: logID, + }) + } + + return results, nil +} + +func (b *Bulker) Run(ctx context.Context, bulk Bulk, continueOnFailure, atomic bool) (BulkResult, error) { + ctrl := b.ctrl + if atomic { + var err error + ctrl, err = b.ctrl.BeginTX(ctx, nil) + if err != nil { + return nil, fmt.Errorf("error starting transaction: %s", err) + } + } + + results, err := b.run(ctx, ctrl, bulk, continueOnFailure) + if err != nil { + if atomic { + if rollbackErr := ctrl.Rollback(ctx); rollbackErr != nil { + logging.FromContext(ctx).Errorf("failed to rollback transaction: %v", rollbackErr) + } + } + + return nil, fmt.Errorf("error running bulk: %s", err) + } + + if atomic { + if results.HasErrors() { + if rollbackErr := ctrl.Rollback(ctx); rollbackErr != nil { + logging.FromContext(ctx).Errorf("failed to rollback transaction: %v", rollbackErr) + } + } else { + if err := ctrl.Commit(ctx); err != nil { + return nil, fmt.Errorf("error committing transaction: %s", err) + } + } + } + + return results, err +} + +func (b *Bulker) processElement(ctx context.Context, ctrl Controller, element BulkElement) (any, int, error) { + + switch element.Action { + case ActionCreateTransaction: + req := &TransactionRequest{} + if err := json.Unmarshal(element.Data, req); err != nil { + return nil, 0, fmt.Errorf("error parsing element: %s", err) + } + rs, err := req.ToRunScript(false) + if err != nil { + return nil, 0, fmt.Errorf("error parsing element: %s", err) + } + + log, createTransactionResult, err := ctrl.CreateTransaction(ctx, Parameters[RunScript]{ + DryRun: false, + IdempotencyKey: element.IdempotencyKey, + Input: *rs, + }) + if err != nil { + return nil, 0, err + } + + return createTransactionResult.Transaction, log.ID, nil + case ActionAddMetadata: + req := &AddMetadataRequest{} + if err := json.Unmarshal(element.Data, req); err != nil { + return nil, 0, fmt.Errorf("error parsing element: %s", err) + } + + var ( + log *ledger.Log + err error + ) + switch req.TargetType { + case ledger.MetaTargetTypeAccount: + address := "" + if err := json.Unmarshal(req.TargetID, &address); err != nil { + return nil, 0, err + } + log, err = ctrl.SaveAccountMetadata(ctx, Parameters[SaveAccountMetadata]{ + DryRun: false, + IdempotencyKey: element.IdempotencyKey, + Input: SaveAccountMetadata{ + Address: address, + Metadata: req.Metadata, + }, + }) + case ledger.MetaTargetTypeTransaction: + transactionID := 0 + if err := json.Unmarshal(req.TargetID, &transactionID); err != nil { + return nil, 0, err + } + log, err = ctrl.SaveTransactionMetadata(ctx, Parameters[SaveTransactionMetadata]{ + DryRun: false, + IdempotencyKey: element.IdempotencyKey, + Input: SaveTransactionMetadata{ + TransactionID: transactionID, + Metadata: req.Metadata, + }, + }) + default: + return nil, 0, fmt.Errorf("invalid target type: %s", req.TargetType) + } + if err != nil { + return nil, 0, err + } + + return nil, log.ID, nil + case ActionRevertTransaction: + req := &RevertTransactionRequest{} + if err := json.Unmarshal(element.Data, req); err != nil { + return nil, 0, fmt.Errorf("error parsing element: %s", err) + } + + log, revertTransactionResult, err := ctrl.RevertTransaction(ctx, Parameters[RevertTransaction]{ + DryRun: false, + IdempotencyKey: element.IdempotencyKey, + Input: RevertTransaction{ + Force: req.Force, + AtEffectiveDate: req.AtEffectiveDate, + TransactionID: req.ID, + }, + }) + if err != nil { + return nil, 0, err + } + + return revertTransactionResult.RevertedTransaction, log.ID, nil + case ActionDeleteMetadata: + req := &DeleteMetadataRequest{} + if err := json.Unmarshal(element.Data, req); err != nil { + return nil, 0, fmt.Errorf("error parsing element: %s", err) + } + + var ( + log *ledger.Log + err error + ) + switch req.TargetType { + case ledger.MetaTargetTypeAccount: + address := "" + if err := json.Unmarshal(req.TargetID, &address); err != nil { + return nil, 0, err + } + + log, err = ctrl.DeleteAccountMetadata(ctx, Parameters[DeleteAccountMetadata]{ + DryRun: false, + IdempotencyKey: element.IdempotencyKey, + Input: DeleteAccountMetadata{ + Address: address, + Key: req.Key, + }, + }) + case ledger.MetaTargetTypeTransaction: + transactionID := 0 + if err := json.Unmarshal(req.TargetID, &transactionID); err != nil { + return nil, 0, err + } + + log, err = ctrl.DeleteTransactionMetadata(ctx, Parameters[DeleteTransactionMetadata]{ + DryRun: false, + IdempotencyKey: element.IdempotencyKey, + Input: DeleteTransactionMetadata{ + TransactionID: transactionID, + Key: req.Key, + }, + }) + default: + return nil, 0, fmt.Errorf("unsupported target type: %s", req.TargetType) + } + if err != nil { + return nil, 0, err + } + + return nil, log.ID, nil + default: + panic("unreachable") + } +} + +func NewBulker(ctrl Controller) *Bulker { + return &Bulker{ctrl: ctrl} +} diff --git a/internal/controller/ledger/controller.go b/internal/controller/ledger/controller.go index d99375ec2..eee5771a8 100644 --- a/internal/controller/ledger/controller.go +++ b/internal/controller/ledger/controller.go @@ -14,7 +14,7 @@ import ( //go:generate mockgen -write_source_comment=false -write_package_comment=false -source controller.go -destination controller_generated_test.go -package ledger . Controller type Controller interface { - BeginTX(ctx context.Context, options *sql.TxOptions) error + BeginTX(ctx context.Context, options *sql.TxOptions) (Controller, error) Commit(ctx context.Context) error Rollback(ctx context.Context) error diff --git a/internal/controller/ledger/controller_default.go b/internal/controller/ledger/controller_default.go index c5d34ce51..ae114e6c4 100644 --- a/internal/controller/ledger/controller_default.go +++ b/internal/controller/ledger/controller_default.go @@ -51,8 +51,15 @@ type DefaultController struct { deleteAccountMetadataLp *logProcessor[DeleteAccountMetadata, ledger.DeletedMetadata] } -func (ctrl *DefaultController) BeginTX(ctx context.Context, options *sql.TxOptions) error { - return ctrl.store.BeginTX(ctx, options) +func (ctrl *DefaultController) BeginTX(ctx context.Context, options *sql.TxOptions) (Controller, error) { + cp := *ctrl + var err error + cp.store, err = ctrl.store.BeginTX(ctx, options) + if err != nil { + return nil, err + } + + return &cp, nil } func (ctrl *DefaultController) Commit(_ context.Context) error { @@ -139,22 +146,35 @@ func (ctrl *DefaultController) Import(ctx context.Context, stream chan ledger.Lo // Use serializable isolation level to ensure no concurrent request use the store. // If a concurrent transactions is made while we are importing some logs, the transaction importing logs will // be canceled with serialization error. - err := ctrl.store.BeginTX(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable}) + store, err := ctrl.store.BeginTX(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable}) if err != nil { return fmt.Errorf("failed to start transaction: %w", err) } - defer func() { - _ = ctrl.store.Rollback() - }() + + if err := ctrl.importLogs(ctx, store, stream); err != nil { + if rollbackErr := store.Rollback(); rollbackErr != nil { + logging.FromContext(ctx).Errorf("failed to rollback transaction: %v", rollbackErr) + } + return err + } + + if err := store.Commit(); err != nil { + return fmt.Errorf("failed to commit transaction: %w", err) + } + + return nil +} + +func (ctrl *DefaultController) importLogs(ctx context.Context, store Store, stream chan ledger.Log) error { // Due to the serializable isolation level, and since we explicitly ask for the ledger state in the sql transaction context // if the state change, the sql transaction will be aborted with a serialization error - if err := ctrl.store.LockLedger(ctx); err != nil { + if err := store.LockLedger(ctx); err != nil { return fmt.Errorf("failed to lock ledger: %w", err) } // We can import only if the ledger is empty. - logs, err := ctrl.store.ListLogs(ctx, NewListLogsQuery(PaginatedQueryOptions[any]{ + logs, err := store.ListLogs(ctx, NewListLogsQuery(PaginatedQueryOptions[any]{ PageSize: 1, })) if err != nil { @@ -166,7 +186,7 @@ func (ctrl *DefaultController) Import(ctx context.Context, stream chan ledger.Lo } for log := range stream { - if err := ctrl.importLog(ctx, log); err != nil { + if err := ctrl.importLog(ctx, store, log); err != nil { if errors.Is(err, postgres.ErrSerialization) { return newErrImport(errors.New("concurrent transaction occur" + "red, cannot import the ledger")) @@ -175,33 +195,29 @@ func (ctrl *DefaultController) Import(ctx context.Context, stream chan ledger.Lo } } - if err := ctrl.store.Commit(); err != nil { - return fmt.Errorf("failed to commit transaction: %w", err) - } - return err } // todo: as ids are generated by the database, the exported ids can not match imported ones // it can cause issues with actions referencing other objects -func (ctrl *DefaultController) importLog(ctx context.Context, log ledger.Log) error { +func (ctrl *DefaultController) importLog(ctx context.Context, store Store, log ledger.Log) error { switch payload := log.Data.(type) { case ledger.CreatedTransaction: logging.FromContext(ctx).Debugf("Importing transaction %d", payload.Transaction.ID) - if err := ctrl.store.CommitTransaction(ctx, &payload.Transaction); err != nil { + if err := store.CommitTransaction(ctx, &payload.Transaction); err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } logging.FromContext(ctx).Debugf("Imported transaction %d", payload.Transaction.ID) if len(payload.AccountMetadata) > 0 { logging.FromContext(ctx).Debugf("Importing metadata of accounts '%s'", Keys(payload.AccountMetadata)) - if err := ctrl.store.UpdateAccountsMetadata(ctx, payload.AccountMetadata); err != nil { + if err := store.UpdateAccountsMetadata(ctx, payload.AccountMetadata); err != nil { return fmt.Errorf("updating metadata of accounts '%s': %w", Keys(payload.AccountMetadata), err) } } case ledger.RevertedTransaction: logging.FromContext(ctx).Debugf("Reverting transaction %d", payload.RevertedTransaction.ID) - _, _, err := ctrl.store.RevertTransaction( + _, _, err := store.RevertTransaction( ctx, payload.RevertedTransaction.ID, *payload.RevertedTransaction.RevertedAt, @@ -209,19 +225,19 @@ func (ctrl *DefaultController) importLog(ctx context.Context, log ledger.Log) er if err != nil { return fmt.Errorf("failed to revert transaction: %w", err) } - if err := ctrl.store.CommitTransaction(ctx, &payload.RevertTransaction); err != nil { + if err := store.CommitTransaction(ctx, &payload.RevertTransaction); err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } case ledger.SavedMetadata: switch payload.TargetType { case ledger.MetaTargetTypeTransaction: logging.FromContext(ctx).Debugf("Saving metadata of transaction %d", payload.TargetID) - if _, _, err := ctrl.store.UpdateTransactionMetadata(ctx, payload.TargetID.(int), payload.Metadata); err != nil { + if _, _, err := store.UpdateTransactionMetadata(ctx, payload.TargetID.(int), payload.Metadata); err != nil { return fmt.Errorf("failed to update transaction metadata: %w", err) } case ledger.MetaTargetTypeAccount: logging.FromContext(ctx).Debugf("Saving metadata of account %s", payload.TargetID) - if err := ctrl.store.UpdateAccountsMetadata(ctx, ledger.AccountMetadata{ + if err := store.UpdateAccountsMetadata(ctx, ledger.AccountMetadata{ payload.TargetID.(string): payload.Metadata, }); err != nil { return fmt.Errorf("failed to update account metadata: %w", err) @@ -231,12 +247,12 @@ func (ctrl *DefaultController) importLog(ctx context.Context, log ledger.Log) er switch payload.TargetType { case ledger.MetaTargetTypeTransaction: logging.FromContext(ctx).Debugf("Deleting metadata of transaction %d", payload.TargetID) - if _, _, err := ctrl.store.DeleteTransactionMetadata(ctx, payload.TargetID.(int), payload.Key); err != nil { + if _, _, err := store.DeleteTransactionMetadata(ctx, payload.TargetID.(int), payload.Key); err != nil { return fmt.Errorf("failed to delete transaction metadata: %w", err) } case ledger.MetaTargetTypeAccount: logging.FromContext(ctx).Debugf("Deleting metadata of account %s", payload.TargetID) - if err := ctrl.store.DeleteAccountMetadata(ctx, payload.TargetID.(string), payload.Key); err != nil { + if err := store.DeleteAccountMetadata(ctx, payload.TargetID.(string), payload.Key); err != nil { return fmt.Errorf("failed to delete account metadata: %w", err) } } @@ -244,7 +260,7 @@ func (ctrl *DefaultController) importLog(ctx context.Context, log ledger.Log) er logCopy := log logging.FromContext(ctx).Debugf("Inserting log %d", log.ID) - if err := ctrl.store.InsertLog(ctx, &log); err != nil { + if err := store.InsertLog(ctx, &log); err != nil { return fmt.Errorf("failed to insert log: %w", err) } @@ -284,7 +300,7 @@ func (ctrl *DefaultController) GetVolumesWithBalances(ctx context.Context, q Get return ctrl.store.GetVolumesWithBalances(ctx, q) } -func (ctrl *DefaultController) createTransaction(ctx context.Context, sqlTX Store, parameters Parameters[RunScript]) (*ledger.CreatedTransaction, error) { +func (ctrl *DefaultController) createTransaction(ctx context.Context, store Store, parameters Parameters[RunScript]) (*ledger.CreatedTransaction, error) { logger := logging.FromContext(ctx).WithField("req", uuid.NewString()[:8]) ctx = logging.ContextWithLogger(ctx, logger) @@ -300,7 +316,7 @@ func (ctrl *DefaultController) createTransaction(ctx context.Context, sqlTX Stor ctrl.tracer, ctrl.executeMachineHistogram, func(ctx context.Context) (*NumscriptExecutionResult, error) { - return m.Execute(ctx, sqlTX, parameters.Input.Vars) + return m.Execute(ctx, store, parameters.Input.Vars) }, ) if err != nil { @@ -327,13 +343,13 @@ func (ctrl *DefaultController) createTransaction(ctx context.Context, sqlTX Stor WithMetadata(finalMetadata). WithTimestamp(parameters.Input.Timestamp). WithReference(parameters.Input.Reference) - err = sqlTX.CommitTransaction(ctx, &transaction) + err = store.CommitTransaction(ctx, &transaction) if err != nil { return nil, err } if len(result.AccountMetadata) > 0 { - if err := sqlTX.UpdateAccountsMetadata(ctx, result.AccountMetadata); err != nil { + if err := store.UpdateAccountsMetadata(ctx, result.AccountMetadata); err != nil { return nil, fmt.Errorf("updating metadata of account '%s': %w", Keys(result.AccountMetadata), err) } } diff --git a/internal/controller/ledger/controller_default_test.go b/internal/controller/ledger/controller_default_test.go index 0996f44ae..d590b19c8 100644 --- a/internal/controller/ledger/controller_default_test.go +++ b/internal/controller/ledger/controller_default_test.go @@ -2,7 +2,6 @@ package ledger import ( "context" - "database/sql" "math/big" "testing" @@ -37,16 +36,12 @@ func TestCreateTransaction(t *testing.T) { store.EXPECT(). BeginTX(gomock.Any(), nil). - Return(nil) + Return(store, nil) store.EXPECT(). Commit(). Return(nil) - store.EXPECT(). - Rollback(). - Return(sql.ErrTxDone) - posting := ledger.NewPosting("world", "bank", "USD", big.NewInt(100)) numscriptRuntime.EXPECT(). Execute(gomock.Any(), store, runScript.Vars). @@ -84,16 +79,12 @@ func TestRevertTransaction(t *testing.T) { store.EXPECT(). BeginTX(gomock.Any(), nil). - Return(nil) + Return(store, nil) store.EXPECT(). Commit(). Return(nil) - store.EXPECT(). - Rollback(). - Return(sql.ErrTxDone) - txToRevert := ledger.Transaction{} store.EXPECT(). RevertTransaction(gomock.Any(), 1, time.Time{}). @@ -136,16 +127,12 @@ func TestSaveTransactionMetadata(t *testing.T) { store.EXPECT(). BeginTX(gomock.Any(), nil). - Return(nil) + Return(store, nil) store.EXPECT(). Commit(). Return(nil) - store.EXPECT(). - Rollback(). - Return(sql.ErrTxDone) - m := metadata.Metadata{ "foo": "bar", } @@ -180,16 +167,12 @@ func TestDeleteTransactionMetadata(t *testing.T) { store.EXPECT(). BeginTX(gomock.Any(), nil). - Return(nil) + Return(store, nil) store.EXPECT(). Commit(). Return(nil) - store.EXPECT(). - Rollback(). - Return(sql.ErrTxDone) - store.EXPECT(). DeleteTransactionMetadata(gomock.Any(), 1, "foo"). Return(&ledger.Transaction{}, true, nil) diff --git a/internal/controller/ledger/controller_generated_test.go b/internal/controller/ledger/controller_generated_test.go index e895d1656..3090e4922 100644 --- a/internal/controller/ledger/controller_generated_test.go +++ b/internal/controller/ledger/controller_generated_test.go @@ -40,11 +40,12 @@ func (m *MockController) EXPECT() *MockControllerMockRecorder { } // BeginTX mocks base method. -func (m *MockController) BeginTX(ctx context.Context, options *sql.TxOptions) error { +func (m *MockController) BeginTX(ctx context.Context, options *sql.TxOptions) (Controller, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "BeginTX", ctx, options) - ret0, _ := ret[0].(error) - return ret0 + ret0, _ := ret[0].(Controller) + ret1, _ := ret[1].(error) + return ret0, ret1 } // BeginTX indicates an expected call of BeginTX. diff --git a/internal/controller/ledger/controller_with_cache.go b/internal/controller/ledger/controller_with_cache.go index ec5f7532b..17daff8c6 100644 --- a/internal/controller/ledger/controller_with_cache.go +++ b/internal/controller/ledger/controller_with_cache.go @@ -2,6 +2,7 @@ package ledger import ( "context" + "database/sql" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -34,6 +35,19 @@ func (c *ControllerWithCache) IsDatabaseUpToDate(ctx context.Context) (bool, err return upToDate, nil } +func (c *ControllerWithCache) BeginTX(ctx context.Context, options *sql.TxOptions) (Controller, error) { + ctrl, err := c.Controller.BeginTX(ctx, options) + if err != nil { + return nil, err + } + + return &ControllerWithCache{ + registry: c.registry, + ledger: c.ledger, + Controller: ctrl, + }, nil +} + func NewControllerWithCache(ledger ledger.Ledger, underlying Controller, registry *StateRegistry) *ControllerWithCache { return &ControllerWithCache{ ledger: ledger, diff --git a/internal/controller/ledger/controller_with_events.go b/internal/controller/ledger/controller_with_events.go index e704bae78..dafbe5255 100644 --- a/internal/controller/ledger/controller_with_events.go +++ b/internal/controller/ledger/controller_with_events.go @@ -2,6 +2,7 @@ package ledger import ( "context" + "database/sql" "fmt" ledger "github.com/formancehq/ledger/internal" ) @@ -120,4 +121,17 @@ func (ctrl *ControllerWithEvents) DeleteAccountMetadata(ctx context.Context, par return log, nil } +func (c *ControllerWithEvents) BeginTX(ctx context.Context, options *sql.TxOptions) (Controller, error) { + ctrl, err := c.Controller.BeginTX(ctx, options) + if err != nil { + return nil, err + } + + return &ControllerWithEvents{ + ledger: c.ledger, + Controller: ctrl, + listener: c.listener, + }, nil +} + var _ Controller = (*ControllerWithEvents)(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 8098103ee..3dbd6bec0 100644 --- a/internal/controller/ledger/controller_with_too_many_client_handling.go +++ b/internal/controller/ledger/controller_with_too_many_client_handling.go @@ -2,6 +2,7 @@ package ledger import ( "context" + "database/sql" "errors" "github.com/formancehq/go-libs/v2/platform/postgres" ledger "github.com/formancehq/ledger/internal" @@ -38,85 +39,98 @@ func NewControllerWithTooManyClientHandling( } } -func (ctrl *ControllerWithTooManyClientHandling) CreateTransaction(ctx context.Context, parameters Parameters[RunScript]) (*ledger.Log, *ledger.CreatedTransaction, error) { +func (c *ControllerWithTooManyClientHandling) CreateTransaction(ctx context.Context, parameters Parameters[RunScript]) (*ledger.Log, *ledger.CreatedTransaction, error) { var ( log *ledger.Log createdTransaction *ledger.CreatedTransaction err error ) - err = handleRetry(ctx, ctrl.tracer, ctrl.delayCalculator, func(ctx context.Context) error { - log, createdTransaction, err = ctrl.Controller.CreateTransaction(ctx, parameters) + err = handleRetry(ctx, c.tracer, c.delayCalculator, func(ctx context.Context) error { + log, createdTransaction, err = c.Controller.CreateTransaction(ctx, parameters) return err }) return log, createdTransaction, err } -func (ctrl *ControllerWithTooManyClientHandling) RevertTransaction(ctx context.Context, parameters Parameters[RevertTransaction]) (*ledger.Log, *ledger.RevertedTransaction, error) { +func (c *ControllerWithTooManyClientHandling) RevertTransaction(ctx context.Context, parameters Parameters[RevertTransaction]) (*ledger.Log, *ledger.RevertedTransaction, error) { var ( log *ledger.Log revertedTransaction *ledger.RevertedTransaction err error ) - err = handleRetry(ctx, ctrl.tracer, ctrl.delayCalculator, func(ctx context.Context) error { - log, revertedTransaction, err = ctrl.Controller.RevertTransaction(ctx, parameters) + err = handleRetry(ctx, c.tracer, c.delayCalculator, func(ctx context.Context) error { + log, revertedTransaction, err = c.Controller.RevertTransaction(ctx, parameters) return err }) return log, revertedTransaction, err } -func (ctrl *ControllerWithTooManyClientHandling) SaveTransactionMetadata(ctx context.Context, parameters Parameters[SaveTransactionMetadata]) (*ledger.Log, error) { +func (c *ControllerWithTooManyClientHandling) SaveTransactionMetadata(ctx context.Context, parameters Parameters[SaveTransactionMetadata]) (*ledger.Log, error) { var ( log *ledger.Log err error ) - err = handleRetry(ctx, ctrl.tracer, ctrl.delayCalculator, func(ctx context.Context) error { - log, err = ctrl.Controller.SaveTransactionMetadata(ctx, parameters) + err = handleRetry(ctx, c.tracer, c.delayCalculator, func(ctx context.Context) error { + log, err = c.Controller.SaveTransactionMetadata(ctx, parameters) return err }) return log, err } -func (ctrl *ControllerWithTooManyClientHandling) SaveAccountMetadata(ctx context.Context, parameters Parameters[SaveAccountMetadata]) (*ledger.Log, error) { +func (c *ControllerWithTooManyClientHandling) SaveAccountMetadata(ctx context.Context, parameters Parameters[SaveAccountMetadata]) (*ledger.Log, error) { var ( log *ledger.Log err error ) - err = handleRetry(ctx, ctrl.tracer, ctrl.delayCalculator, func(ctx context.Context) error { - log, err = ctrl.Controller.SaveAccountMetadata(ctx, parameters) + err = handleRetry(ctx, c.tracer, c.delayCalculator, func(ctx context.Context) error { + log, err = c.Controller.SaveAccountMetadata(ctx, parameters) return err }) return log, err } -func (ctrl *ControllerWithTooManyClientHandling) DeleteTransactionMetadata(ctx context.Context, parameters Parameters[DeleteTransactionMetadata]) (*ledger.Log, error) { +func (c *ControllerWithTooManyClientHandling) DeleteTransactionMetadata(ctx context.Context, parameters Parameters[DeleteTransactionMetadata]) (*ledger.Log, error) { var ( log *ledger.Log err error ) - err = handleRetry(ctx, ctrl.tracer, ctrl.delayCalculator, func(ctx context.Context) error { - log, err = ctrl.Controller.DeleteTransactionMetadata(ctx, parameters) + err = handleRetry(ctx, c.tracer, c.delayCalculator, func(ctx context.Context) error { + log, err = c.Controller.DeleteTransactionMetadata(ctx, parameters) return err }) return log, err } -func (ctrl *ControllerWithTooManyClientHandling) DeleteAccountMetadata(ctx context.Context, parameters Parameters[DeleteAccountMetadata]) (*ledger.Log, error) { +func (c *ControllerWithTooManyClientHandling) DeleteAccountMetadata(ctx context.Context, parameters Parameters[DeleteAccountMetadata]) (*ledger.Log, error) { var ( log *ledger.Log err error ) - err = handleRetry(ctx, ctrl.tracer, ctrl.delayCalculator, func(ctx context.Context) error { - log, err = ctrl.Controller.DeleteAccountMetadata(ctx, parameters) + err = handleRetry(ctx, c.tracer, c.delayCalculator, func(ctx context.Context) error { + log, err = c.Controller.DeleteAccountMetadata(ctx, parameters) return err }) return log, err } +func (c *ControllerWithTooManyClientHandling) BeginTX(ctx context.Context, options *sql.TxOptions) (Controller, error) { + ctrl, err := c.Controller.BeginTX(ctx, options) + if err != nil { + return nil, err + } + + return &ControllerWithTooManyClientHandling{ + Controller: ctrl, + delayCalculator: c.delayCalculator, + tracer: c.tracer, + }, nil +} + var _ Controller = (*ControllerWithTooManyClientHandling)(nil) func handleRetry( diff --git a/internal/controller/ledger/controller_with_traces.go b/internal/controller/ledger/controller_with_traces.go index 39188b027..214e415a5 100644 --- a/internal/controller/ledger/controller_with_traces.go +++ b/internal/controller/ledger/controller_with_traces.go @@ -23,145 +23,153 @@ func NewControllerWithTraces(underlying Controller, tracer trace.Tracer) *Contro } } -func (ctrl *ControllerWithTraces) BeginTX(ctx context.Context, options *sql.TxOptions) error { - return tracing.SkipResult(tracing.Trace(ctx, ctrl.tracer, "BeginTX", tracing.NoResult(func(ctx context.Context) error { - return ctrl.underlying.BeginTX(ctx, options) - }))) +func (c *ControllerWithTraces) BeginTX(ctx context.Context, options *sql.TxOptions) (Controller, error) { + return tracing.Trace(ctx, c.tracer, "BeginTX", func(ctx context.Context) (Controller, error) { + ctrl, err := c.underlying.BeginTX(ctx, options) + if err != nil { + return nil, err + } + + return &ControllerWithTraces{ + underlying: ctrl, + tracer: c.tracer, + }, nil + }) } -func (ctrl *ControllerWithTraces) Commit(ctx context.Context) error { - return tracing.SkipResult(tracing.Trace(ctx, ctrl.tracer, "BeginTX", tracing.NoResult(func(ctx context.Context) error { - return ctrl.underlying.Commit(ctx) +func (c *ControllerWithTraces) Commit(ctx context.Context) error { + return tracing.SkipResult(tracing.Trace(ctx, c.tracer, "BeginTX", tracing.NoResult(func(ctx context.Context) error { + return c.underlying.Commit(ctx) }))) } -func (ctrl *ControllerWithTraces) Rollback(ctx context.Context) error { - return tracing.SkipResult(tracing.Trace(ctx, ctrl.tracer, "BeginTX", tracing.NoResult(func(ctx context.Context) error { - return ctrl.underlying.Rollback(ctx) +func (c *ControllerWithTraces) Rollback(ctx context.Context) error { + return tracing.SkipResult(tracing.Trace(ctx, c.tracer, "BeginTX", tracing.NoResult(func(ctx context.Context) error { + return c.underlying.Rollback(ctx) }))) } -func (ctrl *ControllerWithTraces) GetMigrationsInfo(ctx context.Context) ([]migrations.Info, error) { - return ctrl.underlying.GetMigrationsInfo(ctx) +func (c *ControllerWithTraces) GetMigrationsInfo(ctx context.Context) ([]migrations.Info, error) { + return c.underlying.GetMigrationsInfo(ctx) } -func (ctrl *ControllerWithTraces) ListTransactions(ctx context.Context, q ListTransactionsQuery) (*bunpaginate.Cursor[ledger.Transaction], error) { - return tracing.Trace(ctx, ctrl.tracer, "ListTransactions", func(ctx context.Context) (*bunpaginate.Cursor[ledger.Transaction], error) { - return ctrl.underlying.ListTransactions(ctx, q) +func (c *ControllerWithTraces) ListTransactions(ctx context.Context, q ListTransactionsQuery) (*bunpaginate.Cursor[ledger.Transaction], error) { + return tracing.Trace(ctx, c.tracer, "ListTransactions", func(ctx context.Context) (*bunpaginate.Cursor[ledger.Transaction], error) { + return c.underlying.ListTransactions(ctx, q) }) } -func (ctrl *ControllerWithTraces) CountTransactions(ctx context.Context, q ListTransactionsQuery) (int, error) { - return tracing.Trace(ctx, ctrl.tracer, "CountTransactions", func(ctx context.Context) (int, error) { - return ctrl.underlying.CountTransactions(ctx, q) +func (c *ControllerWithTraces) CountTransactions(ctx context.Context, q ListTransactionsQuery) (int, error) { + return tracing.Trace(ctx, c.tracer, "CountTransactions", func(ctx context.Context) (int, error) { + return c.underlying.CountTransactions(ctx, q) }) } -func (ctrl *ControllerWithTraces) GetTransaction(ctx context.Context, query GetTransactionQuery) (*ledger.Transaction, error) { - return tracing.Trace(ctx, ctrl.tracer, "GetTransaction", func(ctx context.Context) (*ledger.Transaction, error) { - return ctrl.underlying.GetTransaction(ctx, query) +func (c *ControllerWithTraces) GetTransaction(ctx context.Context, query GetTransactionQuery) (*ledger.Transaction, error) { + return tracing.Trace(ctx, c.tracer, "GetTransaction", func(ctx context.Context) (*ledger.Transaction, error) { + return c.underlying.GetTransaction(ctx, query) }) } -func (ctrl *ControllerWithTraces) CountAccounts(ctx context.Context, a ListAccountsQuery) (int, error) { - return tracing.Trace(ctx, ctrl.tracer, "CountAccounts", func(ctx context.Context) (int, error) { - return ctrl.underlying.CountAccounts(ctx, a) +func (c *ControllerWithTraces) CountAccounts(ctx context.Context, a ListAccountsQuery) (int, error) { + return tracing.Trace(ctx, c.tracer, "CountAccounts", func(ctx context.Context) (int, error) { + return c.underlying.CountAccounts(ctx, a) }) } -func (ctrl *ControllerWithTraces) ListAccounts(ctx context.Context, a ListAccountsQuery) (*bunpaginate.Cursor[ledger.Account], error) { - return tracing.Trace(ctx, ctrl.tracer, "ListAccounts", func(ctx context.Context) (*bunpaginate.Cursor[ledger.Account], error) { - return ctrl.underlying.ListAccounts(ctx, a) +func (c *ControllerWithTraces) ListAccounts(ctx context.Context, a ListAccountsQuery) (*bunpaginate.Cursor[ledger.Account], error) { + return tracing.Trace(ctx, c.tracer, "ListAccounts", func(ctx context.Context) (*bunpaginate.Cursor[ledger.Account], error) { + return c.underlying.ListAccounts(ctx, a) }) } -func (ctrl *ControllerWithTraces) GetAccount(ctx context.Context, q GetAccountQuery) (*ledger.Account, error) { - return tracing.Trace(ctx, ctrl.tracer, "GetAccount", func(ctx context.Context) (*ledger.Account, error) { - return ctrl.underlying.GetAccount(ctx, q) +func (c *ControllerWithTraces) GetAccount(ctx context.Context, q GetAccountQuery) (*ledger.Account, error) { + return tracing.Trace(ctx, c.tracer, "GetAccount", func(ctx context.Context) (*ledger.Account, error) { + return c.underlying.GetAccount(ctx, q) }) } -func (ctrl *ControllerWithTraces) GetAggregatedBalances(ctx context.Context, q GetAggregatedBalanceQuery) (ledger.BalancesByAssets, error) { - return tracing.Trace(ctx, ctrl.tracer, "GetAggregatedBalances", func(ctx context.Context) (ledger.BalancesByAssets, error) { - return ctrl.underlying.GetAggregatedBalances(ctx, q) +func (c *ControllerWithTraces) GetAggregatedBalances(ctx context.Context, q GetAggregatedBalanceQuery) (ledger.BalancesByAssets, error) { + return tracing.Trace(ctx, c.tracer, "GetAggregatedBalances", func(ctx context.Context) (ledger.BalancesByAssets, error) { + return c.underlying.GetAggregatedBalances(ctx, q) }) } -func (ctrl *ControllerWithTraces) ListLogs(ctx context.Context, q GetLogsQuery) (*bunpaginate.Cursor[ledger.Log], error) { - return tracing.Trace(ctx, ctrl.tracer, "ListLogs", func(ctx context.Context) (*bunpaginate.Cursor[ledger.Log], error) { - return ctrl.underlying.ListLogs(ctx, q) +func (c *ControllerWithTraces) ListLogs(ctx context.Context, q GetLogsQuery) (*bunpaginate.Cursor[ledger.Log], error) { + return tracing.Trace(ctx, c.tracer, "ListLogs", func(ctx context.Context) (*bunpaginate.Cursor[ledger.Log], error) { + return c.underlying.ListLogs(ctx, q) }) } -func (ctrl *ControllerWithTraces) Import(ctx context.Context, stream chan ledger.Log) error { - return tracing.SkipResult(tracing.Trace(ctx, ctrl.tracer, "Import", tracing.NoResult(func(ctx context.Context) error { - return ctrl.underlying.Import(ctx, stream) +func (c *ControllerWithTraces) Import(ctx context.Context, stream chan ledger.Log) error { + return tracing.SkipResult(tracing.Trace(ctx, c.tracer, "Import", tracing.NoResult(func(ctx context.Context) error { + return c.underlying.Import(ctx, stream) }))) } -func (ctrl *ControllerWithTraces) Export(ctx context.Context, w ExportWriter) error { - return tracing.SkipResult(tracing.Trace(ctx, ctrl.tracer, "Export", tracing.NoResult(func(ctx context.Context) error { - return ctrl.underlying.Export(ctx, w) +func (c *ControllerWithTraces) Export(ctx context.Context, w ExportWriter) error { + return tracing.SkipResult(tracing.Trace(ctx, c.tracer, "Export", tracing.NoResult(func(ctx context.Context) error { + return c.underlying.Export(ctx, w) }))) } -func (ctrl *ControllerWithTraces) IsDatabaseUpToDate(ctx context.Context) (bool, error) { - return tracing.Trace(ctx, ctrl.tracer, "IsDatabaseUpToDate", func(ctx context.Context) (bool, error) { - return ctrl.underlying.IsDatabaseUpToDate(ctx) +func (c *ControllerWithTraces) IsDatabaseUpToDate(ctx context.Context) (bool, error) { + return tracing.Trace(ctx, c.tracer, "IsDatabaseUpToDate", func(ctx context.Context) (bool, error) { + return c.underlying.IsDatabaseUpToDate(ctx) }) } -func (ctrl *ControllerWithTraces) GetVolumesWithBalances(ctx context.Context, q GetVolumesWithBalancesQuery) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { - return tracing.Trace(ctx, ctrl.tracer, "GetVolumesWithBalances", func(ctx context.Context) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { - return ctrl.underlying.GetVolumesWithBalances(ctx, q) +func (c *ControllerWithTraces) GetVolumesWithBalances(ctx context.Context, q GetVolumesWithBalancesQuery) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { + return tracing.Trace(ctx, c.tracer, "GetVolumesWithBalances", func(ctx context.Context) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { + return c.underlying.GetVolumesWithBalances(ctx, q) }) } -func (ctrl *ControllerWithTraces) CreateTransaction(ctx context.Context, parameters Parameters[RunScript]) (*ledger.Log, *ledger.CreatedTransaction, error) { - ctx, span := ctrl.tracer.Start(ctx, "CreateTransaction") +func (c *ControllerWithTraces) CreateTransaction(ctx context.Context, parameters Parameters[RunScript]) (*ledger.Log, *ledger.CreatedTransaction, error) { + ctx, span := c.tracer.Start(ctx, "CreateTransaction") defer span.End() - return ctrl.underlying.CreateTransaction(ctx, parameters) + return c.underlying.CreateTransaction(ctx, parameters) } -func (ctrl *ControllerWithTraces) RevertTransaction(ctx context.Context, parameters Parameters[RevertTransaction]) (*ledger.Log, *ledger.RevertedTransaction, error) { - ctx, span := ctrl.tracer.Start(ctx, "RevertTransaction") +func (c *ControllerWithTraces) RevertTransaction(ctx context.Context, parameters Parameters[RevertTransaction]) (*ledger.Log, *ledger.RevertedTransaction, error) { + ctx, span := c.tracer.Start(ctx, "RevertTransaction") defer span.End() - return ctrl.underlying.RevertTransaction(ctx, parameters) + return c.underlying.RevertTransaction(ctx, parameters) } -func (ctrl *ControllerWithTraces) SaveTransactionMetadata(ctx context.Context, parameters Parameters[SaveTransactionMetadata]) (*ledger.Log, error) { - ctx, span := ctrl.tracer.Start(ctx, "SaveTransactionMetadata") +func (c *ControllerWithTraces) SaveTransactionMetadata(ctx context.Context, parameters Parameters[SaveTransactionMetadata]) (*ledger.Log, error) { + ctx, span := c.tracer.Start(ctx, "SaveTransactionMetadata") defer span.End() - return ctrl.underlying.SaveTransactionMetadata(ctx, parameters) + return c.underlying.SaveTransactionMetadata(ctx, parameters) } -func (ctrl *ControllerWithTraces) SaveAccountMetadata(ctx context.Context, parameters Parameters[SaveAccountMetadata]) (*ledger.Log, error) { - ctx, span := ctrl.tracer.Start(ctx, "SaveAccountMetadata") +func (c *ControllerWithTraces) SaveAccountMetadata(ctx context.Context, parameters Parameters[SaveAccountMetadata]) (*ledger.Log, error) { + ctx, span := c.tracer.Start(ctx, "SaveAccountMetadata") defer span.End() - return ctrl.underlying.SaveAccountMetadata(ctx, parameters) + return c.underlying.SaveAccountMetadata(ctx, parameters) } -func (ctrl *ControllerWithTraces) DeleteTransactionMetadata(ctx context.Context, parameters Parameters[DeleteTransactionMetadata]) (*ledger.Log, error) { - ctx, span := ctrl.tracer.Start(ctx, "DeleteTransactionMetadata") +func (c *ControllerWithTraces) DeleteTransactionMetadata(ctx context.Context, parameters Parameters[DeleteTransactionMetadata]) (*ledger.Log, error) { + ctx, span := c.tracer.Start(ctx, "DeleteTransactionMetadata") defer span.End() - return ctrl.underlying.DeleteTransactionMetadata(ctx, parameters) + return c.underlying.DeleteTransactionMetadata(ctx, parameters) } -func (ctrl *ControllerWithTraces) DeleteAccountMetadata(ctx context.Context, parameters Parameters[DeleteAccountMetadata]) (*ledger.Log, error) { - ctx, span := ctrl.tracer.Start(ctx, "DeleteAccountMetadata") +func (c *ControllerWithTraces) DeleteAccountMetadata(ctx context.Context, parameters Parameters[DeleteAccountMetadata]) (*ledger.Log, error) { + ctx, span := c.tracer.Start(ctx, "DeleteAccountMetadata") defer span.End() - return ctrl.underlying.DeleteAccountMetadata(ctx, parameters) + return c.underlying.DeleteAccountMetadata(ctx, parameters) } -func (ctrl *ControllerWithTraces) GetStats(ctx context.Context) (Stats, error) { - return tracing.Trace(ctx, ctrl.tracer, "GetStats", func(ctx context.Context) (Stats, error) { - return ctrl.underlying.GetStats(ctx) +func (c *ControllerWithTraces) GetStats(ctx context.Context) (Stats, error) { + return tracing.Trace(ctx, c.tracer, "GetStats", func(ctx context.Context) (Stats, error) { + return c.underlying.GetStats(ctx) }) } diff --git a/internal/controller/ledger/log_process.go b/internal/controller/ledger/log_process.go index a3811e861..327e0f1fb 100644 --- a/internal/controller/ledger/log_process.go +++ b/internal/controller/ledger/log_process.go @@ -31,22 +31,45 @@ func (lp *logProcessor[INPUT, OUTPUT]) runTx( parameters Parameters[INPUT], fn func(ctx context.Context, sqlTX Store, parameters Parameters[INPUT]) (*OUTPUT, error), ) (*ledger.Log, *OUTPUT, error) { - var ( - output *OUTPUT - log ledger.Log - ) - if err := store.BeginTX(ctx, nil); err != nil { + store, err := store.BeginTX(ctx, nil) + if err != nil { return nil, nil, fmt.Errorf("failed to start transaction: %w", err) } - defer func() { - _ = store.Rollback() - }() + + log, output, err := lp.runLog(ctx, store, parameters, fn) + if err != nil { + if rollbackErr := store.Rollback(); rollbackErr != nil { + logging.FromContext(ctx).Errorf("failed to rollback transaction: %v", rollbackErr) + } + return nil, nil, err + } + + if parameters.DryRun { + if rollbackErr := store.Rollback(); rollbackErr != nil { + logging.FromContext(ctx).Errorf("failed to rollback transaction: %v", rollbackErr) + } + return log, output, nil + } + + if err := store.Commit(); err != nil { + return nil, nil, fmt.Errorf("failed to commit transaction: %w", err) + } + + return log, output, nil +} + +func (lp *logProcessor[INPUT, OUTPUT]) runLog( + ctx context.Context, + store Store, + parameters Parameters[INPUT], + fn func(ctx context.Context, sqlTX Store, parameters Parameters[INPUT]) (*OUTPUT, error), +) (*ledger.Log, *OUTPUT, error) { output, err := fn(ctx, store, parameters) if err != nil { return nil, nil, err } - log = ledger.NewLog(*output) + log := ledger.NewLog(*output) log.IdempotencyKey = parameters.IdempotencyKey log.IdempotencyHash = ledger.ComputeIdempotencyHash(parameters.Input) @@ -56,14 +79,6 @@ func (lp *logProcessor[INPUT, OUTPUT]) runTx( } logging.FromContext(ctx).Debugf("log inserted with id %d", log.ID) - if parameters.DryRun { - return &log, output, nil - } - - if err := store.Commit(); err != nil { - return nil, nil, fmt.Errorf("failed to commit transaction: %w", err) - } - return &log, output, err } diff --git a/internal/controller/ledger/log_process_test.go b/internal/controller/ledger/log_process_test.go index c62a58003..fd573e788 100644 --- a/internal/controller/ledger/log_process_test.go +++ b/internal/controller/ledger/log_process_test.go @@ -24,7 +24,7 @@ func TestForgeLogWithIKConflict(t *testing.T) { store.EXPECT(). BeginTX(gomock.Any(), gomock.Any()). - Return(nil) + Return(store, nil) store.EXPECT(). Rollback(). @@ -55,7 +55,7 @@ func TestForgeLogWithDeadlock(t *testing.T) { // First call returns a deadlock store.EXPECT(). BeginTX(gomock.Any(), gomock.Any()). - Return(nil) + Return(store, nil) store.EXPECT(). Rollback(). @@ -64,7 +64,7 @@ func TestForgeLogWithDeadlock(t *testing.T) { // Second call is ok store.EXPECT(). BeginTX(gomock.Any(), gomock.Any()). - Return(nil) + Return(store, nil) store.EXPECT(). InsertLog(gomock.Any(), gomock.Any()). @@ -74,10 +74,6 @@ func TestForgeLogWithDeadlock(t *testing.T) { Commit(). Return(nil) - store.EXPECT(). - Rollback(). - Return(nil) - firstCall := true lp := newLogProcessor[RunScript, ledger.CreatedTransaction]("foo", noop.Int64Counter{}) _, _, err := lp.forgeLog(ctx, store, Parameters[RunScript]{}, func(ctx context.Context, store Store, parameters Parameters[RunScript]) (*ledger.CreatedTransaction, error) { diff --git a/internal/api/common/numscript.go b/internal/controller/ledger/numscript.go similarity index 93% rename from internal/api/common/numscript.go rename to internal/controller/ledger/numscript.go index 2eae29a37..310166a84 100644 --- a/internal/api/common/numscript.go +++ b/internal/controller/ledger/numscript.go @@ -1,9 +1,8 @@ -package common +package ledger import ( "fmt" "github.com/formancehq/ledger/internal" - ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" "sort" "strings" @@ -15,7 +14,7 @@ type variable struct { value string } -func TxToScriptData(txData ledger.TransactionData, allowUnboundedOverdrafts bool) ledgercontroller.RunScript { +func TxToScriptData(txData ledger.TransactionData, allowUnboundedOverdrafts bool) RunScript { sb := strings.Builder{} monetaryToVars := map[string]variable{} accountsToVars := map[string]variable{} @@ -113,8 +112,8 @@ func TxToScriptData(txData ledger.TransactionData, allowUnboundedOverdrafts bool txData.Metadata = metadata.Metadata{} } - return ledgercontroller.RunScript{ - Script: ledgercontroller.Script{ + return RunScript{ + Script: Script{ Plain: sb.String(), Vars: vars, }, diff --git a/internal/controller/ledger/store.go b/internal/controller/ledger/store.go index dc10e1ac3..c442293fe 100644 --- a/internal/controller/ledger/store.go +++ b/internal/controller/ledger/store.go @@ -29,7 +29,7 @@ type Balances = vm.Balances //go:generate mockgen -write_source_comment=false -write_package_comment=false -source store.go -destination store_generated_test.go -package ledger . Store type Store interface { - BeginTX(ctx context.Context, options *sql.TxOptions) error + BeginTX(ctx context.Context, options *sql.TxOptions) (Store, error) Commit() error Rollback() error diff --git a/internal/controller/ledger/store_generated_test.go b/internal/controller/ledger/store_generated_test.go index a6d3b4dca..ff5d1465d 100644 --- a/internal/controller/ledger/store_generated_test.go +++ b/internal/controller/ledger/store_generated_test.go @@ -43,11 +43,12 @@ func (m *MockStore) EXPECT() *MockStoreMockRecorder { } // BeginTX mocks base method. -func (m *MockStore) BeginTX(ctx context.Context, options *sql.TxOptions) error { +func (m *MockStore) BeginTX(ctx context.Context, options *sql.TxOptions) (Store, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "BeginTX", ctx, options) - ret0, _ := ret[0].(error) - return ret0 + ret0, _ := ret[0].(Store) + ret1, _ := ret[1].(error) + return ret0, ret1 } // BeginTX indicates an expected call of BeginTX. diff --git a/internal/storage/ledger/legacy/adapters.go b/internal/storage/ledger/legacy/adapters.go index b7c225eec..cb0d20eb3 100644 --- a/internal/storage/ledger/legacy/adapters.go +++ b/internal/storage/ledger/legacy/adapters.go @@ -110,15 +110,18 @@ func (d *DefaultStoreAdapter) GetMigrationsInfo(ctx context.Context) ([]migratio return d.newStore.GetMigrationsInfo(ctx) } -func (d *DefaultStoreAdapter) BeginTX(ctx context.Context, opts *sql.TxOptions) error { - err := d.newStore.BeginTX(ctx, opts) +func (d *DefaultStoreAdapter) BeginTX(ctx context.Context, opts *sql.TxOptions) (ledgercontroller.Store, error) { + store, err := d.newStore.BeginTX(ctx, opts) if err != nil { - return err + return nil, err } - d.legacyStore = d.legacyStore.WithDB(d.newStore.GetDB()) + d.legacyStore = d.legacyStore.WithDB(store.GetDB()) - return nil + return &DefaultStoreAdapter{ + newStore: store, + legacyStore: d.legacyStore, + }, nil } func (d *DefaultStoreAdapter) Commit() error { diff --git a/internal/storage/ledger/store.go b/internal/storage/ledger/store.go index 89e79120c..87bcfeb44 100644 --- a/internal/storage/ledger/store.go +++ b/internal/storage/ledger/store.go @@ -19,10 +19,9 @@ import ( ) type Store struct { - dbStack []bun.IDB - db bun.IDB - bucket bucket.Bucket - ledger ledger.Ledger + db bun.IDB + bucket bucket.Bucket + ledger ledger.Ledger tracer trace.Tracer meter metric.Meter @@ -50,38 +49,32 @@ type Store struct { listTransactionsHistogram metric.Int64Histogram } -func (s *Store) BeginTX(ctx context.Context, options *sql.TxOptions) error { +func (s *Store) BeginTX(ctx context.Context, options *sql.TxOptions) (*Store, error) { tx, err := s.db.BeginTx(ctx, options) if err != nil { - return postgres.ResolveError(err) + return nil, postgres.ResolveError(err) } - s.dbStack = append(s.dbStack, s.db) - s.db = tx + cp := *s + cp.db = tx - return nil + return &cp, nil } func (s *Store) Commit() error { switch db := s.db.(type) { case bun.Tx: - err := db.Commit() - s.db = s.dbStack[len(s.dbStack)-1] - s.dbStack = s.dbStack[:len(s.dbStack)-1] - return err + return db.Commit() default: - return errors.New("not in a transaction") + return errors.New("cannot commit transaction: not in a transaction") } } func (s *Store) Rollback() error { switch db := s.db.(type) { case bun.Tx: - err := db.Rollback() - s.db = s.dbStack[len(s.dbStack)-1] - s.dbStack = s.dbStack[:len(s.dbStack)-1] - return err + return db.Rollback() default: - return errors.New("not in a transaction") + return errors.New("cannot rollback transaction: not in a transaction") } } diff --git a/pkg/generate/generator.go b/pkg/generate/generator.go index 7f319d23b..a71fd7bec 100644 --- a/pkg/generate/generator.go +++ b/pkg/generate/generator.go @@ -8,7 +8,7 @@ import ( "github.com/dop251/goja" "github.com/formancehq/go-libs/v2/collectionutils" ledger "github.com/formancehq/ledger/internal" - v2 "github.com/formancehq/ledger/internal/api/v2" + ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" "github.com/formancehq/ledger/pkg/client" "github.com/formancehq/ledger/pkg/client/models/components" "github.com/formancehq/ledger/pkg/client/models/operations" @@ -18,7 +18,7 @@ import ( ) type Action struct { - v2.BulkElement + ledgercontroller.BulkElement } type Result struct { @@ -44,8 +44,8 @@ func (r Action) Apply(ctx context.Context, client *client.V2, l string) (*Result var bulkElement components.V2BulkElement switch r.Action { - case v2.ActionCreateTransaction: - transactionRequest := &v2.TransactionRequest{} + case ledgercontroller.ActionCreateTransaction: + transactionRequest := &ledgercontroller.TransactionRequest{} err := json.Unmarshal(r.Data, transactionRequest) if err != nil { return nil, fmt.Errorf("failed to unmarshal transaction request: %w", err) @@ -82,8 +82,8 @@ func (r Action) Apply(ctx context.Context, client *client.V2, l string) (*Result Metadata: transactionRequest.Metadata, }, }) - case v2.ActionAddMetadata: - addMetadataRequest := &v2.AddMetadataRequest{} + case ledgercontroller.ActionAddMetadata: + addMetadataRequest := &ledgercontroller.AddMetadataRequest{} err := json.Unmarshal(r.Data, addMetadataRequest) if err != nil { return nil, fmt.Errorf("failed to unmarshal add metadata request: %w", err) @@ -114,8 +114,8 @@ func (r Action) Apply(ctx context.Context, client *client.V2, l string) (*Result Metadata: addMetadataRequest.Metadata, }, }) - case v2.ActionDeleteMetadata: - deleteMetadataRequest := &v2.DeleteMetadataRequest{} + case ledgercontroller.ActionDeleteMetadata: + deleteMetadataRequest := &ledgercontroller.DeleteMetadataRequest{} err := json.Unmarshal(r.Data, deleteMetadataRequest) if err != nil { return nil, fmt.Errorf("failed to unmarshal delete metadata request: %w", err) @@ -146,8 +146,8 @@ func (r Action) Apply(ctx context.Context, client *client.V2, l string) (*Result Key: deleteMetadataRequest.Key, }, }) - case v2.ActionRevertTransaction: - revertMetadataRequest := &v2.RevertTransactionRequest{} + case ledgercontroller.ActionRevertTransaction: + revertMetadataRequest := &ledgercontroller.RevertTransactionRequest{} err := json.Unmarshal(r.Data, revertMetadataRequest) if err != nil { return nil, fmt.Errorf("failed to unmarshal delete metadata request: %w", err) @@ -270,7 +270,7 @@ func NewGenerator(script string, opts ...Option) (*Generator, error) { } return &Action{ - BulkElement: v2.BulkElement{ + BulkElement: ledgercontroller.BulkElement{ Action: action, IdempotencyKey: ik, Data: dataAsJsonRawMessage, diff --git a/test/e2e/api_bulk_test.go b/test/e2e/api_bulk_test.go index f55ac6aff..1bfdfeaa4 100644 --- a/test/e2e/api_bulk_test.go +++ b/test/e2e/api_bulk_test.go @@ -25,7 +25,7 @@ var _ = Context("Ledger engine tests", func() { numscriptRewrite bool }{ {"default", false}, - {"numscript rewrite", true}, + //{"numscript rewrite", true}, } { Context(data.description, func() { diff --git a/tools/generator/go.mod b/tools/generator/go.mod index 149dbb3ec..abd649151 100644 --- a/tools/generator/go.mod +++ b/tools/generator/go.mod @@ -31,21 +31,13 @@ require ( github.com/dop251/goja v0.0.0-20241009100908-5f46f2705ca3 // indirect github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731 // indirect github.com/fatih/color v1.18.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect github.com/formancehq/numscript v0.0.9-0.20241009144012-1150c14a1417 // indirect - github.com/go-chi/chi/v5 v5.1.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect github.com/google/uuid v1.6.0 // indirect - github.com/gorilla/mux v1.8.1 // indirect - github.com/gorilla/schema v1.4.1 // indirect - github.com/gorilla/securecookie v1.1.2 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 // indirect - github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect - github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/jsonschema v0.12.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect @@ -59,14 +51,11 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/muhlemmer/gu v0.3.1 // indirect - github.com/muhlemmer/httpforwarded v0.1.0 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect - github.com/riandyrn/otelchi v0.10.1 // indirect - github.com/rs/cors v1.11.1 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect @@ -76,34 +65,17 @@ require ( github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect - github.com/zitadel/oidc/v2 v2.12.2 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 // indirect - go.opentelemetry.io/contrib/propagators/b3 v1.32.0 // indirect go.opentelemetry.io/otel v1.32.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 // indirect go.opentelemetry.io/otel/log v0.7.0 // indirect go.opentelemetry.io/otel/metric v1.32.0 // indirect - go.opentelemetry.io/otel/sdk v1.32.0 // indirect go.opentelemetry.io/otel/trace v1.32.0 // indirect - go.opentelemetry.io/proto/otlp v1.3.1 // indirect - go.uber.org/dig v1.18.0 // indirect - go.uber.org/fx v1.23.0 // indirect go.uber.org/mock v0.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.28.0 // indirect golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect - golang.org/x/net v0.30.0 // indirect golang.org/x/sync v0.9.0 // indirect golang.org/x/sys v0.27.0 // indirect golang.org/x/text v0.20.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 // indirect - google.golang.org/grpc v1.67.1 // indirect - google.golang.org/protobuf v1.35.1 // indirect - gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/tools/generator/go.sum b/tools/generator/go.sum index 67c8b8372..ea6126fb6 100644 --- a/tools/generator/go.sum +++ b/tools/generator/go.sum @@ -137,14 +137,10 @@ github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIx github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= -github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= @@ -196,8 +192,6 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6 github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= -github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc= -github.com/jeremija/gosubmit v0.2.7/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= From 1e2acc4f3b3b1ccbe1f2dd871a6e8796af1be084 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 22 Nov 2024 14:58:54 +0100 Subject: [PATCH 32/71] fix(bulk): defer events sending at commit --- internal/controller/ledger/bulker.go | 52 ++++--- .../ledger/controller_with_events.go | 146 ++++++++++++------ test/e2e/api_bulk_test.go | 60 +++++-- 3 files changed, 172 insertions(+), 86 deletions(-) diff --git a/internal/controller/ledger/bulker.go b/internal/controller/ledger/bulker.go index eccb169db..a1d79b2ac 100644 --- a/internal/controller/ledger/bulker.go +++ b/internal/controller/ledger/bulker.go @@ -100,30 +100,6 @@ type Bulker struct { } func (b *Bulker) run(ctx context.Context, ctrl Controller, bulk Bulk, continueOnFailure bool) (BulkResult, error) { - for i, element := range bulk { - switch element.Action { - case ActionCreateTransaction: - req := &TransactionRequest{} - if err := json.Unmarshal(element.Data, req); err != nil { - return nil, fmt.Errorf("error parsing element %d: %s", i, err) - } - case ActionAddMetadata: - req := &AddMetadataRequest{} - if err := json.Unmarshal(element.Data, req); err != nil { - return nil, fmt.Errorf("error parsing element %d: %s", i, err) - } - case ActionRevertTransaction: - req := &RevertTransactionRequest{} - if err := json.Unmarshal(element.Data, req); err != nil { - return nil, fmt.Errorf("error parsing element %d: %s", i, err) - } - case ActionDeleteMetadata: - req := &DeleteMetadataRequest{} - if err := json.Unmarshal(element.Data, req); err != nil { - return nil, fmt.Errorf("error parsing element %d: %s", i, err) - } - } - } results := make([]BulkElementResult, 0, len(bulk)) @@ -151,10 +127,36 @@ func (b *Bulker) run(ctx context.Context, ctrl Controller, bulk Bulk, continueOn } func (b *Bulker) Run(ctx context.Context, bulk Bulk, continueOnFailure, atomic bool) (BulkResult, error) { + + for i, element := range bulk { + switch element.Action { + case ActionCreateTransaction: + req := &TransactionRequest{} + if err := json.Unmarshal(element.Data, req); err != nil { + return nil, fmt.Errorf("error parsing element %d: %s", i, err) + } + case ActionAddMetadata: + req := &AddMetadataRequest{} + if err := json.Unmarshal(element.Data, req); err != nil { + return nil, fmt.Errorf("error parsing element %d: %s", i, err) + } + case ActionRevertTransaction: + req := &RevertTransactionRequest{} + if err := json.Unmarshal(element.Data, req); err != nil { + return nil, fmt.Errorf("error parsing element %d: %s", i, err) + } + case ActionDeleteMetadata: + req := &DeleteMetadataRequest{} + if err := json.Unmarshal(element.Data, req); err != nil { + return nil, fmt.Errorf("error parsing element %d: %s", i, err) + } + } + } + ctrl := b.ctrl if atomic { var err error - ctrl, err = b.ctrl.BeginTX(ctx, nil) + ctrl, err = ctrl.BeginTX(ctx, nil) if err != nil { return nil, fmt.Errorf("error starting transaction: %s", err) } diff --git a/internal/controller/ledger/controller_with_events.go b/internal/controller/ledger/controller_with_events.go index dafbe5255..af97bfad5 100644 --- a/internal/controller/ledger/controller_with_events.go +++ b/internal/controller/ledger/controller_with_events.go @@ -11,6 +11,9 @@ type ControllerWithEvents struct { Controller ledger ledger.Ledger listener Listener + atCommit []func() + parent *ControllerWithEvents + hasTx bool } func NewControllerWithEvents(ledger ledger.Ledger, underlying Controller, listener Listener) *ControllerWithEvents { @@ -20,102 +23,128 @@ func NewControllerWithEvents(ledger ledger.Ledger, underlying Controller, listen listener: listener, } } -func (ctrl *ControllerWithEvents) CreateTransaction(ctx context.Context, parameters Parameters[RunScript]) (*ledger.Log, *ledger.CreatedTransaction, error) { - log, ret, err := ctrl.Controller.CreateTransaction(ctx, parameters) + +func (c *ControllerWithEvents) handleEvent(ctx context.Context, fn func()) { + if !c.hasTx { + fn() + return + } + if c.parent != nil && c.parent.hasTx { + c.parent.handleEvent(ctx, fn) + return + } + + c.atCommit = append(c.atCommit, fn) +} + +func (c *ControllerWithEvents) CreateTransaction(ctx context.Context, parameters Parameters[RunScript]) (*ledger.Log, *ledger.CreatedTransaction, error) { + log, ret, err := c.Controller.CreateTransaction(ctx, parameters) if err != nil { return nil, nil, err } if !parameters.DryRun { - ctrl.listener.CommittedTransactions(ctx, ctrl.ledger.Name, ret.Transaction, ret.AccountMetadata) + c.handleEvent(ctx, func() { + c.listener.CommittedTransactions(ctx, c.ledger.Name, ret.Transaction, ret.AccountMetadata) + }) } return log, ret, nil } -func (ctrl *ControllerWithEvents) RevertTransaction(ctx context.Context, parameters Parameters[RevertTransaction]) (*ledger.Log, *ledger.RevertedTransaction, error) { - log, ret, err := ctrl.Controller.RevertTransaction(ctx, parameters) +func (c *ControllerWithEvents) RevertTransaction(ctx context.Context, parameters Parameters[RevertTransaction]) (*ledger.Log, *ledger.RevertedTransaction, error) { + log, ret, err := c.Controller.RevertTransaction(ctx, parameters) if err != nil { return nil, nil, err } if !parameters.DryRun { - ctrl.listener.RevertedTransaction( - ctx, - ctrl.ledger.Name, - ret.RevertedTransaction, - ret.RevertedTransaction, - ) + c.handleEvent(ctx, func() { + c.listener.RevertedTransaction( + ctx, + c.ledger.Name, + ret.RevertedTransaction, + ret.RevertedTransaction, + ) + }) } return log, ret, nil } -func (ctrl *ControllerWithEvents) SaveTransactionMetadata(ctx context.Context, parameters Parameters[SaveTransactionMetadata]) (*ledger.Log, error) { - log, err := ctrl.Controller.SaveTransactionMetadata(ctx, parameters) +func (c *ControllerWithEvents) SaveTransactionMetadata(ctx context.Context, parameters Parameters[SaveTransactionMetadata]) (*ledger.Log, error) { + log, err := c.Controller.SaveTransactionMetadata(ctx, parameters) if err != nil { return nil, err } if !parameters.DryRun { - ctrl.listener.SavedMetadata( - ctx, - ctrl.ledger.Name, - ledger.MetaTargetTypeTransaction, - fmt.Sprint(parameters.Input.TransactionID), - parameters.Input.Metadata, - ) + c.handleEvent(ctx, func() { + c.listener.SavedMetadata( + ctx, + c.ledger.Name, + ledger.MetaTargetTypeTransaction, + fmt.Sprint(parameters.Input.TransactionID), + parameters.Input.Metadata, + ) + }) } return log, nil } -func (ctrl *ControllerWithEvents) SaveAccountMetadata(ctx context.Context, parameters Parameters[SaveAccountMetadata]) (*ledger.Log, error) { - log, err := ctrl.Controller.SaveAccountMetadata(ctx, parameters) +func (c *ControllerWithEvents) SaveAccountMetadata(ctx context.Context, parameters Parameters[SaveAccountMetadata]) (*ledger.Log, error) { + log, err := c.Controller.SaveAccountMetadata(ctx, parameters) if err != nil { return nil, err } if !parameters.DryRun { - ctrl.listener.SavedMetadata( - ctx, - ctrl.ledger.Name, - ledger.MetaTargetTypeAccount, - parameters.Input.Address, - parameters.Input.Metadata, - ) + c.handleEvent(ctx, func() { + c.listener.SavedMetadata( + ctx, + c.ledger.Name, + ledger.MetaTargetTypeAccount, + parameters.Input.Address, + parameters.Input.Metadata, + ) + }) } return log, nil } -func (ctrl *ControllerWithEvents) DeleteTransactionMetadata(ctx context.Context, parameters Parameters[DeleteTransactionMetadata]) (*ledger.Log, error) { - log, err := ctrl.Controller.DeleteTransactionMetadata(ctx, parameters) +func (c *ControllerWithEvents) DeleteTransactionMetadata(ctx context.Context, parameters Parameters[DeleteTransactionMetadata]) (*ledger.Log, error) { + log, err := c.Controller.DeleteTransactionMetadata(ctx, parameters) if err != nil { return nil, err } if !parameters.DryRun { - ctrl.listener.DeletedMetadata( - ctx, - ctrl.ledger.Name, - ledger.MetaTargetTypeTransaction, - fmt.Sprint(parameters.Input.TransactionID), - parameters.Input.Key, - ) + c.handleEvent(ctx, func() { + c.listener.DeletedMetadata( + ctx, + c.ledger.Name, + ledger.MetaTargetTypeTransaction, + fmt.Sprint(parameters.Input.TransactionID), + parameters.Input.Key, + ) + }) } return log, nil } -func (ctrl *ControllerWithEvents) DeleteAccountMetadata(ctx context.Context, parameters Parameters[DeleteAccountMetadata]) (*ledger.Log, error) { - log, err := ctrl.Controller.DeleteAccountMetadata(ctx, parameters) +func (c *ControllerWithEvents) DeleteAccountMetadata(ctx context.Context, parameters Parameters[DeleteAccountMetadata]) (*ledger.Log, error) { + log, err := c.Controller.DeleteAccountMetadata(ctx, parameters) if err != nil { return nil, err } if !parameters.DryRun { - ctrl.listener.DeletedMetadata( - ctx, - ctrl.ledger.Name, - ledger.MetaTargetTypeAccount, - parameters.Input.Address, - parameters.Input.Key, - ) + c.handleEvent(ctx, func() { + c.listener.DeletedMetadata( + ctx, + c.ledger.Name, + ledger.MetaTargetTypeAccount, + parameters.Input.Address, + parameters.Input.Key, + ) + }) } return log, nil @@ -130,8 +159,29 @@ func (c *ControllerWithEvents) BeginTX(ctx context.Context, options *sql.TxOptio return &ControllerWithEvents{ ledger: c.ledger, Controller: ctrl, - listener: c.listener, + listener: c.listener, + parent: c, + hasTx: true, }, nil } +func (c *ControllerWithEvents) Commit(ctx context.Context) error { + err := c.Controller.Commit(ctx) + if err != nil { + return err + } + + for _, f := range c.atCommit { + f() + } + + return nil +} + +func (c *ControllerWithEvents) Rollback(ctx context.Context) error { + c.atCommit = nil + + return c.Controller.Rollback(ctx) +} + var _ Controller = (*ControllerWithEvents)(nil) diff --git a/test/e2e/api_bulk_test.go b/test/e2e/api_bulk_test.go index 1bfdfeaa4..fd568860b 100644 --- a/test/e2e/api_bulk_test.go +++ b/test/e2e/api_bulk_test.go @@ -4,6 +4,10 @@ package test_suite import ( "github.com/formancehq/go-libs/v2/pointer" + ledger "github.com/formancehq/ledger/internal" + "github.com/formancehq/ledger/internal/bus" + ledgerevents "github.com/formancehq/ledger/pkg/events" + "github.com/nats-io/nats.go" "math/big" "time" @@ -25,14 +29,16 @@ var _ = Context("Ledger engine tests", func() { numscriptRewrite bool }{ {"default", false}, - //{"numscript rewrite", true}, + {"numscript rewrite", true}, } { Context(data.description, func() { var ( - db = UseTemplatedDatabase() - ctx = logging.TestingContext() - bulkMaxSize = 5 + db = UseTemplatedDatabase() + ctx = logging.TestingContext() + events chan *nats.Msg + bulkResponse []components.V2BulkElementResult + bulkMaxSize = 5 ) testServer := NewTestServer(func() Configuration { @@ -50,12 +56,14 @@ var _ = Context("Ledger engine tests", func() { Ledger: "default", }) Expect(err).To(BeNil()) + events = Subscribe(GinkgoT(), testServer.GetValue()) }) When("creating a bulk on a ledger", func() { var ( - now = time.Now().Round(time.Microsecond).UTC() - items []components.V2BulkElement - err error + now = time.Now().Round(time.Microsecond).UTC() + items []components.V2BulkElement + err error + atomic bool ) BeforeEach(func() { items = []components.V2BulkElement{ @@ -96,12 +104,13 @@ var _ = Context("Ledger engine tests", func() { } }) JustBeforeEach(func() { - _, err = CreateBulk(ctx, testServer.GetValue(), operations.V2CreateBulkRequest{ + bulkResponse, err = CreateBulk(ctx, testServer.GetValue(), operations.V2CreateBulkRequest{ + Atomic: pointer.For(atomic), RequestBody: items, Ledger: "default", }) }) - It("should be ok", func() { + shouldBeOk := func() { Expect(err).To(Succeed()) tx, err := GetTransaction(ctx, testServer.GetValue(), operations.V2GetTransactionRequest{ @@ -131,6 +140,19 @@ var _ = Context("Ledger engine tests", func() { Timestamp: now, InsertedAt: tx.InsertedAt, })) + By("It should send events", func() { + Eventually(events).Should(Receive(Event(ledgerevents.EventTypeCommittedTransactions))) + Eventually(events).Should(Receive(Event(ledgerevents.EventTypeSavedMetadata))) + Eventually(events).Should(Receive(Event(ledgerevents.EventTypeDeletedMetadata))) + Eventually(events).Should(Receive(Event(ledgerevents.EventTypeRevertedTransaction))) + }) + } + It("should be ok", shouldBeOk) + Context("with atomic", func() { + BeforeEach(func() { + atomic = true + }) + It("should be ok", shouldBeOk) }) Context("with exceeded batch size", func() { BeforeEach(func() { @@ -157,10 +179,9 @@ var _ = Context("Ledger engine tests", func() { }) When("creating a bulk with an error on a ledger", func() { var ( - now = time.Now().Round(time.Microsecond).UTC() - err error - bulkResponse []components.V2BulkElementResult - atomic bool + now = time.Now().Round(time.Microsecond).UTC() + err error + atomic bool ) JustBeforeEach(func() { bulkResponse, err = CreateBulk(ctx, testServer.GetValue(), operations.V2CreateBulkRequest{ @@ -218,6 +239,15 @@ var _ = Context("Ledger engine tests", func() { Expect(err).To(Succeed()) Expect(txs.Data).To(HaveLen(1)) }) + + By("Should have sent one event", func() { + Eventually(events).Should(Receive(Event(ledgerevents.EventTypeCommittedTransactions, WithPayload(bus.CommittedTransactions{ + Ledger: "default", + Transactions: []ledger.Transaction{ConvertSDKTxToCoreTX(&bulkResponse[0].V2BulkElementResultCreateTransaction.Data)}, + AccountMetadata: ledger.AccountMetadata{}, + })))) + Eventually(events).ShouldNot(Receive()) + }) }) Context("with atomic", func() { BeforeEach(func() { @@ -231,6 +261,10 @@ var _ = Context("Ledger engine tests", func() { }) Expect(err).To(Succeed()) Expect(txs.Data).To(HaveLen(0)) + + By("Should not have sent any event", func() { + Eventually(events).ShouldNot(Receive()) + }) }) }) }) From e69c795e4056b29c876cf6c8eeab77a025a95c44 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 22 Nov 2024 22:50:54 +0100 Subject: [PATCH 33/71] fix(migrations): with empty references --- go.mod | 2 +- .../14-transaction-reference-index/up.sql | 3 +- internal/storage/driver/driver.go | 38 +++++++++++++++---- internal/storage/driver/driver_test.go | 9 ++--- test/migrations/upgrade_test.go | 23 +++++++++-- 5 files changed, 55 insertions(+), 20 deletions(-) diff --git a/go.mod b/go.mod index 52290d3f2..cf7f1d2b9 100644 --- a/go.mod +++ b/go.mod @@ -161,7 +161,7 @@ require ( github.com/rs/cors v1.11.1 // indirect github.com/shirou/gopsutil/v4 v4.24.10 // indirect github.com/shomali11/util v0.0.0-20220717175126-f0771b70947f // indirect - github.com/sirupsen/logrus v1.9.3 + github.com/sirupsen/logrus v1.9.3 // indirect github.com/tklauser/go-sysconf v0.3.14 // indirect github.com/tklauser/numcpus v0.9.0 // indirect github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect diff --git a/internal/storage/bucket/migrations/14-transaction-reference-index/up.sql b/internal/storage/bucket/migrations/14-transaction-reference-index/up.sql index 98fa61296..ab3e87586 100644 --- a/internal/storage/bucket/migrations/14-transaction-reference-index/up.sql +++ b/internal/storage/bucket/migrations/14-transaction-reference-index/up.sql @@ -1 +1,2 @@ -create unique index concurrently transactions_reference2 on "{{.Schema}}".transactions (ledger, reference); \ No newline at end of file +-- todo: clean empty reference in subsequent migration +create unique index concurrently transactions_reference2 on "{{.Schema}}".transactions (ledger, reference) where reference <> ''; \ No newline at end of file diff --git a/internal/storage/driver/driver.go b/internal/storage/driver/driver.go index 52ca88cba..175af6959 100644 --- a/internal/storage/driver/driver.go +++ b/internal/storage/driver/driver.go @@ -26,12 +26,15 @@ import ( ) type Driver struct { - ledgerStoreFactory ledgerstore.Factory - systemStore systemstore.Store - bucketFactory bucket.Factory - tracer trace.Tracer - meter metric.Meter + ledgerStoreFactory ledgerstore.Factory + systemStore systemstore.Store + bucketFactory bucket.Factory + tracer trace.Tracer + meter metric.Meter + + migrationRetryPeriod time.Duration migratorLockRetryInterval time.Duration + parallelBucketMigrations int } func (d *Driver) CreateLedger(ctx context.Context, l *ledger.Ledger) (*ledgerstore.Store, error) { @@ -155,7 +158,7 @@ func (d *Driver) UpgradeAllBuckets(ctx context.Context, minimalVersionReached ch sem := make(chan struct{}, len(buckets)) - wp := pond.New(10, len(buckets), pond.Context(ctx)) + wp := pond.New(d.parallelBucketMigrations, len(buckets), pond.Context(ctx)) for _, bucketName := range buckets { wp.Submit(func() { @@ -174,7 +177,7 @@ func (d *Driver) UpgradeAllBuckets(ctx context.Context, minimalVersionReached ch go func() { logger.Infof("Upgrading...") errChan <- b.Migrate( - ctx, + logging.ContextWithLogger(ctx, logger), minimalVersionReached, migrations.WithLockRetryInterval(d.migratorLockRetryInterval), ) @@ -188,7 +191,12 @@ func (d *Driver) UpgradeAllBuckets(ctx context.Context, minimalVersionReached ch case err := <-errChan: if err != nil { logger.Errorf("Error upgrading: %s", err) - continue l + select { + case <-time.After(d.migrationRetryPeriod): + continue l + case <-ctx.Done(): + return + } } if sem != nil { logger.Infof("Reached minimal workable version") @@ -263,7 +271,21 @@ func WithMigratorLockRetryInterval(interval time.Duration) Option { } } +func WithParallelBucketMigration(p int) Option { + return func(d *Driver) { + d.parallelBucketMigrations = p + } +} + +func WithMigrationRetryPeriod(p time.Duration) Option { + return func(d *Driver) { + d.migrationRetryPeriod = p + } +} + var defaultOptions = []Option{ WithMeter(noopmetrics.Meter{}), WithTracer(nooptracer.Tracer{}), + WithParallelBucketMigration(10), + WithMigrationRetryPeriod(5 * time.Second), } diff --git a/internal/storage/driver/driver_test.go b/internal/storage/driver/driver_test.go index 6c2578bf4..a0e0a5811 100644 --- a/internal/storage/driver/driver_test.go +++ b/internal/storage/driver/driver_test.go @@ -14,7 +14,6 @@ import ( "github.com/formancehq/ledger/internal/storage/driver" ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" "github.com/google/uuid" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" "testing" @@ -113,16 +112,14 @@ func TestUpgradeAllLedgers(t *testing.T) { t.Run("and error", func(t *testing.T) { t.Parallel() - //ctx := context.Background() - - ctx := logging.ContextWithLogger(ctx, logging.NewLogrus(logrus.New())) - ctrl := gomock.NewController(t) ledgerStoreFactory := driver.NewLedgerStoreFactory(ctrl) bucketFactory := driver.NewBucketFactory(ctrl) systemStore := driver.NewSystemStore(ctrl) - d := driver.New(ledgerStoreFactory, systemStore, bucketFactory) + d := driver.New(ledgerStoreFactory, systemStore, bucketFactory, + driver.WithMigrationRetryPeriod(10*time.Millisecond), + ) bucket1 := driver.NewMockBucket(ctrl) bucket2 := driver.NewMockBucket(ctrl) diff --git a/test/migrations/upgrade_test.go b/test/migrations/upgrade_test.go index a4ef5ccf8..d43dbf7e0 100644 --- a/test/migrations/upgrade_test.go +++ b/test/migrations/upgrade_test.go @@ -23,20 +23,21 @@ import ( var ( sourceDatabase string destinationDatabase string + skipCopy bool + skipMigrate bool ) func TestMain(m *testing.M) { flag.StringVar(&sourceDatabase, "databases.source", "", "Source database") flag.StringVar(&destinationDatabase, "databases.destination", "", "Destination database") + flag.BoolVar(&skipCopy, "skip-copy", false, "Skip copying database") + flag.BoolVar(&skipMigrate, "skip-migrate", false, "Skip migrating database") flag.Parse() os.Exit(m.Run()) } func TestMigrations(t *testing.T) { - if sourceDatabase == "" { - t.Skip() - } ctx := logging.TestingContext() dockerPool := docker.NewPool(t, logging.Testing()) @@ -46,18 +47,30 @@ func TestMigrations(t *testing.T) { destinationDatabase = pgServer.GetDSN() } - copyDatabase(t, dockerPool, sourceDatabase, destinationDatabase) + if !skipCopy { + if sourceDatabase == "" { + t.Skip() + } + + copyDatabase(t, dockerPool, sourceDatabase, destinationDatabase) + fmt.Println("Database copied") + } db, err := bunconnect.OpenSQLDB(ctx, bunconnect.ConnectionOptions{ DatabaseSourceName: destinationDatabase, }) require.NoError(t, err) + if skipMigrate { + return + } + // Migrate database driver := driver.New( ledger.NewFactory(db), systemstore.New(db), bucket.NewDefaultFactory(db), + driver.WithParallelBucketMigration(1), ) require.NoError(t, driver.Initialize(ctx)) require.NoError(t, driver.UpgradeAllBuckets(ctx, make(chan struct{}))) @@ -111,6 +124,7 @@ func preparePGDumpCommand(t *testing.T, dsn string) string { "-x", // Skip privileges "-h", parsedSource.Hostname(), "-p", parsedSource.Port(), + "-v", ) if username := parsedSource.User.Username(); username != "" { @@ -133,6 +147,7 @@ func preparePSQLCommand(t *testing.T, dsn string) string { args = append(args, "psql", + "--echo-all", "-h", parsedSource.Hostname(), "-p", parsedSource.Port(), parsedSource.Path[1:], From d84cc02bc20152a5a94424107b539a5b3519cc42 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Sat, 23 Nov 2024 13:26:47 +0100 Subject: [PATCH 34/71] feat(performance): batch accounts insertions --- .../controller/ledger/controller_default.go | 2 +- internal/controller/ledger/store.go | 2 +- .../controller/ledger/store_generated_test.go | 22 +++--- internal/storage/ledger/accounts.go | 68 +++++++------------ internal/storage/ledger/accounts_test.go | 25 ++++--- internal/storage/ledger/balances_test.go | 4 +- internal/storage/ledger/legacy/adapters.go | 4 +- internal/storage/ledger/moves_test.go | 2 +- internal/storage/ledger/store.go | 8 +-- internal/storage/ledger/transactions.go | 2 +- 10 files changed, 65 insertions(+), 74 deletions(-) diff --git a/internal/controller/ledger/controller_default.go b/internal/controller/ledger/controller_default.go index ae114e6c4..a8a3f26ef 100644 --- a/internal/controller/ledger/controller_default.go +++ b/internal/controller/ledger/controller_default.go @@ -444,7 +444,7 @@ func (ctrl *DefaultController) SaveTransactionMetadata(ctx context.Context, para } func (ctrl *DefaultController) saveAccountMetadata(ctx context.Context, store Store, parameters Parameters[SaveAccountMetadata]) (*ledger.SavedMetadata, error) { - if _, err := store.UpsertAccount(ctx, &ledger.Account{ + if err := store.UpsertAccounts(ctx, &ledger.Account{ Address: parameters.Input.Address, Metadata: parameters.Input.Metadata, }); err != nil { diff --git a/internal/controller/ledger/store.go b/internal/controller/ledger/store.go index c442293fe..9a4579419 100644 --- a/internal/controller/ledger/store.go +++ b/internal/controller/ledger/store.go @@ -46,7 +46,7 @@ type Store interface { DeleteTransactionMetadata(ctx context.Context, transactionID int, key string) (*ledger.Transaction, bool, error) UpdateAccountsMetadata(ctx context.Context, m map[string]metadata.Metadata) error // UpsertAccount returns a boolean indicating if the account was upserted - UpsertAccount(ctx context.Context, account *ledger.Account) (bool, error) + UpsertAccounts(ctx context.Context, accounts ...*ledger.Account) error DeleteAccountMetadata(ctx context.Context, address, key string) error InsertLog(ctx context.Context, log *ledger.Log) error diff --git a/internal/controller/ledger/store_generated_test.go b/internal/controller/ledger/store_generated_test.go index ff5d1465d..0244559a7 100644 --- a/internal/controller/ledger/store_generated_test.go +++ b/internal/controller/ledger/store_generated_test.go @@ -412,17 +412,21 @@ func (mr *MockStoreMockRecorder) UpdateTransactionMetadata(ctx, transactionID, m return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTransactionMetadata", reflect.TypeOf((*MockStore)(nil).UpdateTransactionMetadata), ctx, transactionID, m) } -// UpsertAccount mocks base method. -func (m *MockStore) UpsertAccount(ctx context.Context, account *ledger.Account) (bool, error) { +// UpsertAccounts mocks base method. +func (m *MockStore) UpsertAccounts(ctx context.Context, accounts ...*ledger.Account) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpsertAccount", ctx, account) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 + varargs := []any{ctx} + for _, a := range accounts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "UpsertAccounts", varargs...) + ret0, _ := ret[0].(error) + return ret0 } -// UpsertAccount indicates an expected call of UpsertAccount. -func (mr *MockStoreMockRecorder) UpsertAccount(ctx, account any) *gomock.Call { +// UpsertAccounts indicates an expected call of UpsertAccounts. +func (mr *MockStoreMockRecorder) UpsertAccounts(ctx any, accounts ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertAccount", reflect.TypeOf((*MockStore)(nil).UpsertAccount), ctx, account) + varargs := append([]any{ctx}, accounts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertAccounts", reflect.TypeOf((*MockStore)(nil).UpsertAccounts), varargs...) } diff --git a/internal/storage/ledger/accounts.go b/internal/storage/ledger/accounts.go index 1e34ca469..6b1972ce3 100644 --- a/internal/storage/ledger/accounts.go +++ b/internal/storage/ledger/accounts.go @@ -2,7 +2,6 @@ package ledger import ( "context" - "database/sql" "fmt" . "github.com/formancehq/go-libs/v2/bun/bunpaginate" "github.com/formancehq/ledger/pkg/features" @@ -12,11 +11,8 @@ import ( "github.com/formancehq/go-libs/v2/metadata" "github.com/formancehq/go-libs/v2/platform/postgres" - ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" - "github.com/formancehq/go-libs/v2/time" + ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" "github.com/formancehq/go-libs/v2/query" ledger "github.com/formancehq/ledger/internal" @@ -333,45 +329,29 @@ func (s *Store) DeleteAccountMetadata(ctx context.Context, account, key string) return err } -// todo: since we update first balances of an accounts in the transaction process, we can avoid nested sql txs -// while upserting account and upsert them all in one shot -func (s *Store) UpsertAccount(ctx context.Context, account *ledger.Account) (bool, error) { - return tracing.TraceWithMetric( +func (s *Store) UpsertAccounts(ctx context.Context, accounts ...*ledger.Account) error { + return tracing.SkipResult(tracing.TraceWithMetric( ctx, - "UpsertAccount", + "UpsertAccounts", s.tracer, - s.upsertAccountHistogram, - func(ctx context.Context) (bool, error) { - upserted := false - err := s.db.RunInTx(ctx, &sql.TxOptions{}, func(ctx context.Context, tx bun.Tx) error { - ret, err := tx.NewInsert(). - Model(account). - ModelTableExpr(s.GetPrefixedRelationName("accounts")). - On("conflict (ledger, address) do update"). - Set("first_usage = case when ? < excluded.first_usage then ? else excluded.first_usage end", account.FirstUsage, account.FirstUsage). - Set("metadata = accounts.metadata || excluded.metadata"). - Set("updated_at = excluded.updated_at"). - Value("ledger", "?", s.ledger.Name). - Returning("*"). - Where("(? < accounts.first_usage) or not accounts.metadata @> excluded.metadata", account.FirstUsage). - Exec(ctx) - if err != nil { - return err - } - rowsModified, err := ret.RowsAffected() - if err != nil { - return err - } - upserted = rowsModified > 0 - return nil - }) - return upserted, postgres.ResolveError(err) - }, - func(ctx context.Context, upserted bool) { - trace.SpanFromContext(ctx).SetAttributes( - attribute.String("address", account.Address), - attribute.Bool("upserted", upserted), - ) - }, - ) + s.upsertAccountsHistogram, + tracing.NoResult(func(ctx context.Context) error { + _, err := s.db.NewInsert(). + Model(&accounts). + ModelTableExpr(s.GetPrefixedRelationName("accounts")). + On("conflict (ledger, address) do update"). + Set("first_usage = case when excluded.first_usage < accounts.first_usage then excluded.first_usage else accounts.first_usage end"). + Set("metadata = accounts.metadata || excluded.metadata"). + Set("updated_at = excluded.updated_at"). + Value("ledger", "?", s.ledger.Name). + Returning("*"). + Where("(excluded.first_usage < accounts.first_usage) or not accounts.metadata @> excluded.metadata"). + Exec(ctx) + if err != nil { + return fmt.Errorf("upserting accounts: %w", postgres.ResolveError(err)) + } + + return nil + }), + )) } diff --git a/internal/storage/ledger/accounts_test.go b/internal/storage/ledger/accounts_test.go index 5277f5fba..ef0c2f6d1 100644 --- a/internal/storage/ledger/accounts_test.go +++ b/internal/storage/ledger/accounts_test.go @@ -402,22 +402,30 @@ func TestAccountsUpsert(t *testing.T) { store := newLedgerStore(t) ctx := logging.TestingContext() - account := ledger.Account{ + account1 := ledger.Account{ Address: "foo", } + account2 := ledger.Account{ + Address: "foo2", + } + // Initial insert - upserted, err := store.UpsertAccount(ctx, &account) + err := store.UpsertAccounts(ctx, &account1, &account2) require.NoError(t, err) - require.True(t, upserted) - require.NotEmpty(t, account.FirstUsage) - require.NotEmpty(t, account.InsertionDate) - require.NotEmpty(t, account.UpdatedAt) + + require.NotEmpty(t, account1.FirstUsage) + require.NotEmpty(t, account1.InsertionDate) + require.NotEmpty(t, account1.UpdatedAt) + + require.NotEmpty(t, account2.FirstUsage) + require.NotEmpty(t, account2.InsertionDate) + require.NotEmpty(t, account2.UpdatedAt) now := time.Now() // Reset the account model - account = ledger.Account{ + account1 = ledger.Account{ Address: "foo", // The account will be upserted on the timeline after its initial usage. // The upsert should not modify anything, but, it should retrieve and load the account entity @@ -427,7 +435,6 @@ func TestAccountsUpsert(t *testing.T) { } // Upsert with no modification - upserted, err = store.UpsertAccount(ctx, &account) + err = store.UpsertAccounts(ctx, &account1) require.NoError(t, err) - require.False(t, upserted) } diff --git a/internal/storage/ledger/balances_test.go b/internal/storage/ledger/balances_test.go index 24a6c9bbc..5b32c9aaa 100644 --- a/internal/storage/ledger/balances_test.go +++ b/internal/storage/ledger/balances_test.go @@ -33,7 +33,7 @@ func TestBalancesGet(t *testing.T) { UpdatedAt: time.Now(), FirstUsage: time.Now(), } - _, err := store.UpsertAccount(ctx, world) + err := store.UpsertAccounts(ctx, world) require.NoError(t, err) _, err = store.UpdateVolumes(ctx, ledger.AccountsVolumes{ @@ -146,7 +146,7 @@ func TestBalancesGet(t *testing.T) { InsertionDate: tx.InsertedAt, UpdatedAt: tx.InsertedAt, } - _, err = store.UpsertAccount(ctx, &bankAccount) + err = store.UpsertAccounts(ctx, &bankAccount) require.NoError(t, err) err = store.InsertMoves(ctx, &ledger.Move{ diff --git a/internal/storage/ledger/legacy/adapters.go b/internal/storage/ledger/legacy/adapters.go index cb0d20eb3..807bb1678 100644 --- a/internal/storage/ledger/legacy/adapters.go +++ b/internal/storage/ledger/legacy/adapters.go @@ -46,8 +46,8 @@ func (d *DefaultStoreAdapter) UpdateAccountsMetadata(ctx context.Context, m map[ return d.newStore.UpdateAccountsMetadata(ctx, m) } -func (d *DefaultStoreAdapter) UpsertAccount(ctx context.Context, account *ledger.Account) (bool, error) { - return d.newStore.UpsertAccount(ctx, account) +func (d *DefaultStoreAdapter) UpsertAccounts(ctx context.Context, accounts ... *ledger.Account) error { + return d.newStore.UpsertAccounts(ctx, accounts...) } func (d *DefaultStoreAdapter) DeleteAccountMetadata(ctx context.Context, address, key string) error { diff --git a/internal/storage/ledger/moves_test.go b/internal/storage/ledger/moves_test.go index e667ee8de..02ace80c4 100644 --- a/internal/storage/ledger/moves_test.go +++ b/internal/storage/ledger/moves_test.go @@ -38,7 +38,7 @@ func TestMovesInsert(t *testing.T) { account := &ledger.Account{ Address: "world", } - _, err := store.UpsertAccount(ctx, account) + err := store.UpsertAccounts(ctx, account) require.NoError(t, err) now := time.Now() diff --git a/internal/storage/ledger/store.go b/internal/storage/ledger/store.go index 87bcfeb44..b15a0eafc 100644 --- a/internal/storage/ledger/store.go +++ b/internal/storage/ledger/store.go @@ -31,9 +31,9 @@ type Store struct { getAccountHistogram metric.Int64Histogram countAccountsHistogram metric.Int64Histogram updateAccountsMetadataHistogram metric.Int64Histogram - deleteAccountMetadataHistogram metric.Int64Histogram - upsertAccountHistogram metric.Int64Histogram - getBalancesHistogram metric.Int64Histogram + deleteAccountMetadataHistogram metric.Int64Histogram + upsertAccountsHistogram metric.Int64Histogram + getBalancesHistogram metric.Int64Histogram insertLogHistogram metric.Int64Histogram listLogsHistogram metric.Int64Histogram readLogWithIdempotencyKeyHistogram metric.Int64Histogram @@ -154,7 +154,7 @@ func New(db bun.IDB, bucket bucket.Bucket, ledger ledger.Ledger, opts ...Option) panic(err) } - ret.upsertAccountHistogram, err = ret.meter.Int64Histogram("store.upsertAccount") + ret.upsertAccountsHistogram, err = ret.meter.Int64Histogram("store.upsertAccounts") if err != nil { panic(err) } diff --git a/internal/storage/ledger/transactions.go b/internal/storage/ledger/transactions.go index 3626c5dc1..4600a9642 100644 --- a/internal/storage/ledger/transactions.go +++ b/internal/storage/ledger/transactions.go @@ -255,7 +255,7 @@ func (s *Store) CommitTransaction(ctx context.Context, tx *ledger.Transaction) e } for _, address := range tx.InvolvedAccounts() { - _, err := s.UpsertAccount(ctx, &ledger.Account{ + err := s.UpsertAccounts(ctx, &ledger.Account{ Address: address, FirstUsage: tx.Timestamp, Metadata: make(metadata.Metadata), From 7300c7ad87eba71dc44fd537b89989805e2878e8 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Sat, 23 Nov 2024 14:24:41 +0100 Subject: [PATCH 35/71] feat: add some benchmarking option --- internal/storage/ledger/transactions.go | 12 ++--- pkg/generate/generator.go | 58 ++++++++++++++++++++++--- test/performance/benchmark_test.go | 26 ++++++----- test/performance/write_test.go | 10 ++++- 4 files changed, 82 insertions(+), 24 deletions(-) diff --git a/internal/storage/ledger/transactions.go b/internal/storage/ledger/transactions.go index 4600a9642..17f787705 100644 --- a/internal/storage/ledger/transactions.go +++ b/internal/storage/ledger/transactions.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/formancehq/go-libs/v2/collectionutils" "github.com/formancehq/ledger/pkg/features" "math/big" "regexp" @@ -254,15 +255,15 @@ func (s *Store) CommitTransaction(ctx context.Context, tx *ledger.Transaction) e return fmt.Errorf("failed to insert transaction: %w", err) } - for _, address := range tx.InvolvedAccounts() { - err := s.UpsertAccounts(ctx, &ledger.Account{ + err = s.UpsertAccounts(ctx, collectionutils.Map(tx.InvolvedAccounts(), func(address string) *ledger.Account { + return &ledger.Account{ Address: address, FirstUsage: tx.Timestamp, Metadata: make(metadata.Metadata), - }) - if err != nil { - return fmt.Errorf("upserting account: %w", err) } + })...) + if err != nil { + return fmt.Errorf("upserting accounts: %w", err) } if s.ledger.HasFeature(features.FeatureMovesHistory, "ON") { @@ -302,7 +303,6 @@ func (s *Store) CommitTransaction(ctx context.Context, tx *ledger.Transaction) e } if s.ledger.HasFeature(features.FeatureMovesHistoryPostCommitEffectiveVolumes, "SYNC") { - // todo: tx is inserted earlier! tx.PostCommitEffectiveVolumes = moves.ComputePostCommitEffectiveVolumes() } } diff --git a/pkg/generate/generator.go b/pkg/generate/generator.go index a71fd7bec..45adb6ea5 100644 --- a/pkg/generate/generator.go +++ b/pkg/generate/generator.go @@ -14,6 +14,8 @@ import ( "github.com/formancehq/ledger/pkg/client/models/operations" "github.com/google/uuid" "math/big" + "os" + "path/filepath" "time" ) @@ -185,12 +187,24 @@ func (r Action) Apply(ctx context.Context, client *client.V2, l string) (*Result return &Result{response.V2BulkResponse.Data[0]}, nil } +type NextOptions struct { + Globals map[string]any +} + +type NextOption func(options *NextOptions) + +func WithNextGlobals(globals map[string]any) NextOption { + return func(options *NextOptions) { + options.Globals = globals + } +} + type Generator struct { - next func(int) (*Action, error) + next func(int, ...NextOption) (*Action, error) } -func (g *Generator) Next(iteration int) (*Action, error) { - return g.next(iteration) +func (g *Generator) Next(iteration int, options ...NextOption) (*Action, error) { + return g.next(iteration, options...) } func NewGenerator(script string, opts ...Option) (*Generator, error) { @@ -221,6 +235,19 @@ func NewGenerator(script string, opts ...Option) (*Generator, error) { return nil, err } + err = runtime.Set("read_file", func(path string) string { + fmt.Println("read file", path) + f, err := os.ReadFile(filepath.Join(cfg.rootPath, path)) + if err != nil { + panic(err) + } + + return string(f) + }) + if err != nil { + return nil, err + } + var next func(int) map[string]any err = runtime.ExportTo(runtime.Get("next"), &next) if err != nil { @@ -228,7 +255,21 @@ func NewGenerator(script string, opts ...Option) (*Generator, error) { } return &Generator{ - next: func(i int) (*Action, error) { + next: func(i int, options ...NextOption) (*Action, error) { + + nextOptions := NextOptions{} + for _, option := range options { + option(&nextOptions) + } + + if nextOptions.Globals != nil { + for k, v := range nextOptions.Globals { + if err := runtime.Set(k, v); err != nil { + return nil, fmt.Errorf("failed to set global variable %s: %w", k, err) + } + } + } + ret := next(i) var ( @@ -281,7 +322,8 @@ func NewGenerator(script string, opts ...Option) (*Generator, error) { } type config struct { - globals map[string]any + globals map[string]any + rootPath string } type Option func(*config) @@ -291,3 +333,9 @@ func WithGlobals(globals map[string]any) Option { c.globals = globals } } + +func WithRootPath(path string) Option { + return func(c *config) { + c.rootPath = path + } +} diff --git a/test/performance/benchmark_test.go b/test/performance/benchmark_test.go index 8f3f6c5d4..55df38f2c 100644 --- a/test/performance/benchmark_test.go +++ b/test/performance/benchmark_test.go @@ -18,12 +18,12 @@ import ( ) type ActionProvider interface { - Get(iteration int) (*generate.Action, error) + Get(globalIteration, iteration int) (*generate.Action, error) } -type ActionProviderFn func(iteration int) (*generate.Action, error) +type ActionProviderFn func(globalIteration, iteration int) (*generate.Action, error) -func (fn ActionProviderFn) Get(iteration int) (*generate.Action, error) { - return fn(iteration) +func (fn ActionProviderFn) Get(globalIteration, iteration int) (*generate.Action, error) { + return fn(globalIteration, iteration) } type ActionProviderFactory interface { @@ -36,15 +36,17 @@ func (fn ActionProviderFactoryFn) Create() (ActionProvider, error) { return fn() } -func NewJSActionProviderFactory(script string) ActionProviderFactoryFn { +func NewJSActionProviderFactory(rootPath, script string) ActionProviderFactoryFn { return func() (ActionProvider, error) { - generator, err := generate.NewGenerator(script) + generator, err := generate.NewGenerator(script, generate.WithRootPath(rootPath)) if err != nil { return nil, err } - return ActionProviderFn(func(iteration int) (*generate.Action, error) { - return generator.Next(iteration) + return ActionProviderFn(func(globalIteration, iteration int) (*generate.Action, error) { + return generator.Next(iteration, generate.WithNextGlobals(map[string]any{ + "iteration": globalIteration, + })) }), nil } } @@ -82,7 +84,7 @@ func (benchmark *Benchmark) Run(ctx context.Context) map[string][]Result { Name: uuid.NewString()[:8], } - cpt := atomic.Int64{} + globalIteration := atomic.Int64{} env := envFactory.Create(ctx, b, l) b.Logf("ledger: %s/%s", l.Bucket, l.Name) @@ -94,11 +96,13 @@ func (benchmark *Benchmark) Run(ctx context.Context) map[string][]Result { actionProvider, err := benchmark.Scenarios[scenario].Create() require.NoError(b, err) + iteration := atomic.Int64{} for pb.Next() { - iteration := int(cpt.Add(1)) + globalIteration := int(globalIteration.Add(1)) + iteration := int(iteration.Add(1)) - action, err := actionProvider.Get(iteration) + action, err := actionProvider.Get(globalIteration, iteration) require.NoError(b, err) now := time.Now() diff --git a/test/performance/write_test.go b/test/performance/write_test.go index a52c03621..d8a200254 100644 --- a/test/performance/write_test.go +++ b/test/performance/write_test.go @@ -104,13 +104,19 @@ func BenchmarkWrite(b *testing.B) { script, err := scriptsDir.ReadFile(filepath.Join("scripts", entry.Name())) require.NoError(b, err) - scripts[strings.TrimSuffix(entry.Name(), ".js")] = NewJSActionProviderFactory(string(script)) + rootPath, err := filepath.Abs("scripts") + require.NoError(b, err) + + scripts[strings.TrimSuffix(entry.Name(), ".js")] = NewJSActionProviderFactory(rootPath, string(script)) } } else { file, err := os.ReadFile(scriptFlag) require.NoError(b, err, "reading file "+scriptFlag) - scripts["provided"] = NewJSActionProviderFactory(string(file)) + rootPath, err := filepath.Abs(filepath.Dir(scriptFlag)) + require.NoError(b, err) + + scripts["provided"] = NewJSActionProviderFactory(rootPath, string(file)) } if envFactory == nil { From 1f431e868f1566d7e0d023112a0a473ce42f1923 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Sat, 23 Nov 2024 15:11:21 +0100 Subject: [PATCH 36/71] feat: add bulk options --- internal/api/v2/controllers_bulk.go | 4 ++-- internal/controller/ledger/bulker.go | 34 ++++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/internal/api/v2/controllers_bulk.go b/internal/api/v2/controllers_bulk.go index b3a66151d..b246ec82c 100644 --- a/internal/api/v2/controllers_bulk.go +++ b/internal/api/v2/controllers_bulk.go @@ -31,8 +31,8 @@ func bulkHandler(bulkMaxSize int) http.HandlerFunc { bulker := ledgercontroller.NewBulker(ledgerController) results, err := bulker.Run(r.Context(), b, - api.QueryParamBool(r, "continueOnFailure"), - api.QueryParamBool(r, "atomic"), + ledgercontroller.WithContinueOnFailure(api.QueryParamBool(r, "continueOnFailure")), + ledgercontroller.WithAtomic(api.QueryParamBool(r, "atomic")), ) if err != nil { api.InternalServerError(w, r, err) diff --git a/internal/controller/ledger/bulker.go b/internal/controller/ledger/bulker.go index a1d79b2ac..bd0078908 100644 --- a/internal/controller/ledger/bulker.go +++ b/internal/controller/ledger/bulker.go @@ -126,7 +126,12 @@ func (b *Bulker) run(ctx context.Context, ctrl Controller, bulk Bulk, continueOn return results, nil } -func (b *Bulker) Run(ctx context.Context, bulk Bulk, continueOnFailure, atomic bool) (BulkResult, error) { +func (b *Bulker) Run(ctx context.Context, bulk Bulk, providedOptions ... BulkOption) (BulkResult, error) { + + bulkOptions := BulkOptions{} + for _, option := range providedOptions { + option(&bulkOptions) + } for i, element := range bulk { switch element.Action { @@ -154,7 +159,7 @@ func (b *Bulker) Run(ctx context.Context, bulk Bulk, continueOnFailure, atomic b } ctrl := b.ctrl - if atomic { + if bulkOptions.Atomic { var err error ctrl, err = ctrl.BeginTX(ctx, nil) if err != nil { @@ -162,9 +167,9 @@ func (b *Bulker) Run(ctx context.Context, bulk Bulk, continueOnFailure, atomic b } } - results, err := b.run(ctx, ctrl, bulk, continueOnFailure) + results, err := b.run(ctx, ctrl, bulk, bulkOptions.ContinueOnFailure) if err != nil { - if atomic { + if bulkOptions.Atomic { if rollbackErr := ctrl.Rollback(ctx); rollbackErr != nil { logging.FromContext(ctx).Errorf("failed to rollback transaction: %v", rollbackErr) } @@ -173,7 +178,7 @@ func (b *Bulker) Run(ctx context.Context, bulk Bulk, continueOnFailure, atomic b return nil, fmt.Errorf("error running bulk: %s", err) } - if atomic { + if bulkOptions.Atomic { if results.HasErrors() { if rollbackErr := ctrl.Rollback(ctx); rollbackErr != nil { logging.FromContext(ctx).Errorf("failed to rollback transaction: %v", rollbackErr) @@ -331,3 +336,22 @@ func (b *Bulker) processElement(ctx context.Context, ctrl Controller, element Bu func NewBulker(ctrl Controller) *Bulker { return &Bulker{ctrl: ctrl} } + +type BulkOptions struct { + ContinueOnFailure bool + Atomic bool +} + +type BulkOption func(*BulkOptions) + +func WithContinueOnFailure(v bool) BulkOption { + return func(options *BulkOptions) { + options.ContinueOnFailure = v + } +} + +func WithAtomic(v bool) BulkOption { + return func(options *BulkOptions) { + options.Atomic = v + } +} \ No newline at end of file From 6bf16a60eae57867a27de39f9707cd3e65dba2ba Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Sat, 23 Nov 2024 15:52:45 +0100 Subject: [PATCH 37/71] feat: add parallel bulk --- cmd/serve.go | 16 ++- internal/api/module.go | 13 ++- internal/api/router.go | 13 ++- internal/api/v2/controllers_bulk.go | 8 +- internal/api/v2/controllers_bulk_test.go | 4 + internal/api/v2/routes.go | 11 +- internal/controller/ledger/bulker.go | 125 ++++++++++++++++++----- 7 files changed, 150 insertions(+), 40 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 1489d3a2c..c4a050763 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -41,6 +41,7 @@ const ( AutoUpgradeFlag = "auto-upgrade" ExperimentalFeaturesFlag = "experimental-features" BulkMaxSizeFlag = "bulk-max-size" + BulkParallelFlag = "bulk-parallel" NumscriptInterpreterFlag = "experimental-numscript-interpreter" ) @@ -67,6 +68,11 @@ func NewServeCommand() *cobra.Command { return err } + bulkParallel, err := cmd.Flags().GetInt(BulkParallelFlag) + if err != nil { + return err + } + options := []fx.Option{ fx.NopLogger, otlp.FXModuleFromFlags(cmd), @@ -90,9 +96,12 @@ func NewServeCommand() *cobra.Command { bus.NewFxModule(), ballast.Module(serveConfiguration.ballastSize), api.Module(api.Config{ - Version: Version, - Debug: service.IsDebug(cmd), - BulkMaxSize: bulkMaxSize, + Version: Version, + Debug: service.IsDebug(cmd), + Bulk: api.BulkConfig{ + MaxSize: bulkMaxSize, + Parallel: bulkParallel, + }, }), fx.Decorate(func( params struct { @@ -129,6 +138,7 @@ func NewServeCommand() *cobra.Command { cmd.Flags().String(BindFlag, "0.0.0.0:3068", "API bind address") cmd.Flags().Bool(ExperimentalFeaturesFlag, false, "Enable features configurability") cmd.Flags().Int(BulkMaxSizeFlag, api.DefaultBulkMaxSize, "Bulk max size (default 100)") + cmd.Flags().Int(BulkParallelFlag, 10, "Bulk max parallelism") cmd.Flags().Bool(NumscriptInterpreterFlag, false, "Enable experimental numscript rewrite") service.AddFlags(cmd.Flags()) diff --git a/internal/api/module.go b/internal/api/module.go index 45a144452..d890e634f 100644 --- a/internal/api/module.go +++ b/internal/api/module.go @@ -4,16 +4,22 @@ import ( _ "embed" "github.com/formancehq/go-libs/v2/auth" "github.com/formancehq/go-libs/v2/health" + "github.com/formancehq/ledger/internal/controller/ledger" "github.com/formancehq/ledger/internal/controller/system" "github.com/go-chi/chi/v5" "go.opentelemetry.io/otel/trace" "go.uber.org/fx" ) +type BulkConfig struct { + MaxSize int + Parallel int +} + type Config struct { Version string Debug bool - BulkMaxSize int + Bulk BulkConfig } func Module(cfg Config) fx.Option { @@ -29,7 +35,10 @@ func Module(cfg Config) fx.Option { "develop", cfg.Debug, WithTracer(tracer.Tracer("api")), - WithBulkMaxSize(cfg.BulkMaxSize), + WithBulkMaxSize(cfg.Bulk.MaxSize), + WithBulkerFactory(ledger.NewDefaultBulkerFactory( + ledger.WithParallelism(cfg.Bulk.Parallel), + )), ) }), health.Module(), diff --git a/internal/api/router.go b/internal/api/router.go index a8407ac79..7c66e84a4 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -2,6 +2,7 @@ package api import ( "github.com/formancehq/go-libs/v2/api" + ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" "github.com/formancehq/ledger/internal/controller/system" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -66,6 +67,7 @@ func NewRouter( v2.WithTracer(routerOptions.tracer), v2.WithMiddlewares(commonMiddlewares...), v2.WithBulkMaxSize(routerOptions.bulkMaxSize), + v2.WithBulkerFactory(routerOptions.bulkerFactory), ) mux.Handle("/v2*", http.StripPrefix("/v2", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { chi.RouteContext(r.Context()).Reset() @@ -84,8 +86,9 @@ func NewRouter( } type routerOptions struct { - tracer trace.Tracer - bulkMaxSize int + tracer trace.Tracer + bulkMaxSize int + bulkerFactory ledgercontroller.BulkerFactory } type RouterOption func(ro *routerOptions) @@ -102,6 +105,12 @@ func WithBulkMaxSize(bulkMaxSize int) RouterOption { } } +func WithBulkerFactory(bf ledgercontroller.BulkerFactory) RouterOption { + return func(ro *routerOptions) { + ro.bulkerFactory = bf + } +} + var defaultRouterOptions = []RouterOption{ WithTracer(nooptracer.Tracer{}), WithBulkMaxSize(DefaultBulkMaxSize), diff --git a/internal/api/v2/controllers_bulk.go b/internal/api/v2/controllers_bulk.go index b246ec82c..2fc248e94 100644 --- a/internal/api/v2/controllers_bulk.go +++ b/internal/api/v2/controllers_bulk.go @@ -12,7 +12,7 @@ import ( ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" ) -func bulkHandler(bulkMaxSize int) http.HandlerFunc { +func bulkHandler(bulkerFactory ledgercontroller.BulkerFactory, bulkMaxSize int) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { b := ledgercontroller.Bulk{} if err := json.NewDecoder(r.Body).Decode(&b); err != nil { @@ -28,11 +28,11 @@ func bulkHandler(bulkMaxSize int) http.HandlerFunc { w.Header().Set("Content-Type", "application/json") ledgerController := common.LedgerFromContext(r.Context()) - bulker := ledgercontroller.NewBulker(ledgerController) - results, err := bulker.Run(r.Context(), - b, + + results, err := bulkerFactory.CreateBulker(ledgerController).Run(r.Context(), b, ledgercontroller.WithContinueOnFailure(api.QueryParamBool(r, "continueOnFailure")), ledgercontroller.WithAtomic(api.QueryParamBool(r, "atomic")), + ledgercontroller.WithParallel(api.QueryParamBool(r, "parallel")), ) if err != nil { api.InternalServerError(w, r, err) diff --git a/internal/api/v2/controllers_bulk_test.go b/internal/api/v2/controllers_bulk_test.go index b94eae76f..cea9bb27a 100644 --- a/internal/api/v2/controllers_bulk_test.go +++ b/internal/api/v2/controllers_bulk_test.go @@ -265,6 +265,10 @@ func TestBulk(t *testing.T) { ErrorCode: api.ErrorInternal, ErrorDescription: "unexpected error", ResponseType: "ERROR", + }, { + ErrorCode: api.ErrorInternal, + ErrorDescription: "canceled", + ResponseType: "ERROR", }}, expectError: true, }, diff --git a/internal/api/v2/routes.go b/internal/api/v2/routes.go index 0bc4394b7..eea3ef656 100644 --- a/internal/api/v2/routes.go +++ b/internal/api/v2/routes.go @@ -1,6 +1,7 @@ package v2 import ( + "github.com/formancehq/ledger/internal/controller/ledger" nooptracer "go.opentelemetry.io/otel/trace/noop" "net/http" @@ -52,7 +53,7 @@ func NewRouter( router.With(common.LedgerMiddleware(systemController, func(r *http.Request) string { return chi.URLParam(r, "ledger") }, routerOptions.tracer, "/_info")).Group(func(router chi.Router) { - router.Post("/_bulk", bulkHandler(routerOptions.bulkMaxSize)) + router.Post("/_bulk", bulkHandler(routerOptions.bulkerFactory, routerOptions.bulkMaxSize)) // LedgerController router.Get("/_info", getLedgerInfo) @@ -92,6 +93,7 @@ func NewRouter( type routerOptions struct { tracer trace.Tracer middlewares []func(http.Handler) http.Handler + bulkerFactory ledger.BulkerFactory bulkMaxSize int } @@ -115,6 +117,13 @@ func WithBulkMaxSize(bulkMaxSize int) RouterOption { } } +func WithBulkerFactory(bulkerFactory ledger.BulkerFactory) RouterOption { + return func(ro *routerOptions) { + ro.bulkerFactory = bulkerFactory + } +} + var defaultRouterOptions = []RouterOption{ WithTracer(nooptracer.Tracer{}), + WithBulkerFactory(ledger.NewDefaultBulkerFactory()), } diff --git a/internal/controller/ledger/bulker.go b/internal/controller/ledger/bulker.go index bd0078908..49e620fce 100644 --- a/internal/controller/ledger/bulker.go +++ b/internal/controller/ledger/bulker.go @@ -3,12 +3,15 @@ package ledger import ( "context" "encoding/json" + "errors" "fmt" + "github.com/alitto/pond" "github.com/formancehq/go-libs/v2/logging" "github.com/formancehq/go-libs/v2/metadata" "github.com/formancehq/go-libs/v2/pointer" "github.com/formancehq/go-libs/v2/time" ledger "github.com/formancehq/ledger/internal" + "sync" ) const ( @@ -96,39 +99,65 @@ func (req *TransactionRequest) ToRunScript(allowUnboundedOverdrafts bool) (*RunS } type Bulker struct { - ctrl Controller + ctrl Controller + parallelism int } -func (b *Bulker) run(ctx context.Context, ctrl Controller, bulk Bulk, continueOnFailure bool) (BulkResult, error) { +func (b *Bulker) run(ctx context.Context, ctrl Controller, bulk Bulk, continueOnFailure, parallel bool) (BulkResult, error) { - results := make([]BulkElementResult, 0, len(bulk)) + ctx, cancel := context.WithCancel(ctx) + defer cancel() - for _, element := range bulk { - ret, logID, err := b.processElement(ctx, ctrl, element) - if err != nil { - results = append(results, BulkElementResult{ - Error: err, - }) + parallelism := 1 + if parallel && b.parallelism != 0 { + parallelism = b.parallelism + } + + wp := pond.New(parallelism, len(bulk), pond.Context(ctx)) + results := sync.Map{} - if !continueOnFailure { - return results, nil + for index, element := range bulk { + wp.Submit(func() { + ret, logID, err := b.processElement(ctx, ctrl, element) + if err != nil { + results.Store(index, BulkElementResult{ + Error: err, + }) + + if !continueOnFailure { + cancel() + } + + return } + results.Store(index, BulkElementResult{ + Data: ret, + LogID: logID, + }) + }) + } + + wp.StopAndWait() + + finalResults := make(BulkResult, 0, len(bulk)) + for index := range bulk { + v, ok := results.Load(index) + if ok { + finalResults = append(finalResults, v.(BulkElementResult)) continue } - - results = append(results, BulkElementResult{ - Data: ret, - LogID: logID, + finalResults = append(finalResults, BulkElementResult{ + Error: errors.New("canceled"), }) } - return results, nil + return finalResults, nil } -func (b *Bulker) Run(ctx context.Context, bulk Bulk, providedOptions ... BulkOption) (BulkResult, error) { +func (b *Bulker) Run(ctx context.Context, bulk Bulk, providedOptions ...BulkingOption) (BulkResult, error) { - bulkOptions := BulkOptions{} + bulkOptions := BulkingOptions{} for _, option := range providedOptions { option(&bulkOptions) } @@ -167,7 +196,7 @@ func (b *Bulker) Run(ctx context.Context, bulk Bulk, providedOptions ... BulkOpt } } - results, err := b.run(ctx, ctrl, bulk, bulkOptions.ContinueOnFailure) + results, err := b.run(ctx, ctrl, bulk, bulkOptions.ContinueOnFailure, bulkOptions.Parallel) if err != nil { if bulkOptions.Atomic { if rollbackErr := ctrl.Rollback(ctx); rollbackErr != nil { @@ -333,25 +362,65 @@ func (b *Bulker) processElement(ctx context.Context, ctrl Controller, element Bu } } -func NewBulker(ctrl Controller) *Bulker { - return &Bulker{ctrl: ctrl} +func NewBulker(ctrl Controller, options ...BulkerOption) *Bulker { + ret := &Bulker{ctrl: ctrl} + for _, option := range options { + option(ret) + } + + return ret +} + +type BulkerOption func(bulker *Bulker) + +func WithParallelism(v int) BulkerOption { + return func(options *Bulker) { + options.parallelism = v + } } -type BulkOptions struct { +type BulkingOptions struct { ContinueOnFailure bool Atomic bool + Parallel bool } -type BulkOption func(*BulkOptions) +type BulkingOption func(*BulkingOptions) -func WithContinueOnFailure(v bool) BulkOption { - return func(options *BulkOptions) { +func WithContinueOnFailure(v bool) BulkingOption { + return func(options *BulkingOptions) { options.ContinueOnFailure = v } } -func WithAtomic(v bool) BulkOption { - return func(options *BulkOptions) { +func WithAtomic(v bool) BulkingOption { + return func(options *BulkingOptions) { options.Atomic = v } -} \ No newline at end of file +} + +func WithParallel(v bool) BulkingOption { + return func(options *BulkingOptions) { + options.Parallel = v + } +} + +type BulkerFactory interface { + CreateBulker(ctrl Controller) *Bulker +} + +type DefaultBulkerFactory struct { + Options []BulkerOption +} + +func (d *DefaultBulkerFactory) CreateBulker(ctrl Controller) *Bulker { + return NewBulker(ctrl, d.Options...) +} + +func NewDefaultBulkerFactory(options ...BulkerOption) *DefaultBulkerFactory { + return &DefaultBulkerFactory{ + Options: options, + } +} + +var _ BulkerFactory = (*DefaultBulkerFactory)(nil) From dc778decefaf024835bae4281a88d88f771f7170 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Sat, 23 Nov 2024 16:19:28 +0100 Subject: [PATCH 38/71] feat: update generator to accept a complete bulk --- pkg/generate/generator.go | 324 +++++++++---------- pkg/generate/generator_test.go | 6 +- pkg/generate/set.go | 25 +- test/performance/example_scripts/example1.js | 20 +- 4 files changed, 194 insertions(+), 181 deletions(-) diff --git a/pkg/generate/generator.go b/pkg/generate/generator.go index 45adb6ea5..f821d07c3 100644 --- a/pkg/generate/generator.go +++ b/pkg/generate/generator.go @@ -20,171 +20,149 @@ import ( ) type Action struct { - ledgercontroller.BulkElement + elements []ledgercontroller.BulkElement } -type Result struct { - components.V2BulkElementResult -} +func (r Action) Apply(ctx context.Context, client *client.V2, l string) ([]components.V2BulkElementResult, error) { -func (r Result) GetLogID() int64 { - switch r.Type { - case components.V2BulkElementResultTypeCreateTransaction: - return r.V2BulkElementResultCreateTransaction.LogID - case components.V2BulkElementResultTypeAddMetadata: - return r.V2BulkElementResultAddMetadata.LogID - case components.V2BulkElementResultTypeDeleteMetadata: - return r.V2BulkElementResultDeleteMetadata.LogID - case components.V2BulkElementResultTypeRevertTransaction: - return r.V2BulkElementResultRevertTransaction.LogID - default: - panic(fmt.Sprintf("unexpected result type: %s", r.Type)) - } -} + bulkElements := make([]components.V2BulkElement, 0) -func (r Action) Apply(ctx context.Context, client *client.V2, l string) (*Result, error) { + for _, element := range r.elements { + var bulkElement components.V2BulkElement - var bulkElement components.V2BulkElement - switch r.Action { - case ledgercontroller.ActionCreateTransaction: - transactionRequest := &ledgercontroller.TransactionRequest{} - err := json.Unmarshal(r.Data, transactionRequest) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal transaction request: %w", err) - } + switch element.Action { + case ledgercontroller.ActionCreateTransaction: + transactionRequest := &ledgercontroller.TransactionRequest{} + err := json.Unmarshal(element.Data, transactionRequest) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal transaction request: %w", err) + } - bulkElement = components.CreateV2BulkElementCreateTransaction(components.V2BulkElementCreateTransaction{ - Data: &components.V2PostTransaction{ - Timestamp: func() *time.Time { - if transactionRequest.Timestamp.IsZero() { - return nil - } - return &transactionRequest.Timestamp.Time - }(), - Script: &components.V2PostTransactionScript{ - Plain: transactionRequest.Script.Plain, - Vars: collectionutils.ConvertMap(transactionRequest.Script.Vars, func(from any) string { - return fmt.Sprint(from) + bulkElement = components.CreateV2BulkElementCreateTransaction(components.V2BulkElementCreateTransaction{ + Data: &components.V2PostTransaction{ + Timestamp: func() *time.Time { + if transactionRequest.Timestamp.IsZero() { + return nil + } + return &transactionRequest.Timestamp.Time + }(), + Script: &components.V2PostTransactionScript{ + Plain: transactionRequest.Script.Plain, + Vars: collectionutils.ConvertMap(transactionRequest.Script.Vars, func(from any) string { + return fmt.Sprint(from) + }), + }, + Postings: collectionutils.Map(transactionRequest.Postings, func(p ledger.Posting) components.V2Posting { + return components.V2Posting{ + Amount: p.Amount, + Asset: p.Asset, + Destination: p.Destination, + Source: p.Source, + } }), + Reference: func() *string { + if transactionRequest.Reference == "" { + return nil + } + return &transactionRequest.Reference + }(), + Metadata: transactionRequest.Metadata, }, - Postings: collectionutils.Map(transactionRequest.Postings, func(p ledger.Posting) components.V2Posting { - return components.V2Posting{ - Amount: p.Amount, - Asset: p.Asset, - Destination: p.Destination, - Source: p.Source, - } - }), - Reference: func() *string { - if transactionRequest.Reference == "" { - return nil - } - return &transactionRequest.Reference - }(), - Metadata: transactionRequest.Metadata, - }, - }) - case ledgercontroller.ActionAddMetadata: - addMetadataRequest := &ledgercontroller.AddMetadataRequest{} - err := json.Unmarshal(r.Data, addMetadataRequest) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal add metadata request: %w", err) - } - - var targetID components.V2TargetID - switch addMetadataRequest.TargetType { - case ledger.MetaTargetTypeAccount: - var targetIDStr string - if err := json.Unmarshal(addMetadataRequest.TargetID, &targetIDStr); err != nil { - return nil, fmt.Errorf("failed to unmarshal target id: %w", err) + }) + case ledgercontroller.ActionAddMetadata: + addMetadataRequest := &ledgercontroller.AddMetadataRequest{} + err := json.Unmarshal(element.Data, addMetadataRequest) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal add metadata request: %w", err) } - targetID = components.CreateV2TargetIDStr(targetIDStr) - case ledger.MetaTargetTypeTransaction: - var targetIDInt int - if err := json.Unmarshal(addMetadataRequest.TargetID, &targetIDInt); err != nil { - return nil, fmt.Errorf("failed to unmarshal target id: %w", err) + + var targetID components.V2TargetID + switch addMetadataRequest.TargetType { + case ledger.MetaTargetTypeAccount: + var targetIDStr string + if err := json.Unmarshal(addMetadataRequest.TargetID, &targetIDStr); err != nil { + return nil, fmt.Errorf("failed to unmarshal target id: %w", err) + } + targetID = components.CreateV2TargetIDStr(targetIDStr) + case ledger.MetaTargetTypeTransaction: + var targetIDInt int + if err := json.Unmarshal(addMetadataRequest.TargetID, &targetIDInt); err != nil { + return nil, fmt.Errorf("failed to unmarshal target id: %w", err) + } + targetID = components.CreateV2TargetIDBigint(big.NewInt(int64(targetIDInt))) + default: + panic("unexpected target id type") } - targetID = components.CreateV2TargetIDBigint(big.NewInt(int64(targetIDInt))) - default: - panic("unexpected target id type") - } - bulkElement = components.CreateV2BulkElementAddMetadata(components.V2BulkElementAddMetadata{ - Data: &components.Data{ - TargetID: targetID, - TargetType: components.V2TargetType(addMetadataRequest.TargetType), - Metadata: addMetadataRequest.Metadata, - }, - }) - case ledgercontroller.ActionDeleteMetadata: - deleteMetadataRequest := &ledgercontroller.DeleteMetadataRequest{} - err := json.Unmarshal(r.Data, deleteMetadataRequest) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal delete metadata request: %w", err) - } + bulkElement = components.CreateV2BulkElementAddMetadata(components.V2BulkElementAddMetadata{ + Data: &components.Data{ + TargetID: targetID, + TargetType: components.V2TargetType(addMetadataRequest.TargetType), + Metadata: addMetadataRequest.Metadata, + }, + }) + case ledgercontroller.ActionDeleteMetadata: + deleteMetadataRequest := &ledgercontroller.DeleteMetadataRequest{} + err := json.Unmarshal(element.Data, deleteMetadataRequest) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal delete metadata request: %w", err) + } - var targetID components.V2TargetID - switch deleteMetadataRequest.TargetType { - case ledger.MetaTargetTypeAccount: - var targetIDStr string - if err := json.Unmarshal(deleteMetadataRequest.TargetID, &targetIDStr); err != nil { - return nil, fmt.Errorf("failed to unmarshal target id: %w", err) + var targetID components.V2TargetID + switch deleteMetadataRequest.TargetType { + case ledger.MetaTargetTypeAccount: + var targetIDStr string + if err := json.Unmarshal(deleteMetadataRequest.TargetID, &targetIDStr); err != nil { + return nil, fmt.Errorf("failed to unmarshal target id: %w", err) + } + targetID = components.CreateV2TargetIDStr(targetIDStr) + case ledger.MetaTargetTypeTransaction: + var targetIDInt int + if err := json.Unmarshal(deleteMetadataRequest.TargetID, &targetIDInt); err != nil { + return nil, fmt.Errorf("failed to unmarshal target id: %w", err) + } + targetID = components.CreateV2TargetIDBigint(big.NewInt(int64(targetIDInt))) + default: + panic("unexpected target id type") } - targetID = components.CreateV2TargetIDStr(targetIDStr) - case ledger.MetaTargetTypeTransaction: - var targetIDInt int - if err := json.Unmarshal(deleteMetadataRequest.TargetID, &targetIDInt); err != nil { - return nil, fmt.Errorf("failed to unmarshal target id: %w", err) + + bulkElement = components.CreateV2BulkElementDeleteMetadata(components.V2BulkElementDeleteMetadata{ + Data: &components.V2BulkElementDeleteMetadataData{ + TargetID: targetID, + TargetType: components.V2TargetType(deleteMetadataRequest.TargetType), + Key: deleteMetadataRequest.Key, + }, + }) + case ledgercontroller.ActionRevertTransaction: + revertMetadataRequest := &ledgercontroller.RevertTransactionRequest{} + err := json.Unmarshal(element.Data, revertMetadataRequest) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal delete metadata request: %w", err) } - targetID = components.CreateV2TargetIDBigint(big.NewInt(int64(targetIDInt))) - default: - panic("unexpected target id type") - } - bulkElement = components.CreateV2BulkElementDeleteMetadata(components.V2BulkElementDeleteMetadata{ - Data: &components.V2BulkElementDeleteMetadataData{ - TargetID: targetID, - TargetType: components.V2TargetType(deleteMetadataRequest.TargetType), - Key: deleteMetadataRequest.Key, - }, - }) - case ledgercontroller.ActionRevertTransaction: - revertMetadataRequest := &ledgercontroller.RevertTransactionRequest{} - err := json.Unmarshal(r.Data, revertMetadataRequest) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal delete metadata request: %w", err) + bulkElement = components.CreateV2BulkElementRevertTransaction(components.V2BulkElementRevertTransaction{ + Data: &components.V2BulkElementRevertTransactionData{ + ID: big.NewInt(int64(revertMetadataRequest.ID)), + Force: &revertMetadataRequest.Force, + AtEffectiveDate: &revertMetadataRequest.AtEffectiveDate, + }, + }) + default: + panic("unexpected action") } - bulkElement = components.CreateV2BulkElementRevertTransaction(components.V2BulkElementRevertTransaction{ - Data: &components.V2BulkElementRevertTransactionData{ - ID: big.NewInt(int64(revertMetadataRequest.ID)), - Force: &revertMetadataRequest.Force, - AtEffectiveDate: &revertMetadataRequest.AtEffectiveDate, - }, - }) - default: - panic("unexpected action") + bulkElements = append(bulkElements, bulkElement) } response, err := client.CreateBulk(ctx, operations.V2CreateBulkRequest{ Ledger: l, - RequestBody: []components.V2BulkElement{bulkElement}, + RequestBody: bulkElements, }) if err != nil { return nil, fmt.Errorf("creating transaction: %w", err) } - if errorResponse := response.V2BulkResponse.Data[0].V2BulkElementResultError; errorResponse != nil { - if errorResponse.ErrorCode != "" { - errorDescription := errorResponse.ErrorDescription - if errorDescription == "" { - errorDescription = "" - } - return nil, fmt.Errorf("[%s] %s", errorResponse.ErrorCode, errorDescription) - } - } - - return &Result{response.V2BulkResponse.Data[0]}, nil + return response.V2BulkResponse.Data, nil } type NextOptions struct { @@ -248,7 +226,7 @@ func NewGenerator(script string, opts ...Option) (*Generator, error) { return nil, err } - var next func(int) map[string]any + var next func(int) []map[string]any err = runtime.ExportTo(runtime.Get("next"), &next) if err != nil { panic(err) @@ -270,52 +248,58 @@ func NewGenerator(script string, opts ...Option) (*Generator, error) { } } - ret := next(i) + rawElements := next(i) var ( action string ik string data map[string]any ok bool + elements = make([]ledgercontroller.BulkElement, 0) ) - rawAction := ret["action"] - if rawAction == nil { - return nil, errors.New("'action' must be set") - } - - action, ok = rawAction.(string) - if !ok { - return nil, errors.New("'action' must be a string") - } + for _, rawElement := range rawElements { - rawData := ret["data"] - if rawData == nil { - return nil, errors.New("'data' must be set") - } - data, ok = rawData.(map[string]any) - if !ok { - return nil, errors.New("'data' must be a map[string]any") - } + rawAction := rawElement["action"] + if rawAction == nil { + return nil, errors.New("'action' must be set") + } - dataAsJsonRawMessage, err := json.Marshal(data) - if err != nil { - return nil, err - } + action, ok = rawAction.(string) + if !ok { + return nil, errors.New("'action' must be a string") + } - rawIK := ret["ik"] - if rawIK != nil { - ik, ok = rawIK.(string) + rawData := rawElement["data"] + if rawData == nil { + return nil, errors.New("'data' must be set") + } + data, ok = rawData.(map[string]any) if !ok { - return nil, errors.New("'ik' must be a string") + return nil, errors.New("'data' must be a map[string]any") } - } - return &Action{ - BulkElement: ledgercontroller.BulkElement{ + dataAsJsonRawMessage, err := json.Marshal(data) + if err != nil { + return nil, err + } + + rawIK := rawElement["ik"] + if rawIK != nil { + ik, ok = rawIK.(string) + if !ok { + return nil, errors.New("'ik' must be a string") + } + } + + elements = append(elements, ledgercontroller.BulkElement{ Action: action, IdempotencyKey: ik, Data: dataAsJsonRawMessage, - }, + }) + } + + return &Action{ + elements: elements, }, nil }, }, nil diff --git a/pkg/generate/generator_test.go b/pkg/generate/generator_test.go index b06977485..eff693317 100644 --- a/pkg/generate/generator_test.go +++ b/pkg/generate/generator_test.go @@ -65,7 +65,7 @@ func TestGenerator(t *testing.T) { } const script = ` -function next(iteration) { +function nextElement(iteration) { switch (iteration % 4) { case 0: return { @@ -120,4 +120,8 @@ set_tx_meta("globalMetadata", "${globalMetadata}") } } } + +function next(iteration) { + return [nextElement(iteration)] +} ` diff --git a/pkg/generate/set.go b/pkg/generate/set.go index b3ae52f63..17246a8b8 100644 --- a/pkg/generate/set.go +++ b/pkg/generate/set.go @@ -4,8 +4,10 @@ import ( "context" "errors" "fmt" + "github.com/formancehq/go-libs/v2/collectionutils" "github.com/formancehq/go-libs/v2/logging" "github.com/formancehq/ledger/pkg/client" + "github.com/formancehq/ledger/pkg/client/models/components" "golang.org/x/sync/errgroup" ) @@ -51,7 +53,28 @@ func (s *GeneratorSet) Run(ctx context.Context) error { } return fmt.Errorf("iteration %d/%d failed: %w", vu, iteration, err) } - if s.untilLogID != 0 && uint64(ret.GetLogID()) >= s.untilLogID { + maxLogID := collectionutils.Reduce(ret, func(acc int64, r components.V2BulkElementResult) int64 { + var logID int64 + switch r.Type { + case components.V2BulkElementResultTypeCreateTransaction: + logID = r.V2BulkElementResultCreateTransaction.LogID + case components.V2BulkElementResultTypeAddMetadata: + logID = r.V2BulkElementResultAddMetadata.LogID + case components.V2BulkElementResultTypeDeleteMetadata: + logID = r.V2BulkElementResultDeleteMetadata.LogID + case components.V2BulkElementResultTypeRevertTransaction: + logID = r.V2BulkElementResultRevertTransaction.LogID + default: + panic(fmt.Sprintf("unexpected result type: %s", r.Type)) + } + + if logID > acc { + return logID + } + return acc + }, 0) + + if s.untilLogID != 0 && uint64(maxLogID) >= s.untilLogID { return nil } iteration++ diff --git a/test/performance/example_scripts/example1.js b/test/performance/example_scripts/example1.js index 0157f6c5b..7e16f1da7 100644 --- a/test/performance/example_scripts/example1.js +++ b/test/performance/example_scripts/example1.js @@ -16,18 +16,20 @@ send [USD/2 99] ( )` function next(iteration) { - return { - action: 'CREATE_TRANSACTION', - data: { - script: { - plain, - vars: { - order: `orders:${uuid()}`, - seller: `sellers:${iteration % 5}` + return [ + { + action: 'CREATE_TRANSACTION', + data: { + script: { + plain, + vars: { + order: `orders:${uuid()}`, + seller: `sellers:${iteration % 5}` + } } } } - } + ] } From 7e1b49c5ba87d846e0a059d2b2c64831240f59a9 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Sat, 23 Nov 2024 16:26:48 +0100 Subject: [PATCH 39/71] feat(bulk): add parallel processing --- docs/api/README.md | 1 + openapi.yaml | 6 ++++ openapi/v2.yaml | 6 ++++ pkg/client/.speakeasy/gen.lock | 6 ++-- pkg/client/.speakeasy/gen.yaml | 2 +- .../models/operations/v2createbulkrequest.md | 1 + pkg/client/docs/sdks/v2/README.md | 1 + pkg/client/formance.go | 4 +-- pkg/client/models/operations/v2createbulk.go | 11 +++++- test/e2e/api_bulk_test.go | 34 ++++++++++++++++--- tools/generator/go.mod | 1 + 11 files changed, 61 insertions(+), 12 deletions(-) diff --git a/docs/api/README.md b/docs/api/README.md index f8e344946..a4ad477c0 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -500,6 +500,7 @@ Accept: application/json |ledger|path|string|true|Name of the ledger.| |continueOnFailure|query|boolean|false|Continue on failure| |atomic|query|boolean|false|Make bulk atomic| +|parallel|query|boolean|false|Process bulk elements in parallel| |body|body|[V2Bulk](#schemav2bulk)|false|none| > Example responses diff --git a/openapi.yaml b/openapi.yaml index 9f8baf13b..45e46e777 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1460,6 +1460,12 @@ paths: schema: type: boolean example: true + - name: parallel + in: query + description: Process bulk elements in parallel + schema: + type: boolean + example: true requestBody: content: application/json: diff --git a/openapi/v2.yaml b/openapi/v2.yaml index 13eb82791..c78ffe775 100644 --- a/openapi/v2.yaml +++ b/openapi/v2.yaml @@ -291,6 +291,12 @@ paths: schema: type: boolean example: true + - name: parallel + in: query + description: Process bulk elements in parallel + schema: + type: boolean + example: true requestBody: content: application/json: diff --git a/pkg/client/.speakeasy/gen.lock b/pkg/client/.speakeasy/gen.lock index 983ac76d8..198238b13 100644 --- a/pkg/client/.speakeasy/gen.lock +++ b/pkg/client/.speakeasy/gen.lock @@ -1,12 +1,12 @@ lockVersion: 2.0.0 id: a9ac79e1-e429-4ee3-96c4-ec973f19bec3 management: - docChecksum: 4a4a3929b808f3192cbb2f02351bc186 + docChecksum: 2624238aba49e6a33f19ef1d62f0b568 docVersion: v1 speakeasyVersion: 1.351.0 generationVersion: 2.384.1 - releaseVersion: 0.4.33 - configChecksum: 22f33d29f62599fa892d20fe5d13f4cc + releaseVersion: 0.4.34 + configChecksum: 44b98e4f6380b040c4360085974a2b3f features: go: additionalDependencies: 0.1.0 diff --git a/pkg/client/.speakeasy/gen.yaml b/pkg/client/.speakeasy/gen.yaml index 117836507..c3cb8f2b1 100644 --- a/pkg/client/.speakeasy/gen.yaml +++ b/pkg/client/.speakeasy/gen.yaml @@ -15,7 +15,7 @@ generation: auth: oAuth2ClientCredentialsEnabled: true go: - version: 0.4.33 + version: 0.4.34 additionalDependencies: {} allowUnknownFieldsInWeakUnions: false clientServerStatusCodesAsErrors: true diff --git a/pkg/client/docs/models/operations/v2createbulkrequest.md b/pkg/client/docs/models/operations/v2createbulkrequest.md index 078c42518..46c8f599a 100644 --- a/pkg/client/docs/models/operations/v2createbulkrequest.md +++ b/pkg/client/docs/models/operations/v2createbulkrequest.md @@ -8,4 +8,5 @@ | `Ledger` | *string* | :heavy_check_mark: | Name of the ledger. | ledger001 | | `ContinueOnFailure` | **bool* | :heavy_minus_sign: | Continue on failure | true | | `Atomic` | **bool* | :heavy_minus_sign: | Make bulk atomic | true | +| `Parallel` | **bool* | :heavy_minus_sign: | Process bulk elements in parallel | true | | `RequestBody` | [][components.V2BulkElement](../../models/components/v2bulkelement.md) | :heavy_minus_sign: | 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 38dd2752e..3788cf758 100644 --- a/pkg/client/docs/sdks/v2/README.md +++ b/pkg/client/docs/sdks/v2/README.md @@ -398,6 +398,7 @@ func main() { Ledger: "ledger001", ContinueOnFailure: client.Bool(true), Atomic: client.Bool(true), + Parallel: client.Bool(true), RequestBody: []components.V2BulkElement{ components.CreateV2BulkElementV2BulkElementCreateTransaction( components.V2BulkElementCreateTransaction{ diff --git a/pkg/client/formance.go b/pkg/client/formance.go index f6e207f06..4aa7b84a6 100644 --- a/pkg/client/formance.go +++ b/pkg/client/formance.go @@ -143,9 +143,9 @@ func New(opts ...SDKOption) *Formance { sdkConfiguration: sdkConfiguration{ Language: "go", OpenAPIDocVersion: "v1", - SDKVersion: "0.4.33", + SDKVersion: "0.4.34", GenVersion: "2.384.1", - UserAgent: "speakeasy-sdk/go 0.4.33 2.384.1 v1 github.com/formancehq/ledger/pkg/client", + UserAgent: "speakeasy-sdk/go 0.4.34 2.384.1 v1 github.com/formancehq/ledger/pkg/client", Hooks: hooks.New(), }, } diff --git a/pkg/client/models/operations/v2createbulk.go b/pkg/client/models/operations/v2createbulk.go index eb3819184..876185e0c 100644 --- a/pkg/client/models/operations/v2createbulk.go +++ b/pkg/client/models/operations/v2createbulk.go @@ -12,7 +12,9 @@ type V2CreateBulkRequest struct { // Continue on failure ContinueOnFailure *bool `queryParam:"style=form,explode=true,name=continueOnFailure"` // Make bulk atomic - Atomic *bool `queryParam:"style=form,explode=true,name=atomic"` + Atomic *bool `queryParam:"style=form,explode=true,name=atomic"` + // Process bulk elements in parallel + Parallel *bool `queryParam:"style=form,explode=true,name=parallel"` RequestBody []components.V2BulkElement `request:"mediaType=application/json"` } @@ -37,6 +39,13 @@ func (o *V2CreateBulkRequest) GetAtomic() *bool { return o.Atomic } +func (o *V2CreateBulkRequest) GetParallel() *bool { + if o == nil { + return nil + } + return o.Parallel +} + func (o *V2CreateBulkRequest) GetRequestBody() []components.V2BulkElement { if o == nil { return nil diff --git a/test/e2e/api_bulk_test.go b/test/e2e/api_bulk_test.go index fd568860b..24e3a6fb1 100644 --- a/test/e2e/api_bulk_test.go +++ b/test/e2e/api_bulk_test.go @@ -38,7 +38,7 @@ var _ = Context("Ledger engine tests", func() { ctx = logging.TestingContext() events chan *nats.Msg bulkResponse []components.V2BulkElementResult - bulkMaxSize = 5 + bulkMaxSize = 100 ) testServer := NewTestServer(func() Configuration { @@ -60,10 +60,10 @@ var _ = Context("Ledger engine tests", func() { }) When("creating a bulk on a ledger", func() { var ( - now = time.Now().Round(time.Microsecond).UTC() - items []components.V2BulkElement - err error - atomic bool + now = time.Now().Round(time.Microsecond).UTC() + items []components.V2BulkElement + err error + atomic, parallel bool ) BeforeEach(func() { items = []components.V2BulkElement{ @@ -106,6 +106,7 @@ var _ = Context("Ledger engine tests", func() { JustBeforeEach(func() { bulkResponse, err = CreateBulk(ctx, testServer.GetValue(), operations.V2CreateBulkRequest{ Atomic: pointer.For(atomic), + Parallel: pointer.For(parallel), RequestBody: items, Ledger: "default", }) @@ -176,6 +177,29 @@ var _ = Context("Ledger engine tests", func() { Expect(err).To(HaveErrorCode(string(components.V2ErrorsEnumBulkSizeExceeded))) }) }) + Context("with parallel", func() { + BeforeEach(func() { + parallel = true + items = make([]components.V2BulkElement, 0) + for i := 0; i < bulkMaxSize; i++ { + items = append(items, components.CreateV2BulkElementCreateTransaction(components.V2BulkElementCreateTransaction{ + Data: &components.V2PostTransaction{ + Metadata: map[string]string{}, + Postings: []components.V2Posting{{ + Amount: big.NewInt(100), + Asset: "USD/2", + Destination: "bank", + Source: "world", + }}, + Timestamp: &now, + }, + })) + } + }) + It("should be ok", func() { + Expect(err).To(BeNil()) + }) + }) }) When("creating a bulk with an error on a ledger", func() { var ( diff --git a/tools/generator/go.mod b/tools/generator/go.mod index abd649151..5467b81c9 100644 --- a/tools/generator/go.mod +++ b/tools/generator/go.mod @@ -20,6 +20,7 @@ require ( require ( dario.cat/mergo v1.0.1 // indirect github.com/ThreeDotsLabs/watermill v1.4.1 // indirect + github.com/alitto/pond v1.9.2 // indirect github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect From ecc9c7d2abea7a759ba7fea5c2a98234fd47a2df Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 25 Nov 2024 09:13:29 +0100 Subject: [PATCH 40/71] fix: data race --- internal/storage/ledger/legacy/store.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/internal/storage/ledger/legacy/store.go b/internal/storage/ledger/legacy/store.go index 657b0d70a..d3ae4cb62 100644 --- a/internal/storage/ledger/legacy/store.go +++ b/internal/storage/ledger/legacy/store.go @@ -13,8 +13,8 @@ type Store struct { name string } -func (s *Store) GetPrefixedRelationName(v string) string { - return fmt.Sprintf(`"%s".%s`, s.bucket, v) +func (store *Store) GetPrefixedRelationName(v string) string { + return fmt.Sprintf(`"%s".%s`, store.bucket, v) } func (store *Store) Name() string { @@ -25,10 +25,9 @@ func (store *Store) GetDB() bun.IDB { return store.db } -func (s *Store) WithDB(db bun.IDB) *Store { - ret := *s - ret.db = db - return &ret +func (store Store) WithDB(db bun.IDB) *Store { + store.db = db + return &store } func New( From ecd9142a2a4be9ec492938acb407ee3e9dd8aeea Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 25 Nov 2024 09:38:36 +0100 Subject: [PATCH 41/71] fix: data race --- internal/storage/ledger/legacy/adapters.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/storage/ledger/legacy/adapters.go b/internal/storage/ledger/legacy/adapters.go index 807bb1678..f63695b4b 100644 --- a/internal/storage/ledger/legacy/adapters.go +++ b/internal/storage/ledger/legacy/adapters.go @@ -116,11 +116,11 @@ func (d *DefaultStoreAdapter) BeginTX(ctx context.Context, opts *sql.TxOptions) return nil, err } - d.legacyStore = d.legacyStore.WithDB(store.GetDB()) + legacyStore := d.legacyStore.WithDB(store.GetDB()) return &DefaultStoreAdapter{ newStore: store, - legacyStore: d.legacyStore, + legacyStore: legacyStore, }, nil } From fb4c804bc242bfee143852105204ca987d37d168 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 25 Nov 2024 17:19:56 +0100 Subject: [PATCH 42/71] fix: tests --- pkg/generate/generator.go | 32 ++++++++++++++-------------- tools/generator/examples/example1.js | 4 ++-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/pkg/generate/generator.go b/pkg/generate/generator.go index f821d07c3..6dbcebdfa 100644 --- a/pkg/generate/generator.go +++ b/pkg/generate/generator.go @@ -8,7 +8,7 @@ import ( "github.com/dop251/goja" "github.com/formancehq/go-libs/v2/collectionutils" ledger "github.com/formancehq/ledger/internal" - ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" + bulking "github.com/formancehq/ledger/internal/controller/ledger" "github.com/formancehq/ledger/pkg/client" "github.com/formancehq/ledger/pkg/client/models/components" "github.com/formancehq/ledger/pkg/client/models/operations" @@ -20,7 +20,7 @@ import ( ) type Action struct { - elements []ledgercontroller.BulkElement + elements []bulking.BulkElement } func (r Action) Apply(ctx context.Context, client *client.V2, l string) ([]components.V2BulkElementResult, error) { @@ -31,8 +31,8 @@ func (r Action) Apply(ctx context.Context, client *client.V2, l string) ([]compo var bulkElement components.V2BulkElement switch element.Action { - case ledgercontroller.ActionCreateTransaction: - transactionRequest := &ledgercontroller.TransactionRequest{} + case bulking.ActionCreateTransaction: + transactionRequest := &bulking.TransactionRequest{} err := json.Unmarshal(element.Data, transactionRequest) if err != nil { return nil, fmt.Errorf("failed to unmarshal transaction request: %w", err) @@ -69,8 +69,8 @@ func (r Action) Apply(ctx context.Context, client *client.V2, l string) ([]compo Metadata: transactionRequest.Metadata, }, }) - case ledgercontroller.ActionAddMetadata: - addMetadataRequest := &ledgercontroller.AddMetadataRequest{} + case bulking.ActionAddMetadata: + addMetadataRequest := &bulking.AddMetadataRequest{} err := json.Unmarshal(element.Data, addMetadataRequest) if err != nil { return nil, fmt.Errorf("failed to unmarshal add metadata request: %w", err) @@ -101,8 +101,8 @@ func (r Action) Apply(ctx context.Context, client *client.V2, l string) ([]compo Metadata: addMetadataRequest.Metadata, }, }) - case ledgercontroller.ActionDeleteMetadata: - deleteMetadataRequest := &ledgercontroller.DeleteMetadataRequest{} + case bulking.ActionDeleteMetadata: + deleteMetadataRequest := &bulking.DeleteMetadataRequest{} err := json.Unmarshal(element.Data, deleteMetadataRequest) if err != nil { return nil, fmt.Errorf("failed to unmarshal delete metadata request: %w", err) @@ -133,8 +133,8 @@ func (r Action) Apply(ctx context.Context, client *client.V2, l string) ([]compo Key: deleteMetadataRequest.Key, }, }) - case ledgercontroller.ActionRevertTransaction: - revertMetadataRequest := &ledgercontroller.RevertTransactionRequest{} + case bulking.ActionRevertTransaction: + revertMetadataRequest := &bulking.RevertTransactionRequest{} err := json.Unmarshal(element.Data, revertMetadataRequest) if err != nil { return nil, fmt.Errorf("failed to unmarshal delete metadata request: %w", err) @@ -251,11 +251,11 @@ func NewGenerator(script string, opts ...Option) (*Generator, error) { rawElements := next(i) var ( - action string - ik string - data map[string]any - ok bool - elements = make([]ledgercontroller.BulkElement, 0) + action string + ik string + data map[string]any + ok bool + elements = make([]bulking.BulkElement, 0) ) for _, rawElement := range rawElements { @@ -291,7 +291,7 @@ func NewGenerator(script string, opts ...Option) (*Generator, error) { } } - elements = append(elements, ledgercontroller.BulkElement{ + elements = append(elements, bulking.BulkElement{ Action: action, IdempotencyKey: ik, Data: dataAsJsonRawMessage, diff --git a/tools/generator/examples/example1.js b/tools/generator/examples/example1.js index fc50dd1a1..6abe3d376 100644 --- a/tools/generator/examples/example1.js +++ b/tools/generator/examples/example1.js @@ -16,7 +16,7 @@ send [USD/2 99] ( )` function next(iteration) { - return { + return [{ action: 'CREATE_TRANSACTION', data: { script: { @@ -27,5 +27,5 @@ function next(iteration) { } } } - } + }] } \ No newline at end of file From 30c76eec6b49fc36bdd85c3f92377b1f89f8e17a Mon Sep 17 00:00:00 2001 From: rsln <93119071+reslene@users.noreply.github.com> Date: Tue, 26 Nov 2024 09:51:09 +0100 Subject: [PATCH 43/71] fix(openapi): update fields (#580) * fix(openapi): update fields * fix(openapi): update sdk client --- openapi.yaml | 10 +--------- openapi/v1.yaml | 10 +--------- pkg/client/.speakeasy/gen.lock | 1 - pkg/client/.speakeasy/gen.yaml | 2 +- pkg/client/docs/models/components/migrationinfo.md | 2 +- pkg/client/formance.go | 4 ++-- pkg/client/models/components/migrationinfo.go | 4 ++-- pkg/client/models/operations/getbalances.go | 14 +------------- pkg/client/models/operations/listaccounts.go | 14 +------------- pkg/client/models/operations/listlogs.go | 2 +- pkg/client/models/operations/listtransactions.go | 2 +- 11 files changed, 12 insertions(+), 53 deletions(-) diff --git a/openapi.yaml b/openapi.yaml index 45e46e777..a441ab8e7 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -129,7 +129,6 @@ paths: format: int64 minimum: 1 maximum: 1000 - default: 15 - name: page_size x-speakeasy-ignore: true in: query @@ -142,7 +141,6 @@ paths: format: int64 minimum: 1 maximum: 1000 - default: 15 deprecated: true - name: after in: query @@ -603,7 +601,6 @@ paths: format: int64 minimum: 1 maximum: 1000 - default: 15 - name: page_size x-speakeasy-ignore: true in: query @@ -616,7 +613,6 @@ paths: format: int64 minimum: 1 maximum: 1000 - default: 15 deprecated: true - name: after in: query @@ -969,7 +965,6 @@ paths: format: int64 minimum: 1 maximum: 1000 - default: 15 - name: after in: query description: Pagination cursor, will return accounts after given address, in descending order. @@ -1081,7 +1076,6 @@ paths: format: int64 minimum: 1 maximum: 1000 - default: 15 - name: page_size x-speakeasy-ignore: true in: query @@ -1094,7 +1088,6 @@ paths: format: int64 minimum: 1 maximum: 1000 - default: 15 deprecated: true - name: after in: query @@ -3058,8 +3051,7 @@ components: type: object properties: version: - type: integer - format: int64 + type: string minimum: 0 example: 11 name: diff --git a/openapi/v1.yaml b/openapi/v1.yaml index ea016a195..3ae81b27d 100644 --- a/openapi/v1.yaml +++ b/openapi/v1.yaml @@ -131,7 +131,6 @@ paths: format: int64 minimum: 1 maximum: 1000 - default: 15 - name: page_size x-speakeasy-ignore: true in: query @@ -144,7 +143,6 @@ paths: format: int64 minimum: 1 maximum: 1000 - default: 15 deprecated: true - name: after in: query @@ -656,7 +654,6 @@ paths: format: int64 minimum: 1 maximum: 1000 - default: 15 - name: page_size x-speakeasy-ignore: true in: query @@ -669,7 +666,6 @@ paths: format: int64 minimum: 1 maximum: 1000 - default: 15 deprecated: true - name: after in: query @@ -1055,7 +1051,6 @@ paths: format: int64 minimum: 1 maximum: 1000 - default: 15 - name: after in: query description: >- @@ -1175,7 +1170,6 @@ paths: format: int64 minimum: 1 maximum: 1000 - default: 15 - name: page_size x-speakeasy-ignore: true in: query @@ -1188,7 +1182,6 @@ paths: format: int64 minimum: 1 maximum: 1000 - default: 15 deprecated: true - name: after in: query @@ -1875,8 +1868,7 @@ components: type: object properties: version: - type: integer - format: int64 + type: string minimum: 0 example: 11 name: diff --git a/pkg/client/.speakeasy/gen.lock b/pkg/client/.speakeasy/gen.lock index 198238b13..0bd31125e 100644 --- a/pkg/client/.speakeasy/gen.lock +++ b/pkg/client/.speakeasy/gen.lock @@ -10,7 +10,6 @@ management: features: go: additionalDependencies: 0.1.0 - constsAndDefaults: 0.1.4 core: 3.5.2 defaultEnabledRetries: 0.2.0 deprecations: 2.81.1 diff --git a/pkg/client/.speakeasy/gen.yaml b/pkg/client/.speakeasy/gen.yaml index c3cb8f2b1..bfd90d79b 100644 --- a/pkg/client/.speakeasy/gen.yaml +++ b/pkg/client/.speakeasy/gen.yaml @@ -15,7 +15,7 @@ generation: auth: oAuth2ClientCredentialsEnabled: true go: - version: 0.4.34 + version: 0.5.0 additionalDependencies: {} allowUnknownFieldsInWeakUnions: false clientServerStatusCodesAsErrors: true diff --git a/pkg/client/docs/models/components/migrationinfo.md b/pkg/client/docs/models/components/migrationinfo.md index aabaecf30..2155fbbaf 100644 --- a/pkg/client/docs/models/components/migrationinfo.md +++ b/pkg/client/docs/models/components/migrationinfo.md @@ -5,7 +5,7 @@ | Field | Type | Required | Description | Example | | ----------------------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------- | -| `Version` | **int64* | :heavy_minus_sign: | N/A | 11 | +| `Version` | **string* | :heavy_minus_sign: | N/A | 11 | | `Name` | **string* | :heavy_minus_sign: | N/A | migrations:001 | | `Date` | [*time.Time](https://pkg.go.dev/time#Time) | :heavy_minus_sign: | N/A | | | `State` | [*components.State](../../models/components/state.md) | :heavy_minus_sign: | N/A | | \ No newline at end of file diff --git a/pkg/client/formance.go b/pkg/client/formance.go index 4aa7b84a6..65656349a 100644 --- a/pkg/client/formance.go +++ b/pkg/client/formance.go @@ -143,9 +143,9 @@ func New(opts ...SDKOption) *Formance { sdkConfiguration: sdkConfiguration{ Language: "go", OpenAPIDocVersion: "v1", - SDKVersion: "0.4.34", + SDKVersion: "0.5.0", GenVersion: "2.384.1", - UserAgent: "speakeasy-sdk/go 0.4.34 2.384.1 v1 github.com/formancehq/ledger/pkg/client", + UserAgent: "speakeasy-sdk/go 0.5.0 2.384.1 v1 github.com/formancehq/ledger/pkg/client", Hooks: hooks.New(), }, } diff --git a/pkg/client/models/components/migrationinfo.go b/pkg/client/models/components/migrationinfo.go index 2ae19da20..b7adfd74d 100644 --- a/pkg/client/models/components/migrationinfo.go +++ b/pkg/client/models/components/migrationinfo.go @@ -36,7 +36,7 @@ func (e *State) UnmarshalJSON(data []byte) error { } type MigrationInfo struct { - Version *int64 `json:"version,omitempty"` + Version *string `json:"version,omitempty"` Name *string `json:"name,omitempty"` Date *time.Time `json:"date,omitempty"` State *State `json:"state,omitempty"` @@ -53,7 +53,7 @@ func (m *MigrationInfo) UnmarshalJSON(data []byte) error { return nil } -func (o *MigrationInfo) GetVersion() *int64 { +func (o *MigrationInfo) GetVersion() *string { if o == nil { return nil } diff --git a/pkg/client/models/operations/getbalances.go b/pkg/client/models/operations/getbalances.go index 59701f3cc..b67590975 100644 --- a/pkg/client/models/operations/getbalances.go +++ b/pkg/client/models/operations/getbalances.go @@ -3,7 +3,6 @@ package operations import ( - "github.com/formancehq/ledger/pkg/client/internal/utils" "github.com/formancehq/ledger/pkg/client/models/components" ) @@ -14,7 +13,7 @@ type GetBalancesRequest struct { Address *string `queryParam:"style=form,explode=true,name=address"` // The maximum number of results to return per page. // - PageSize *int64 `default:"15" queryParam:"style=form,explode=true,name=pageSize"` + PageSize *int64 `queryParam:"style=form,explode=true,name=pageSize"` // Pagination cursor, will return accounts after given address, in descending order. After *string `queryParam:"style=form,explode=true,name=after"` // Parameter used in pagination requests. Maximum page size is set to 1000. @@ -25,17 +24,6 @@ type GetBalancesRequest struct { Cursor *string `queryParam:"style=form,explode=true,name=cursor"` } -func (g GetBalancesRequest) MarshalJSON() ([]byte, error) { - return utils.MarshalJSON(g, "", false) -} - -func (g *GetBalancesRequest) UnmarshalJSON(data []byte) error { - if err := utils.UnmarshalJSON(data, &g, "", false, false); err != nil { - return err - } - return nil -} - func (o *GetBalancesRequest) GetLedger() string { if o == nil { return "" diff --git a/pkg/client/models/operations/listaccounts.go b/pkg/client/models/operations/listaccounts.go index 3fb8b6cf3..cddea4d6e 100644 --- a/pkg/client/models/operations/listaccounts.go +++ b/pkg/client/models/operations/listaccounts.go @@ -3,7 +3,6 @@ package operations import ( - "github.com/formancehq/ledger/pkg/client/internal/utils" "github.com/formancehq/ledger/pkg/client/models/components" "github.com/formancehq/ledger/pkg/client/models/sdkerrors" ) @@ -13,7 +12,7 @@ type ListAccountsRequest struct { Ledger string `pathParam:"style=simple,explode=false,name=ledger"` // The maximum number of results to return per page. // - PageSize *int64 `default:"15" queryParam:"style=form,explode=true,name=pageSize"` + PageSize *int64 `queryParam:"style=form,explode=true,name=pageSize"` // Pagination cursor, will return accounts after given address, in descending order. After *string `queryParam:"style=form,explode=true,name=after"` // Filter accounts by address pattern (regular expression placed between ^ and $). @@ -39,17 +38,6 @@ type ListAccountsRequest struct { PaginationToken *string `queryParam:"style=form,explode=true,name=pagination_token"` } -func (l ListAccountsRequest) MarshalJSON() ([]byte, error) { - return utils.MarshalJSON(l, "", false) -} - -func (l *ListAccountsRequest) UnmarshalJSON(data []byte) error { - if err := utils.UnmarshalJSON(data, &l, "", false, false); err != nil { - return err - } - return nil -} - func (o *ListAccountsRequest) GetLedger() string { if o == nil { return "" diff --git a/pkg/client/models/operations/listlogs.go b/pkg/client/models/operations/listlogs.go index 9c090226e..1b5133e5b 100644 --- a/pkg/client/models/operations/listlogs.go +++ b/pkg/client/models/operations/listlogs.go @@ -13,7 +13,7 @@ type ListLogsRequest struct { Ledger string `pathParam:"style=simple,explode=false,name=ledger"` // The maximum number of results to return per page. // - PageSize *int64 `default:"15" queryParam:"style=form,explode=true,name=pageSize"` + PageSize *int64 `queryParam:"style=form,explode=true,name=pageSize"` // Pagination cursor, will return the logs after a given ID. (in descending order). After *string `queryParam:"style=form,explode=true,name=after"` // Filter transactions that occurred after this timestamp. diff --git a/pkg/client/models/operations/listtransactions.go b/pkg/client/models/operations/listtransactions.go index f1df0c690..e3613f84d 100644 --- a/pkg/client/models/operations/listtransactions.go +++ b/pkg/client/models/operations/listtransactions.go @@ -13,7 +13,7 @@ type ListTransactionsRequest struct { Ledger string `pathParam:"style=simple,explode=false,name=ledger"` // The maximum number of results to return per page. // - PageSize *int64 `default:"15" queryParam:"style=form,explode=true,name=pageSize"` + PageSize *int64 `queryParam:"style=form,explode=true,name=pageSize"` // Pagination cursor, will return transactions after given txid (in descending order). After *string `queryParam:"style=form,explode=true,name=after"` // Find transactions by reference field. From d7ba7d81d38255b18003fd785b55d8b7e62e68f7 Mon Sep 17 00:00:00 2001 From: Ragot Geoffrey Date: Tue, 26 Nov 2024 10:34:41 +0100 Subject: [PATCH 44/71] feat: migrations post stateless version (#515) * feat: migrate old data * fix: dependencies * chore: some fix * fix: slow migrations * chore: reorder migrations and clean next-minor todos * feat: adapt for v2.2 merge --- go.mod | 3 +- internal/storage/bucket/bucket.go | 1 + internal/storage/bucket/default_bucket.go | 11 ++- .../bucket/migrations/1-fix-trigger/up.sql | 2 +- .../migrations/11-make-stateless/up.sql | 3 - .../16-moves-fill-transaction-id/notes.yaml | 1 + .../16-moves-fill-transaction-id/up.sql | 44 +++++++++ .../up_tests_after.sql | 0 .../up_tests_before.sql | 0 .../notes.yaml | 1 + .../17-transactions-fill-inserted-at/up.sql | 59 +++++++++++ .../up_tests_after.sql | 0 .../up_tests_before.sql | 0 .../18-transactions-fill-pcv/notes.yaml | 1 + .../18-transactions-fill-pcv/up.sql | 62 ++++++++++++ .../up_tests_after.sql | 0 .../up_tests_before.sql | 0 .../notes.yaml | 1 + .../19-accounts-volumes-fill-history/up.sql | 54 ++++++++++ .../up_tests_after.sql | 0 .../up_tests_before.sql | 0 .../2-fix-volumes-aggregation/up.sql | 3 +- .../notes.yaml | 1 + .../up.sql | 44 +++++++++ .../up_tests_after.sql | 0 .../up_tests_before.sql | 0 .../notes.yaml | 1 + .../21-accounts-metadata-fill-address/up.sql | 44 +++++++++ .../up_tests_after.sql | 0 .../up_tests_before.sql | 0 .../22-logs-fill-memento/notes.yaml | 1 + .../migrations/22-logs-fill-memento/up.sql | 38 +++++++ .../22-logs-fill-memento/up_tests_after.sql | 0 .../22-logs-fill-memento/up_tests_before.sql | 0 internal/storage/driver/adapters.go | 10 +- .../storage/driver/buckets_generated_test.go | 15 +++ internal/storage/ledger/balances.go | 2 +- internal/storage/ledger/legacy/adapters.go | 66 ++++++++++--- internal/storage/ledger/store.go | 4 + internal/storage/ledger/transactions.go | 5 - internal/storage/ledger/transactions_test.go | 99 ------------------- test/e2e/api_accounts_list_test.go | 2 +- test/e2e/api_transactions_list_test.go | 4 +- 43 files changed, 449 insertions(+), 133 deletions(-) create mode 100644 internal/storage/bucket/migrations/16-moves-fill-transaction-id/notes.yaml create mode 100644 internal/storage/bucket/migrations/16-moves-fill-transaction-id/up.sql create mode 100644 internal/storage/bucket/migrations/16-moves-fill-transaction-id/up_tests_after.sql create mode 100644 internal/storage/bucket/migrations/16-moves-fill-transaction-id/up_tests_before.sql create mode 100644 internal/storage/bucket/migrations/17-transactions-fill-inserted-at/notes.yaml create mode 100644 internal/storage/bucket/migrations/17-transactions-fill-inserted-at/up.sql create mode 100644 internal/storage/bucket/migrations/17-transactions-fill-inserted-at/up_tests_after.sql create mode 100644 internal/storage/bucket/migrations/17-transactions-fill-inserted-at/up_tests_before.sql create mode 100644 internal/storage/bucket/migrations/18-transactions-fill-pcv/notes.yaml create mode 100644 internal/storage/bucket/migrations/18-transactions-fill-pcv/up.sql create mode 100644 internal/storage/bucket/migrations/18-transactions-fill-pcv/up_tests_after.sql create mode 100644 internal/storage/bucket/migrations/18-transactions-fill-pcv/up_tests_before.sql create mode 100644 internal/storage/bucket/migrations/19-accounts-volumes-fill-history/notes.yaml create mode 100644 internal/storage/bucket/migrations/19-accounts-volumes-fill-history/up.sql create mode 100644 internal/storage/bucket/migrations/19-accounts-volumes-fill-history/up_tests_after.sql create mode 100644 internal/storage/bucket/migrations/19-accounts-volumes-fill-history/up_tests_before.sql create mode 100644 internal/storage/bucket/migrations/20-transactions-metadata-fill-transaction-id/notes.yaml create mode 100644 internal/storage/bucket/migrations/20-transactions-metadata-fill-transaction-id/up.sql create mode 100644 internal/storage/bucket/migrations/20-transactions-metadata-fill-transaction-id/up_tests_after.sql create mode 100644 internal/storage/bucket/migrations/20-transactions-metadata-fill-transaction-id/up_tests_before.sql create mode 100644 internal/storage/bucket/migrations/21-accounts-metadata-fill-address/notes.yaml create mode 100644 internal/storage/bucket/migrations/21-accounts-metadata-fill-address/up.sql create mode 100644 internal/storage/bucket/migrations/21-accounts-metadata-fill-address/up_tests_after.sql create mode 100644 internal/storage/bucket/migrations/21-accounts-metadata-fill-address/up_tests_before.sql create mode 100644 internal/storage/bucket/migrations/22-logs-fill-memento/notes.yaml create mode 100644 internal/storage/bucket/migrations/22-logs-fill-memento/up.sql create mode 100644 internal/storage/bucket/migrations/22-logs-fill-memento/up_tests_after.sql create mode 100644 internal/storage/bucket/migrations/22-logs-fill-memento/up_tests_before.sql diff --git a/go.mod b/go.mod index cf7f1d2b9..af672e6af 100644 --- a/go.mod +++ b/go.mod @@ -49,10 +49,11 @@ require ( golang.org/x/sync v0.9.0 ) +require gopkg.in/yaml.v3 v3.0.1 // indirect + require ( github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/jackc/pgxlisten v0.0.0-20241005155529-9d952acd6a6c // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) require ( diff --git a/internal/storage/bucket/bucket.go b/internal/storage/bucket/bucket.go index e3188c3a4..cd63b3029 100644 --- a/internal/storage/bucket/bucket.go +++ b/internal/storage/bucket/bucket.go @@ -13,6 +13,7 @@ type Bucket interface { Migrate(ctx context.Context, minimalVersionReached chan struct{}, opts ...migrations.Option) error AddLedger(ctx context.Context, ledger ledger.Ledger) error HasMinimalVersion(ctx context.Context) (bool, error) + IsUpToDate(ctx context.Context) (bool, error) GetMigrationsInfo(ctx context.Context) ([]migrations.Info, error) } diff --git a/internal/storage/bucket/default_bucket.go b/internal/storage/bucket/default_bucket.go index 17805b41d..467b8ee36 100644 --- a/internal/storage/bucket/default_bucket.go +++ b/internal/storage/bucket/default_bucket.go @@ -22,13 +22,16 @@ type DefaultBucket struct { tracer trace.Tracer } +func (b *DefaultBucket) IsUpToDate(ctx context.Context) (bool, error) { + return GetMigrator(b.db, b.name).IsUpToDate(ctx) +} + func (b *DefaultBucket) Migrate(ctx context.Context, minimalVersionReached chan struct{}, options ...migrations.Option) error { return migrate(ctx, b.tracer, b.db, b.name, minimalVersionReached, options...) } func (b *DefaultBucket) HasMinimalVersion(ctx context.Context) (bool, error) { - migrator := GetMigrator(b.db, b.name) - lastVersion, err := migrator.GetLastVersion(ctx) + lastVersion, err := b.GetLastVersion(ctx) if err != nil { return false, err } @@ -36,6 +39,10 @@ func (b *DefaultBucket) HasMinimalVersion(ctx context.Context) (bool, error) { return lastVersion >= MinimalSchemaVersion, nil } +func (b *DefaultBucket) GetLastVersion(ctx context.Context) (int, error) { + return GetMigrator(b.db, b.name).GetLastVersion(ctx) +} + func (b *DefaultBucket) GetMigrationsInfo(ctx context.Context) ([]migrations.Info, error) { return GetMigrator(b.db, b.name).GetMigrations(ctx) } diff --git a/internal/storage/bucket/migrations/1-fix-trigger/up.sql b/internal/storage/bucket/migrations/1-fix-trigger/up.sql index 73866f58c..cbef57036 100644 --- a/internal/storage/bucket/migrations/1-fix-trigger/up.sql +++ b/internal/storage/bucket/migrations/1-fix-trigger/up.sql @@ -29,4 +29,4 @@ begin posting ->> 'destination', posting ->> 'asset', (posting ->> 'amount')::numeric, false, _destination_exists); end; -$$ set search_path from current; +$$ set search_path from current; \ No newline at end of file diff --git a/internal/storage/bucket/migrations/11-make-stateless/up.sql b/internal/storage/bucket/migrations/11-make-stateless/up.sql index 182ce53b4..251d1448a 100644 --- a/internal/storage/bucket/migrations/11-make-stateless/up.sql +++ b/internal/storage/bucket/migrations/11-make-stateless/up.sql @@ -200,8 +200,6 @@ execute procedure set_compat_on_transactions_metadata(); alter table transactions add column post_commit_volumes jsonb, --- todo: set in subsequent migration `default transaction_date()`, --- otherwise the function is called for every existing lines add column inserted_at timestamp without time zone, alter column timestamp set default transaction_date() -- todo: we should change the type of this column, but actually it cause a full lock of the table @@ -363,7 +361,6 @@ from (select row_number() over () as number, v.value select null) v) data $$ set search_path from current; --- todo(next-minor): remove that on future version when the table will have this default value (need to fill nulls before) create or replace function set_transaction_inserted_at() returns trigger security definer language plpgsql diff --git a/internal/storage/bucket/migrations/16-moves-fill-transaction-id/notes.yaml b/internal/storage/bucket/migrations/16-moves-fill-transaction-id/notes.yaml new file mode 100644 index 000000000..4e7ed8eef --- /dev/null +++ b/internal/storage/bucket/migrations/16-moves-fill-transaction-id/notes.yaml @@ -0,0 +1 @@ +name: Fill transaction ids of table moves diff --git a/internal/storage/bucket/migrations/16-moves-fill-transaction-id/up.sql b/internal/storage/bucket/migrations/16-moves-fill-transaction-id/up.sql new file mode 100644 index 000000000..5c486cf6e --- /dev/null +++ b/internal/storage/bucket/migrations/16-moves-fill-transaction-id/up.sql @@ -0,0 +1,44 @@ +do $$ + declare + _batch_size integer := 100; + _max integer; + begin + set search_path = '{{.Schema}}'; + + create index moves_transactions_id on moves(transactions_id); + + select count(seq) + from moves + where transactions_id is null + into _max; + + perform pg_notify('migrations-{{ .Schema }}', 'init: ' || _max); + loop + + with _outdated_moves as ( + select * + from moves + where transactions_id is null + limit _batch_size + ) + update moves + set transactions_id = ( + select id + from transactions + where seq = moves.transactions_seq + ) + from _outdated_moves + where moves.seq in (_outdated_moves.seq); + + exit when not found; + + perform pg_notify('migrations-{{ .Schema }}', 'continue: ' || _batch_size); + + commit ; + end loop; + + alter table moves + alter column transactions_id set not null; + end +$$ +language plpgsql; \ No newline at end of file diff --git a/internal/storage/bucket/migrations/16-moves-fill-transaction-id/up_tests_after.sql b/internal/storage/bucket/migrations/16-moves-fill-transaction-id/up_tests_after.sql new file mode 100644 index 000000000..e69de29bb diff --git a/internal/storage/bucket/migrations/16-moves-fill-transaction-id/up_tests_before.sql b/internal/storage/bucket/migrations/16-moves-fill-transaction-id/up_tests_before.sql new file mode 100644 index 000000000..e69de29bb diff --git a/internal/storage/bucket/migrations/17-transactions-fill-inserted-at/notes.yaml b/internal/storage/bucket/migrations/17-transactions-fill-inserted-at/notes.yaml new file mode 100644 index 000000000..69c43fb23 --- /dev/null +++ b/internal/storage/bucket/migrations/17-transactions-fill-inserted-at/notes.yaml @@ -0,0 +1 @@ +name: Fill inserted_at column of transactions table diff --git a/internal/storage/bucket/migrations/17-transactions-fill-inserted-at/up.sql b/internal/storage/bucket/migrations/17-transactions-fill-inserted-at/up.sql new file mode 100644 index 000000000..6adca135f --- /dev/null +++ b/internal/storage/bucket/migrations/17-transactions-fill-inserted-at/up.sql @@ -0,0 +1,59 @@ +do $$ + declare + _batch_size integer := 100; + _date timestamp without time zone; + _count integer := 0; + begin + --todo: take explicit advisory lock to avoid concurrent migrations when the service is killed + set search_path = '{{.Schema}}'; + + -- select the date where the "11-make-stateless" migration has been applied + select tstamp into _date + from _system.goose_db_version + where version_id = 12; + + create temporary table logs_transactions as + select id, ledger, date, (data->'transaction'->>'id')::bigint as transaction_id + from logs + where date <= _date; + + create index on logs_transactions (ledger, transaction_id) include (id, date); + + select count(*) into _count + from logs_transactions; + + perform pg_notify('migrations-{{ .Schema }}', 'init: ' || _count); + + for i in 0.._count by _batch_size loop + -- disable triggers + set session_replication_role = replica; + + with _rows as ( + select * + from logs_transactions + order by ledger, transaction_id + offset i + limit _batch_size + ) + update transactions + set inserted_at = _rows.date + from _rows + where transactions.ledger = _rows.ledger and transactions.id = _rows.transaction_id; + + -- enable triggers + set session_replication_role = default; + + commit; + + perform pg_notify('migrations-{{ .Schema }}', 'continue: ' || _batch_size); + end loop; + + drop table logs_transactions; + + alter table transactions + alter column inserted_at set default transaction_date(); + + drop trigger set_transaction_inserted_at on transactions; + drop function set_transaction_inserted_at; + end +$$; \ No newline at end of file diff --git a/internal/storage/bucket/migrations/17-transactions-fill-inserted-at/up_tests_after.sql b/internal/storage/bucket/migrations/17-transactions-fill-inserted-at/up_tests_after.sql new file mode 100644 index 000000000..e69de29bb diff --git a/internal/storage/bucket/migrations/17-transactions-fill-inserted-at/up_tests_before.sql b/internal/storage/bucket/migrations/17-transactions-fill-inserted-at/up_tests_before.sql new file mode 100644 index 000000000..e69de29bb diff --git a/internal/storage/bucket/migrations/18-transactions-fill-pcv/notes.yaml b/internal/storage/bucket/migrations/18-transactions-fill-pcv/notes.yaml new file mode 100644 index 000000000..4a8274783 --- /dev/null +++ b/internal/storage/bucket/migrations/18-transactions-fill-pcv/notes.yaml @@ -0,0 +1 @@ +name: Fill post_commit_volumes column of transactions table diff --git a/internal/storage/bucket/migrations/18-transactions-fill-pcv/up.sql b/internal/storage/bucket/migrations/18-transactions-fill-pcv/up.sql new file mode 100644 index 000000000..39dd9e9f4 --- /dev/null +++ b/internal/storage/bucket/migrations/18-transactions-fill-pcv/up.sql @@ -0,0 +1,62 @@ +do $$ + declare + _batch_size integer := 100; + _count integer; + begin + set search_path = '{{.Schema}}'; + + select count(id) + from transactions + where post_commit_volumes is null + into _count; + + perform pg_notify('migrations-{{ .Schema }}', 'init: ' || _count); + + loop + -- disable triggers + set session_replication_role = replica; + + with _outdated_transactions as ( + select id + from transactions + where post_commit_volumes is null + limit _batch_size + ) + update transactions + set post_commit_volumes = ( + select public.aggregate_objects(post_commit_volumes::jsonb) as post_commit_volumes + from ( + select accounts_address, json_build_object(accounts_address, post_commit_volumes) post_commit_volumes + from ( + select accounts_address, json_build_object(asset, post_commit_volumes) as post_commit_volumes + from ( + select distinct on (accounts_address, asset) + accounts_address, + asset, + first_value(post_commit_volumes) over ( + partition by accounts_address, asset + order by seq desc + ) as post_commit_volumes + from moves + where transactions_id = transactions.id and ledger = transactions.ledger + ) moves + ) values + ) values + ) + from _outdated_transactions + where transactions.id in (_outdated_transactions.id); + + -- enable triggers + set session_replication_role = default; + + exit when not found; + + commit; + + perform pg_notify('migrations-{{ .Schema }}', 'continue: ' || _batch_size); + end loop; + + alter table transactions + alter column post_commit_volumes set not null; + end +$$; \ No newline at end of file diff --git a/internal/storage/bucket/migrations/18-transactions-fill-pcv/up_tests_after.sql b/internal/storage/bucket/migrations/18-transactions-fill-pcv/up_tests_after.sql new file mode 100644 index 000000000..e69de29bb diff --git a/internal/storage/bucket/migrations/18-transactions-fill-pcv/up_tests_before.sql b/internal/storage/bucket/migrations/18-transactions-fill-pcv/up_tests_before.sql new file mode 100644 index 000000000..e69de29bb diff --git a/internal/storage/bucket/migrations/19-accounts-volumes-fill-history/notes.yaml b/internal/storage/bucket/migrations/19-accounts-volumes-fill-history/notes.yaml new file mode 100644 index 000000000..35624b619 --- /dev/null +++ b/internal/storage/bucket/migrations/19-accounts-volumes-fill-history/notes.yaml @@ -0,0 +1 @@ +name: Populate accounts_volumes table with historic data diff --git a/internal/storage/bucket/migrations/19-accounts-volumes-fill-history/up.sql b/internal/storage/bucket/migrations/19-accounts-volumes-fill-history/up.sql new file mode 100644 index 000000000..f77f2a0ec --- /dev/null +++ b/internal/storage/bucket/migrations/19-accounts-volumes-fill-history/up.sql @@ -0,0 +1,54 @@ +do $$ + declare + _count integer; + _batch_size integer := 100; + begin + set search_path = '{{.Schema}}'; + + create temporary table tmp_volumes as + select distinct on (ledger, accounts_address, asset) + ledger, + accounts_address, + asset, + first_value(post_commit_volumes) over ( + partition by ledger, accounts_address, asset + order by seq desc + ) as post_commit_volumes + from moves + where not exists( + select + from accounts_volumes + where ledger = moves.ledger + and asset = moves.asset + and accounts_address = moves.accounts_address + ); + + select count(*) + from tmp_volumes + into _count; + + perform pg_notify('migrations-{{ .Schema }}', 'init: ' || _count); + + raise info '_count: %', _count; + + for i in 0.._count by _batch_size loop + with _rows as ( + select * + from tmp_volumes + offset i + limit _batch_size + ) + insert into accounts_volumes (ledger, accounts_address, asset, input, output) + select ledger, accounts_address, asset, (post_commit_volumes).inputs, (post_commit_volumes).outputs + from _rows + on conflict do nothing; -- can be inserted by a concurrent transaction + + commit; + + perform pg_notify('migrations-{{ .Schema }}', 'continue: ' || _batch_size); + + end loop; + + drop table tmp_volumes; + end +$$; \ No newline at end of file diff --git a/internal/storage/bucket/migrations/19-accounts-volumes-fill-history/up_tests_after.sql b/internal/storage/bucket/migrations/19-accounts-volumes-fill-history/up_tests_after.sql new file mode 100644 index 000000000..e69de29bb diff --git a/internal/storage/bucket/migrations/19-accounts-volumes-fill-history/up_tests_before.sql b/internal/storage/bucket/migrations/19-accounts-volumes-fill-history/up_tests_before.sql new file mode 100644 index 000000000..e69de29bb diff --git a/internal/storage/bucket/migrations/2-fix-volumes-aggregation/up.sql b/internal/storage/bucket/migrations/2-fix-volumes-aggregation/up.sql index 9cdc09172..986b42575 100644 --- a/internal/storage/bucket/migrations/2-fix-volumes-aggregation/up.sql +++ b/internal/storage/bucket/migrations/2-fix-volumes-aggregation/up.sql @@ -22,5 +22,4 @@ with all_assets as (select v.v as asset ) m on true) select moves.asset, moves.post_commit_volumes from moves -$$ set search_path from current; - +$$ set search_path from current; \ No newline at end of file diff --git a/internal/storage/bucket/migrations/20-transactions-metadata-fill-transaction-id/notes.yaml b/internal/storage/bucket/migrations/20-transactions-metadata-fill-transaction-id/notes.yaml new file mode 100644 index 000000000..449dcfd17 --- /dev/null +++ b/internal/storage/bucket/migrations/20-transactions-metadata-fill-transaction-id/notes.yaml @@ -0,0 +1 @@ +name: Fill transactions_id column of transactions_metadata table diff --git a/internal/storage/bucket/migrations/20-transactions-metadata-fill-transaction-id/up.sql b/internal/storage/bucket/migrations/20-transactions-metadata-fill-transaction-id/up.sql new file mode 100644 index 000000000..7823fa915 --- /dev/null +++ b/internal/storage/bucket/migrations/20-transactions-metadata-fill-transaction-id/up.sql @@ -0,0 +1,44 @@ + +do $$ + declare + _batch_size integer := 100; + _count integer; + begin + set search_path = '{{.Schema}}'; + + select count(seq) + from transactions_metadata + where transactions_id is null + into _count; + + perform pg_notify('migrations-{{ .Schema }}', 'init: ' || _count); + + loop + with _outdated_transactions_metadata as ( + select seq + from transactions_metadata + where transactions_id is null + limit _batch_size + ) + update transactions_metadata + set transactions_id = ( + select id + from transactions + where transactions_metadata.transactions_seq = seq + ) + from _outdated_transactions_metadata + where transactions_metadata.seq in (_outdated_transactions_metadata.seq); + + exit when not found; + + commit; + + perform pg_notify('migrations-{{ .Schema }}', 'continue: ' || _batch_size); + + end loop; + + alter table transactions_metadata + alter column transactions_id set not null ; + end +$$; + diff --git a/internal/storage/bucket/migrations/20-transactions-metadata-fill-transaction-id/up_tests_after.sql b/internal/storage/bucket/migrations/20-transactions-metadata-fill-transaction-id/up_tests_after.sql new file mode 100644 index 000000000..e69de29bb diff --git a/internal/storage/bucket/migrations/20-transactions-metadata-fill-transaction-id/up_tests_before.sql b/internal/storage/bucket/migrations/20-transactions-metadata-fill-transaction-id/up_tests_before.sql new file mode 100644 index 000000000..e69de29bb diff --git a/internal/storage/bucket/migrations/21-accounts-metadata-fill-address/notes.yaml b/internal/storage/bucket/migrations/21-accounts-metadata-fill-address/notes.yaml new file mode 100644 index 000000000..f599539a8 --- /dev/null +++ b/internal/storage/bucket/migrations/21-accounts-metadata-fill-address/notes.yaml @@ -0,0 +1 @@ +name: Fill accounts_address column of accounts_metadata table diff --git a/internal/storage/bucket/migrations/21-accounts-metadata-fill-address/up.sql b/internal/storage/bucket/migrations/21-accounts-metadata-fill-address/up.sql new file mode 100644 index 000000000..752ef3cfd --- /dev/null +++ b/internal/storage/bucket/migrations/21-accounts-metadata-fill-address/up.sql @@ -0,0 +1,44 @@ + +do $$ + declare + _batch_size integer := 100; + _count integer; + begin + set search_path = '{{.Schema}}'; + + select count(seq) + from accounts_metadata + where accounts_address is null + into _count; + + perform pg_notify('migrations-{{ .Schema }}', 'init: ' || _count); + + loop + with _outdated_accounts_metadata as ( + select seq + from accounts_metadata + where accounts_address is null + limit _batch_size + ) + update accounts_metadata + set accounts_address = ( + select address + from accounts + where accounts_metadata.accounts_seq = seq + ) + from _outdated_accounts_metadata + where accounts_metadata.seq in (_outdated_accounts_metadata.seq); + + exit when not found; + + commit; + + perform pg_notify('migrations-{{ .Schema }}', 'continue: ' || _batch_size); + + end loop; + + alter table accounts_metadata + alter column accounts_address set not null ; + end +$$; + diff --git a/internal/storage/bucket/migrations/21-accounts-metadata-fill-address/up_tests_after.sql b/internal/storage/bucket/migrations/21-accounts-metadata-fill-address/up_tests_after.sql new file mode 100644 index 000000000..e69de29bb diff --git a/internal/storage/bucket/migrations/21-accounts-metadata-fill-address/up_tests_before.sql b/internal/storage/bucket/migrations/21-accounts-metadata-fill-address/up_tests_before.sql new file mode 100644 index 000000000..e69de29bb diff --git a/internal/storage/bucket/migrations/22-logs-fill-memento/notes.yaml b/internal/storage/bucket/migrations/22-logs-fill-memento/notes.yaml new file mode 100644 index 000000000..1f7fd9415 --- /dev/null +++ b/internal/storage/bucket/migrations/22-logs-fill-memento/notes.yaml @@ -0,0 +1 @@ +name: Fill memento column of logs table diff --git a/internal/storage/bucket/migrations/22-logs-fill-memento/up.sql b/internal/storage/bucket/migrations/22-logs-fill-memento/up.sql new file mode 100644 index 000000000..7923084b3 --- /dev/null +++ b/internal/storage/bucket/migrations/22-logs-fill-memento/up.sql @@ -0,0 +1,38 @@ +do $$ + declare + _batch_size integer := 100; + _count integer; + begin + set search_path = '{{.Schema}}'; + + select count(seq) + from logs + where memento is null + into _count; + + perform pg_notify('migrations-{{ .Schema }}', 'init: ' || _count); + + loop + with _outdated_logs as ( + select seq + from logs + where memento is null + limit _batch_size + ) + update logs + set memento = convert_to(data::varchar, 'LATIN1')::bytea + from _outdated_logs + where logs.seq in (_outdated_logs.seq); + + exit when not found; + + commit; + + perform pg_notify('migrations-{{ .Schema }}', 'continue: ' || _batch_size); + end loop; + + alter table logs + alter column memento set not null; + end +$$; + diff --git a/internal/storage/bucket/migrations/22-logs-fill-memento/up_tests_after.sql b/internal/storage/bucket/migrations/22-logs-fill-memento/up_tests_after.sql new file mode 100644 index 000000000..e69de29bb diff --git a/internal/storage/bucket/migrations/22-logs-fill-memento/up_tests_before.sql b/internal/storage/bucket/migrations/22-logs-fill-memento/up_tests_before.sql new file mode 100644 index 000000000..e69de29bb diff --git a/internal/storage/driver/adapters.go b/internal/storage/driver/adapters.go index 7150c6503..304d9b7f6 100644 --- a/internal/storage/driver/adapters.go +++ b/internal/storage/driver/adapters.go @@ -2,6 +2,7 @@ package driver import ( "context" + "fmt" ledgerstore "github.com/formancehq/ledger/internal/storage/ledger/legacy" ledger "github.com/formancehq/ledger/internal" @@ -19,7 +20,12 @@ func (d *DefaultStorageDriverAdapter) OpenLedger(ctx context.Context, name strin return nil, nil, err } - return ledgerstore.NewDefaultStoreAdapter(store), l, nil + isUpToDate, err := store.GetBucket().IsUpToDate(ctx) + if err != nil { + return nil, nil, fmt.Errorf("checking if bucket is up to date: %w", err) + } + + return ledgerstore.NewDefaultStoreAdapter(isUpToDate, store), l, nil } func (d *DefaultStorageDriverAdapter) CreateLedger(ctx context.Context, l *ledger.Ledger) error { @@ -31,4 +37,4 @@ func NewControllerStorageDriverAdapter(d *Driver) *DefaultStorageDriverAdapter { return &DefaultStorageDriverAdapter{Driver: d} } -var _ systemcontroller.Store = (*DefaultStorageDriverAdapter)(nil) \ No newline at end of file +var _ systemcontroller.Store = (*DefaultStorageDriverAdapter)(nil) diff --git a/internal/storage/driver/buckets_generated_test.go b/internal/storage/driver/buckets_generated_test.go index 92e52b8f1..e26f69bce 100644 --- a/internal/storage/driver/buckets_generated_test.go +++ b/internal/storage/driver/buckets_generated_test.go @@ -82,6 +82,21 @@ func (mr *MockBucketMockRecorder) HasMinimalVersion(ctx any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasMinimalVersion", reflect.TypeOf((*MockBucket)(nil).HasMinimalVersion), ctx) } +// IsUpToDate mocks base method. +func (m *MockBucket) IsUpToDate(ctx context.Context) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsUpToDate", ctx) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsUpToDate indicates an expected call of IsUpToDate. +func (mr *MockBucketMockRecorder) IsUpToDate(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsUpToDate", reflect.TypeOf((*MockBucket)(nil).IsUpToDate), ctx) +} + // Migrate mocks base method. func (m *MockBucket) Migrate(ctx context.Context, minimalVersionReached chan struct{}, opts ...migrations.Option) error { m.ctrl.T.Helper() diff --git a/internal/storage/ledger/balances.go b/internal/storage/ledger/balances.go index 442c984dc..1ddbf6eee 100644 --- a/internal/storage/ledger/balances.go +++ b/internal/storage/ledger/balances.go @@ -157,7 +157,7 @@ func (s *Store) selectAccountWithAggregatedVolumes(date *time.Time, useInsertion TableExpr("(?) values", selectAccountWithAssetAndVolumes). Group("accounts_address"). Column("accounts_address"). - ColumnExpr("aggregate_objects(json_build_object(asset, json_build_object('input', (volumes).inputs, 'output', (volumes).outputs))::jsonb) as " + alias) + ColumnExpr("public.aggregate_objects(json_build_object(asset, json_build_object('input', (volumes).inputs, 'output', (volumes).outputs))::jsonb) as " + alias) } func (s *Store) SelectAggregatedBalances(date *time.Time, useInsertionDate bool, builder query.Builder) *bun.SelectQuery { diff --git a/internal/storage/ledger/legacy/adapters.go b/internal/storage/ledger/legacy/adapters.go index f63695b4b..e6ad4b72a 100644 --- a/internal/storage/ledger/legacy/adapters.go +++ b/internal/storage/ledger/legacy/adapters.go @@ -14,8 +14,9 @@ import ( ) type DefaultStoreAdapter struct { - newStore *ledgerstore.Store - legacyStore *Store + newStore *ledgerstore.Store + legacyStore *Store + isFullUpToDate bool } func (d *DefaultStoreAdapter) GetDB() bun.IDB { @@ -63,7 +64,11 @@ func (d *DefaultStoreAdapter) LockLedger(ctx context.Context) error { } func (d *DefaultStoreAdapter) ListLogs(ctx context.Context, q ledgercontroller.GetLogsQuery) (*bunpaginate.Cursor[ledger.Log], error) { - return d.legacyStore.GetLogs(ctx, q) + if !d.isFullUpToDate { + return d.legacyStore.GetLogs(ctx, q) + } + + return d.newStore.ListLogs(ctx, q) } func (d *DefaultStoreAdapter) ReadLogWithIdempotencyKey(ctx context.Context, ik string) (*ledger.Log, error) { @@ -71,35 +76,67 @@ func (d *DefaultStoreAdapter) ReadLogWithIdempotencyKey(ctx context.Context, ik } func (d *DefaultStoreAdapter) ListTransactions(ctx context.Context, q ledgercontroller.ListTransactionsQuery) (*bunpaginate.Cursor[ledger.Transaction], error) { - return d.legacyStore.GetTransactions(ctx, q) + if !d.isFullUpToDate { + return d.legacyStore.GetTransactions(ctx, q) + } + + return d.newStore.ListTransactions(ctx, q) } func (d *DefaultStoreAdapter) CountTransactions(ctx context.Context, q ledgercontroller.ListTransactionsQuery) (int, error) { - return d.legacyStore.CountTransactions(ctx, q) + if !d.isFullUpToDate { + return d.legacyStore.CountTransactions(ctx, q) + } + + return d.newStore.CountTransactions(ctx, q) } func (d *DefaultStoreAdapter) GetTransaction(ctx context.Context, query ledgercontroller.GetTransactionQuery) (*ledger.Transaction, error) { - return d.legacyStore.GetTransactionWithVolumes(ctx, query) + if !d.isFullUpToDate { + return d.legacyStore.GetTransactionWithVolumes(ctx, query) + } + + return d.newStore.GetTransaction(ctx, query) } func (d *DefaultStoreAdapter) CountAccounts(ctx context.Context, q ledgercontroller.ListAccountsQuery) (int, error) { - return d.legacyStore.CountAccounts(ctx, q) + if !d.isFullUpToDate { + return d.legacyStore.CountAccounts(ctx, q) + } + + return d.newStore.CountAccounts(ctx, q) } func (d *DefaultStoreAdapter) ListAccounts(ctx context.Context, q ledgercontroller.ListAccountsQuery) (*bunpaginate.Cursor[ledger.Account], error) { - return d.legacyStore.GetAccountsWithVolumes(ctx, q) + if !d.isFullUpToDate { + return d.legacyStore.GetAccountsWithVolumes(ctx, q) + } + + return d.newStore.ListAccounts(ctx, q) } func (d *DefaultStoreAdapter) GetAccount(ctx context.Context, q ledgercontroller.GetAccountQuery) (*ledger.Account, error) { - return d.legacyStore.GetAccountWithVolumes(ctx, q) + if !d.isFullUpToDate { + return d.legacyStore.GetAccountWithVolumes(ctx, q) + } + + return d.newStore.GetAccount(ctx, q) } func (d *DefaultStoreAdapter) GetAggregatedBalances(ctx context.Context, q ledgercontroller.GetAggregatedBalanceQuery) (ledger.BalancesByAssets, error) { - return d.legacyStore.GetAggregatedBalances(ctx, q) + if !d.isFullUpToDate { + return d.legacyStore.GetAggregatedBalances(ctx, q) + } + + return d.newStore.GetAggregatedBalances(ctx, q) } func (d *DefaultStoreAdapter) GetVolumesWithBalances(ctx context.Context, q ledgercontroller.GetVolumesWithBalancesQuery) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { - return d.legacyStore.GetVolumesWithBalances(ctx, q) + if !d.isFullUpToDate { + return d.legacyStore.GetVolumesWithBalances(ctx, q) + } + + return d.newStore.GetVolumesWithBalances(ctx, q) } func (d *DefaultStoreAdapter) IsUpToDate(ctx context.Context) (bool, error) { @@ -132,10 +169,11 @@ func (d *DefaultStoreAdapter) Rollback() error { return d.newStore.Rollback() } -func NewDefaultStoreAdapter(store *ledgerstore.Store) *DefaultStoreAdapter { +func NewDefaultStoreAdapter(isFullUpToDate bool, store *ledgerstore.Store) *DefaultStoreAdapter { return &DefaultStoreAdapter{ - newStore: store, - legacyStore: New(store.GetDB(), store.GetLedger().Bucket, store.GetLedger().Name), + isFullUpToDate: isFullUpToDate, + newStore: store, + legacyStore: New(store.GetDB(), store.GetLedger().Bucket, store.GetLedger().Name), } } diff --git a/internal/storage/ledger/store.go b/internal/storage/ledger/store.go index b15a0eafc..34a2b8809 100644 --- a/internal/storage/ledger/store.go +++ b/internal/storage/ledger/store.go @@ -86,6 +86,10 @@ func (s *Store) GetDB() bun.IDB { return s.db } +func (s *Store) GetBucket() bucket.Bucket { + return s.bucket +} + func (s *Store) GetPrefixedRelationName(v string) string { return fmt.Sprintf(`"%s".%s`, s.ledger.Bucket, v) } diff --git a/internal/storage/ledger/transactions.go b/internal/storage/ledger/transactions.go index 17f787705..e0fb7b612 100644 --- a/internal/storage/ledger/transactions.go +++ b/internal/storage/ledger/transactions.go @@ -407,11 +407,6 @@ func (s *Store) InsertTransaction(ctx context.Context, tx *ledger.Transaction) e if err.(postgres.ErrConstraintsFailed).GetConstraint() == "transactions_reference" { return nil, ledgercontroller.NewErrTransactionReferenceConflict(tx.Reference) } - case errors.Is(err, postgres.ErrRaisedException{}): - // todo(next-minor): remove this test - if err.(postgres.ErrRaisedException).GetMessage() == "duplicate reference" { - return nil, ledgercontroller.NewErrTransactionReferenceConflict(tx.Reference) - } default: return nil, err } diff --git a/internal/storage/ledger/transactions_test.go b/internal/storage/ledger/transactions_test.go index 4548947a0..32702020c 100644 --- a/internal/storage/ledger/transactions_test.go +++ b/internal/storage/ledger/transactions_test.go @@ -7,13 +7,6 @@ import ( "database/sql" "fmt" "github.com/alitto/pond" - "github.com/formancehq/go-libs/v2/bun/bunconnect" - "github.com/formancehq/ledger/internal/storage/bucket" - driver "github.com/formancehq/ledger/internal/storage/driver" - ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" - systemstore "github.com/formancehq/ledger/internal/storage/system" - "github.com/google/uuid" - "go.opentelemetry.io/otel/trace/noop" "math/big" "slices" "testing" @@ -610,98 +603,6 @@ func TestTransactionsInsert(t *testing.T) { require.Error(t, err) require.True(t, errors.Is(err, ledgercontroller.ErrTransactionReferenceConflict{})) }) - // todo(next-minor): remove this test - t.Run("check reference conflict with minimal store version", func(t *testing.T) { - t.Parallel() - - // Waiting for the pg server to be ready - <-srv.Done() - - // Create a dedicated database for this test as we want to run migrations only until the minimal schema version - db := srv.GetValue().NewDatabase(t) - bunDB, err := bunconnect.OpenSQLDB(ctx, db.ConnectionOptions()) - require.NoError(t, err) - - driver := driver.New( - ledgerstore.NewFactory(bunDB), - systemstore.New(bunDB), - bucket.NewDefaultFactory(bunDB), - ) - require.NoError(t, driver.Initialize(ctx)) - - ledgerName := uuid.NewString()[:8] - - l := ledger.MustNewWithDefault(ledgerName) - l.Bucket = ledgerName - - migrator := bucket.GetMigrator(bunDB, ledgerName) - for i := 0; i < bucket.MinimalSchemaVersion; i++ { - require.NoError(t, migrator.UpByOne(ctx)) - } - - b := bucket.NewDefault(bunDB, noop.Tracer{}, ledgerName) - err = b.AddLedger(ctx, l) - require.NoError(t, err) - - store := ledgerstore.New(bunDB, b, l) - - const nbTry = 100 - - for i := 0; i < nbTry; i++ { - errChan := make(chan error, 2) - - // Create a simple tx - tx1 := ledger.Transaction{ - TransactionData: ledger.TransactionData{ - Timestamp: now, - Reference: fmt.Sprintf("foo:%d", i), - Postings: []ledger.Posting{ - ledger.NewPosting("world", "bank", "USD/2", big.NewInt(100)), - }, - }, - } - go func() { - errChan <- store.InsertTransaction(ctx, &tx1) - }() - - // Create another tx with the same reference - tx2 := ledger.Transaction{ - TransactionData: ledger.TransactionData{ - Timestamp: now, - Reference: fmt.Sprintf("foo:%d", i), - Postings: []ledger.Posting{ - ledger.NewPosting("world", "bank", "USD/2", big.NewInt(100)), - }, - }, - } - go func() { - errChan <- store.InsertTransaction(ctx, &tx2) - }() - - select { - case err1 := <-errChan: - if err1 != nil { - require.True(t, errors.Is(err1, ledgercontroller.ErrTransactionReferenceConflict{})) - select { - case err2 := <-errChan: - require.NoError(t, err2) - case <-time.After(time.Second): - require.Fail(t, "should have received an error") - } - } else { - select { - case err2 := <-errChan: - require.Error(t, err2) - require.True(t, errors.Is(err2, ledgercontroller.ErrTransactionReferenceConflict{})) - case <-time.After(time.Second): - require.Fail(t, "should have received an error") - } - } - case <-time.After(time.Second): - require.Fail(t, "should have received an error") - } - } - }) t.Run("check denormalization", func(t *testing.T) { t.Parallel() diff --git a/test/e2e/api_accounts_list_test.go b/test/e2e/api_accounts_list_test.go index 0853e0132..7f11cc0ec 100644 --- a/test/e2e/api_accounts_list_test.go +++ b/test/e2e/api_accounts_list_test.go @@ -110,7 +110,7 @@ var _ = Context("Ledger accounts list API tests", func() { }, ) Expect(err).To(HaveOccurred()) - Expect(err).To(HaveErrorCode(string(components.V2ErrorsEnumInternal))) + Expect(err).To(HaveErrorCode(string(components.V2ErrorsEnumValidation))) }) It("should be countable on api", func() { response, err := CountAccounts( diff --git a/test/e2e/api_transactions_list_test.go b/test/e2e/api_transactions_list_test.go index bdbdd4c6d..a4ff42884 100644 --- a/test/e2e/api_transactions_list_test.go +++ b/test/e2e/api_transactions_list_test.go @@ -331,8 +331,8 @@ var _ = Context("Ledger transactions list API tests", func() { ) Expect(err).To(HaveOccurred()) }) - It("Should fail with "+string(components.V2ErrorsEnumInternal)+" error code", func() { - Expect(err).To(HaveErrorCode(string(components.V2ErrorsEnumInternal))) + It("Should fail with "+string(components.V2ErrorsEnumValidation)+" error code", func() { + Expect(err).To(HaveErrorCode(string(components.V2ErrorsEnumValidation))) }) }) }) From aeb7585d287068e1ad0a89947ff9ef3e8e3c8767 Mon Sep 17 00:00:00 2001 From: Ragot Geoffrey Date: Wed, 27 Nov 2024 13:28:40 +0100 Subject: [PATCH 45/71] feat: streamable bulk (#590) * feat: add stream at core level * feat: refactor api part to support switch on content types * refacto: bulk streams * feat: dedicated bulking package * feat: simplify * test: refactor and test * test: add some tests * fix: remove useless CI step * feat: add some traces * chore: clean unused indexes * fix: rolling upgrade tests --- .github/workflows/main.yml | 6 - docker-compose.yml | 5 + internal/README.md | 10 + internal/account.go | 4 + internal/api/bulking/bulker.go | 313 ++++++++++++ internal/api/bulking/bulker_test.go | 410 +++++++++++++++ internal/api/bulking/elements.go | 128 +++++ internal/api/bulking/factory.go | 12 + internal/api/bulking/handler_json.go | 148 ++++++ internal/api/bulking/handler_json_test.go | 129 +++++ internal/api/bulking/handler_text.go | 90 ++++ internal/api/bulking/handler_text_test.go | 98 ++++ internal/api/bulking/mocks.go | 2 + .../bulking/mocks_ledger_controller_test.go | 384 ++++++++++++++ internal/api/bulking/result.go | 9 + internal/api/bulking/text_stream.go | 83 +++ internal/api/bulking/text_stream_test.go | 180 +++++++ internal/api/common/errors.go | 19 + internal/api/module.go | 19 +- internal/api/router.go | 11 +- .../v1/controllers_accounts_add_metadata.go | 6 +- .../controllers_accounts_add_metadata_test.go | 7 +- internal/api/v1/controllers_accounts_count.go | 4 +- .../api/v1/controllers_accounts_count_test.go | 7 +- .../controllers_accounts_delete_metadata.go | 2 +- ...ntrollers_accounts_delete_metadata_test.go | 3 +- internal/api/v1/controllers_accounts_list.go | 4 +- .../api/v1/controllers_accounts_list_test.go | 7 +- internal/api/v1/controllers_accounts_read.go | 2 +- .../api/v1/controllers_accounts_read_test.go | 3 +- .../api/v1/controllers_balances_aggregates.go | 2 +- internal/api/v1/controllers_balances_list.go | 2 +- internal/api/v1/controllers_logs_list.go | 2 +- internal/api/v1/controllers_logs_list_test.go | 3 +- .../controllers_transactions_add_metadata.go | 2 +- ...trollers_transactions_add_metadata_test.go | 3 +- .../api/v1/controllers_transactions_count.go | 2 +- .../api/v1/controllers_transactions_create.go | 24 +- .../controllers_transactions_create_test.go | 7 +- ...ontrollers_transactions_delete_metadata.go | 2 +- .../api/v1/controllers_transactions_list.go | 2 +- .../v1/controllers_transactions_list_test.go | 5 +- .../api/v1/controllers_transactions_read.go | 2 +- .../api/v1/controllers_transactions_revert.go | 6 +- .../controllers_transactions_revert_test.go | 5 +- internal/api/v1/errors.go | 11 - .../api/v1/middleware_auto_create_ledger.go | 3 +- .../v2/controllers_accounts_add_metadata.go | 4 +- .../controllers_accounts_add_metadata_test.go | 5 +- internal/api/v2/controllers_accounts_count.go | 4 +- .../api/v2/controllers_accounts_count_test.go | 9 +- .../controllers_accounts_delete_metadata.go | 2 +- ...ntrollers_accounts_delete_metadata_test.go | 3 +- internal/api/v2/controllers_accounts_list.go | 4 +- .../api/v2/controllers_accounts_list_test.go | 11 +- internal/api/v2/controllers_accounts_read.go | 4 +- .../api/v2/controllers_accounts_read_test.go | 3 +- internal/api/v2/controllers_balances.go | 6 +- internal/api/v2/controllers_bulk.go | 96 +--- internal/api/v2/controllers_bulk_test.go | 46 +- internal/api/v2/controllers_ledgers_create.go | 6 +- .../api/v2/controllers_ledgers_create_test.go | 9 +- internal/api/v2/controllers_ledgers_list.go | 4 +- .../api/v2/controllers_ledgers_list_test.go | 7 +- .../v2/controllers_ledgers_update_metadata.go | 2 +- internal/api/v2/controllers_logs_list.go | 8 +- internal/api/v2/controllers_logs_list_test.go | 9 +- .../controllers_transactions_add_metadata.go | 4 +- ...trollers_transactions_add_metadata_test.go | 5 +- .../api/v2/controllers_transactions_count.go | 4 +- .../v2/controllers_transactions_count_test.go | 5 +- .../api/v2/controllers_transactions_create.go | 27 +- .../controllers_transactions_create_test.go | 50 +- ...ontrollers_transactions_delete_metadata.go | 2 +- .../api/v2/controllers_transactions_list.go | 4 +- .../v2/controllers_transactions_list_test.go | 5 +- .../api/v2/controllers_transactions_read.go | 4 +- .../api/v2/controllers_transactions_revert.go | 6 +- .../controllers_transactions_revert_test.go | 5 +- internal/api/v2/controllers_volumes.go | 4 +- internal/api/v2/controllers_volumes_test.go | 3 +- internal/api/v2/errors.go | 16 - internal/api/v2/routes.go | 27 +- internal/controller/ledger/bulker.go | 426 --------------- .../23-delete-orphan-indices/notes.yaml | 1 + .../23-delete-orphan-indices/up.sql | 2 + .../up_tests_after.sql | 0 .../up_tests_before.sql | 0 internal/storage/ledger/accounts.go | 34 +- pkg/generate/generator.go | 32 +- test/e2e/api_bulk_test.go | 483 +++++++++--------- test/rolling-upgrades/Earthfile | 2 +- test/rolling-upgrades/main_test.go | 11 +- tools/generator/go.mod | 3 + 94 files changed, 2576 insertions(+), 1019 deletions(-) create mode 100644 internal/api/bulking/bulker.go create mode 100644 internal/api/bulking/bulker_test.go create mode 100644 internal/api/bulking/elements.go create mode 100644 internal/api/bulking/factory.go create mode 100644 internal/api/bulking/handler_json.go create mode 100644 internal/api/bulking/handler_json_test.go create mode 100644 internal/api/bulking/handler_text.go create mode 100644 internal/api/bulking/handler_text_test.go create mode 100644 internal/api/bulking/mocks.go create mode 100644 internal/api/bulking/mocks_ledger_controller_test.go create mode 100644 internal/api/bulking/result.go create mode 100644 internal/api/bulking/text_stream.go create mode 100644 internal/api/bulking/text_stream_test.go delete mode 100644 internal/api/v1/errors.go delete mode 100644 internal/api/v2/errors.go delete mode 100644 internal/controller/ledger/bulker.go create mode 100644 internal/storage/bucket/migrations/23-delete-orphan-indices/notes.yaml create mode 100644 internal/storage/bucket/migrations/23-delete-orphan-indices/up.sql create mode 100644 internal/storage/bucket/migrations/23-delete-orphan-indices/up_tests_after.sql create mode 100644 internal/storage/bucket/migrations/23-delete-orphan-indices/up_tests_before.sql diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 52b6bf515..8c3160804 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -172,12 +172,6 @@ jobs: - uses: 'actions/checkout@v4' with: fetch-depth: 0 - - name: Tailscale - uses: tailscale/github-action@v2 - with: - oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} - oauth-secret: ${{ secrets.TS_OAUTH_SECRET }} - tags: tag:ci - name: "Deploy in staging" env: TAG: ${{ github.sha }} diff --git a/docker-compose.yml b/docker-compose.yml index b6260a214..40a521713 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -71,4 +71,9 @@ services: OTEL_TRACES_EXPORTER_OTLP_INSECURE: "true" OTEL_TRACES_BATCH: "true" POSTGRES_URI: "postgresql://ledger:ledger@postgres/ledger?sslmode=disable" + POSTGRES_MAX_OPEN_CONNS: "40" + POSTGRES_MAX_IDLE_CONNS: "40" + POSTGRES_CONN_MAX_IDLE_TIME: "5m" + EXPERIMENTAL_FEATURES: "true" AUTO_UPGRADE: "true" + BULK_PARALLEL: "10" \ No newline at end of file diff --git a/internal/README.md b/internal/README.md index a647da7f6..026b9e02e 100644 --- a/internal/README.md +++ b/internal/README.md @@ -12,6 +12,7 @@ import "github.com/formancehq/ledger/internal" - [Variables](<#variables>) - [func ComputeIdempotencyHash\(inputs any\) string](<#ComputeIdempotencyHash>) - [type Account](<#Account>) + - [func \(a Account\) GetAddress\(\) string](<#Account.GetAddress>) - [type AccountMetadata](<#AccountMetadata>) - [type AccountsVolumes](<#AccountsVolumes>) - [type BalancesByAssets](<#BalancesByAssets>) @@ -172,6 +173,15 @@ type Account struct { } ``` + +### func \(Account\) GetAddress + +```go +func (a Account) GetAddress() string +``` + + + ## type AccountMetadata diff --git a/internal/account.go b/internal/account.go index cb3b21515..1fc877fd6 100644 --- a/internal/account.go +++ b/internal/account.go @@ -23,6 +23,10 @@ type Account struct { EffectiveVolumes VolumesByAssets `json:"effectiveVolumes,omitempty" bun:"effective_volumes,scanonly"` } +func (a Account) GetAddress() string { + return a.Address +} + type AccountsVolumes struct { bun.BaseModel `bun:"accounts_volumes"` diff --git a/internal/api/bulking/bulker.go b/internal/api/bulking/bulker.go new file mode 100644 index 000000000..a4a66d60f --- /dev/null +++ b/internal/api/bulking/bulker.go @@ -0,0 +1,313 @@ +package bulking + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "github.com/alitto/pond" + "github.com/formancehq/go-libs/v2/logging" + ledger "github.com/formancehq/ledger/internal" + ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" + "sync/atomic" +) + +type Bulker struct { + ctrl ledgercontroller.Controller + parallelism int + tracer trace.Tracer +} + +func (b *Bulker) run(ctx context.Context, ctrl ledgercontroller.Controller, bulk Bulk, result chan BulkElementResult, continueOnFailure, parallel bool) bool { + + parallelism := 1 + if parallel && b.parallelism != 0 { + parallelism = b.parallelism + } + + wp := pond.New(parallelism, parallelism) + hasError := atomic.Bool{} + + index := 0 + for element := range bulk { + wp.Submit(func() { + ctx, span := b.tracer.Start(ctx, "Bulk:ProcessElement", + trace.WithNewRoot(), + trace.WithLinks(trace.LinkFromContext(ctx)), + trace.WithAttributes(attribute.Int("index", index)), + ) + defer span.End() + + select { + case <-ctx.Done(): + result <- BulkElementResult{ + Error: ctx.Err(), + } + default: + if hasError.Load() && !continueOnFailure { + result <- BulkElementResult{ + Error: context.Canceled, + } + return + } + ret, logID, err := b.processElement(ctx, ctrl, element) + if err != nil { + hasError.Store(true) + span.RecordError(err) + + result <- BulkElementResult{ + Error: err, + } + + return + } + + result <- BulkElementResult{ + Data: ret, + LogID: logID, + } + } + + }) + } + + wp.StopAndWait() + + defer close(result) + + return hasError.Load() +} + +func (b *Bulker) Run(ctx context.Context, bulk Bulk, result chan BulkElementResult, bulkOptions BulkingOptions) error { + + ctx, span := b.tracer.Start(ctx, "Bulk:Run", trace.WithAttributes( + attribute.Bool("atomic", bulkOptions.Atomic), + attribute.Bool("parallel", bulkOptions.Parallel), + attribute.Bool("continueOnFailure", bulkOptions.ContinueOnFailure), + attribute.Int("parallelism", b.parallelism), + )) + defer span.End() + + if err := bulkOptions.Validate(); err != nil { + return fmt.Errorf("validating bulk options: %s", err) + } + + ctrl := b.ctrl + if bulkOptions.Atomic { + var err error + ctrl, err = ctrl.BeginTX(ctx, nil) + if err != nil { + return fmt.Errorf("error starting transaction: %s", err) + } + } + + hasError := b.run(ctx, ctrl, bulk, result, bulkOptions.ContinueOnFailure, bulkOptions.Parallel) + if hasError && bulkOptions.Atomic { + if rollbackErr := ctrl.Rollback(ctx); rollbackErr != nil { + logging.FromContext(ctx).Errorf("failed to rollback transaction: %v", rollbackErr) + } + + return nil + } + + if bulkOptions.Atomic { + if err := ctrl.Commit(ctx); err != nil { + return fmt.Errorf("error committing transaction: %s", err) + } + } + + return nil +} + +func (b *Bulker) processElement(ctx context.Context, ctrl ledgercontroller.Controller, data BulkElement) (any, int, error) { + + switch data.Action { + case ActionCreateTransaction: + rs, err := data.Data.(TransactionRequest).ToRunScript(false) + if err != nil { + return nil, 0, fmt.Errorf("error parsing element: %s", err) + } + + log, createTransactionResult, err := ctrl.CreateTransaction(ctx, ledgercontroller.Parameters[ledgercontroller.RunScript]{ + DryRun: false, + IdempotencyKey: data.IdempotencyKey, + Input: *rs, + }) + if err != nil { + return nil, 0, err + } + + // todo(next api version): no reason to return only the transaction... + return createTransactionResult.Transaction, log.ID, nil + case ActionAddMetadata: + req := data.Data.(AddMetadataRequest) + + var ( + log *ledger.Log + err error + ) + switch req.TargetType { + case ledger.MetaTargetTypeAccount: + address := "" + if err := json.Unmarshal(req.TargetID, &address); err != nil { + return nil, 0, err + } + log, err = ctrl.SaveAccountMetadata(ctx, ledgercontroller.Parameters[ledgercontroller.SaveAccountMetadata]{ + DryRun: false, + IdempotencyKey: data.IdempotencyKey, + Input: ledgercontroller.SaveAccountMetadata{ + Address: address, + Metadata: req.Metadata, + }, + }) + case ledger.MetaTargetTypeTransaction: + transactionID := 0 + if err := json.Unmarshal(req.TargetID, &transactionID); err != nil { + return nil, 0, err + } + log, err = ctrl.SaveTransactionMetadata(ctx, ledgercontroller.Parameters[ledgercontroller.SaveTransactionMetadata]{ + DryRun: false, + IdempotencyKey: data.IdempotencyKey, + Input: ledgercontroller.SaveTransactionMetadata{ + TransactionID: transactionID, + Metadata: req.Metadata, + }, + }) + default: + return nil, 0, fmt.Errorf("invalid target type: %s", req.TargetType) + } + if err != nil { + return nil, 0, err + } + + return nil, log.ID, nil + case ActionRevertTransaction: + req := data.Data.(RevertTransactionRequest) + + log, revertTransactionResult, err := ctrl.RevertTransaction(ctx, ledgercontroller.Parameters[ledgercontroller.RevertTransaction]{ + DryRun: false, + IdempotencyKey: data.IdempotencyKey, + Input: ledgercontroller.RevertTransaction{ + Force: req.Force, + AtEffectiveDate: req.AtEffectiveDate, + TransactionID: req.ID, + }, + }) + if err != nil { + return nil, 0, err + } + + return revertTransactionResult.RevertedTransaction, log.ID, nil + case ActionDeleteMetadata: + req := data.Data.(DeleteMetadataRequest) + + var ( + log *ledger.Log + err error + ) + switch req.TargetType { + case ledger.MetaTargetTypeAccount: + address := "" + if err := json.Unmarshal(req.TargetID, &address); err != nil { + return nil, 0, err + } + + log, err = ctrl.DeleteAccountMetadata(ctx, ledgercontroller.Parameters[ledgercontroller.DeleteAccountMetadata]{ + DryRun: false, + IdempotencyKey: data.IdempotencyKey, + Input: ledgercontroller.DeleteAccountMetadata{ + Address: address, + Key: req.Key, + }, + }) + case ledger.MetaTargetTypeTransaction: + transactionID := 0 + if err := json.Unmarshal(req.TargetID, &transactionID); err != nil { + return nil, 0, err + } + + log, err = ctrl.DeleteTransactionMetadata(ctx, ledgercontroller.Parameters[ledgercontroller.DeleteTransactionMetadata]{ + DryRun: false, + IdempotencyKey: data.IdempotencyKey, + Input: ledgercontroller.DeleteTransactionMetadata{ + TransactionID: transactionID, + Key: req.Key, + }, + }) + default: + return nil, 0, fmt.Errorf("unsupported target type: %s", req.TargetType) + } + if err != nil { + return nil, 0, err + } + + return nil, log.ID, nil + default: + panic("unreachable") + } +} + +func NewBulker(ctrl ledgercontroller.Controller, options ...BulkerOption) *Bulker { + ret := &Bulker{ctrl: ctrl} + for _, option := range append(defaultBulkerOptions, options...) { + option(ret) + } + + return ret +} + +type BulkerOption func(bulker *Bulker) + +func WithParallelism(v int) BulkerOption { + return func(options *Bulker) { + options.parallelism = v + } +} + +func WithTracer(tracer trace.Tracer) BulkerOption { + return func(options *Bulker) { + options.tracer = tracer + } +} + +var defaultBulkerOptions = []BulkerOption{ + WithTracer(noop.Tracer{}), + WithParallelism(10), +} + +type BulkingOptions struct { + ContinueOnFailure bool + Atomic bool + Parallel bool +} + +func (opts BulkingOptions) Validate() error { + if opts.Atomic && opts.Parallel { + return errors.New("atomic and parallel options are mutually exclusive") + } + + return nil +} + +type BulkerFactory interface { + CreateBulker(ctrl ledgercontroller.Controller) *Bulker +} + +type DefaultBulkerFactory struct { + Options []BulkerOption +} + +func (d *DefaultBulkerFactory) CreateBulker(ctrl ledgercontroller.Controller) *Bulker { + return NewBulker(ctrl, d.Options...) +} + +func NewDefaultBulkerFactory(options ...BulkerOption) *DefaultBulkerFactory { + return &DefaultBulkerFactory{ + Options: options, + } +} + +var _ BulkerFactory = (*DefaultBulkerFactory)(nil) diff --git a/internal/api/bulking/bulker_test.go b/internal/api/bulking/bulker_test.go new file mode 100644 index 000000000..1a0f657a4 --- /dev/null +++ b/internal/api/bulking/bulker_test.go @@ -0,0 +1,410 @@ +package bulking + +import ( + "encoding/json" + "github.com/formancehq/go-libs/v2/logging" + "math/big" + "testing" + + ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" + + "github.com/formancehq/go-libs/v2/time" + + "errors" + "github.com/formancehq/go-libs/v2/metadata" + ledger "github.com/formancehq/ledger/internal" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func TestBulk(t *testing.T) { + t.Parallel() + + now := time.Now() + + type bulkTestCase struct { + name string + bulk []BulkElement + expectations func(mockLedger *LedgerController) + expectError bool + expectResults []BulkElementResult + options BulkingOptions + } + + testCases := []bulkTestCase{ + { + name: "create transaction", + bulk: []BulkElement{{ + Action: ActionCreateTransaction, + Data: TransactionRequest{ + Postings: []ledger.Posting{{ + Source: "world", + Destination: "bank", + Amount: big.NewInt(100), + Asset: "USD/2", + }}, + Timestamp: now, + }, + }}, + expectations: func(mockLedger *LedgerController) { + postings := []ledger.Posting{{ + Source: "world", + Destination: "bank", + Amount: big.NewInt(100), + Asset: "USD/2", + }} + mockLedger.EXPECT(). + CreateTransaction(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.RunScript]{ + Input: ledgercontroller.TxToScriptData(ledger.TransactionData{ + Postings: postings, + Timestamp: now, + }, false), + }). + Return(&ledger.Log{}, &ledger.CreatedTransaction{ + Transaction: ledger.Transaction{ + TransactionData: ledger.TransactionData{ + Postings: postings, + Metadata: metadata.Metadata{}, + Timestamp: now, + }, + }, + }, nil) + }, + expectResults: []BulkElementResult{{ + Data: ledger.Transaction{ + TransactionData: ledger.TransactionData{ + Postings: []ledger.Posting{{Source: "world", Destination: "bank", Amount: big.NewInt(100), Asset: "USD/2"}}, + Timestamp: now, + Metadata: metadata.Metadata{}, + }, + }, + LogID: 1, + ElementID: 0, + }}, + }, + { + name: "add metadata on transaction", + bulk: []BulkElement{{ + Action: ActionAddMetadata, + Data: AddMetadataRequest{ + TargetID: json.RawMessage(`1`), + TargetType: "TRANSACTION", + Metadata: metadata.Metadata{ + "foo": "bar", + }, + }, + }}, + expectations: func(mockLedger *LedgerController) { + mockLedger.EXPECT(). + SaveTransactionMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.SaveTransactionMetadata]{ + Input: ledgercontroller.SaveTransactionMetadata{ + TransactionID: 1, + Metadata: metadata.Metadata{ + "foo": "bar", + }, + }, + }). + Return(&ledger.Log{}, nil) + }, + expectResults: []BulkElementResult{{}}, + }, + { + name: "add metadata on account", + bulk: []BulkElement{{ + Action: ActionAddMetadata, + Data: AddMetadataRequest{ + TargetID: json.RawMessage(`"world"`), + TargetType: "ACCOUNT", + Metadata: metadata.Metadata{ + "foo": "bar", + }, + }, + }}, + expectations: func(mockLedger *LedgerController) { + mockLedger.EXPECT(). + SaveAccountMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.SaveAccountMetadata]{ + Input: ledgercontroller.SaveAccountMetadata{ + Address: "world", + Metadata: metadata.Metadata{ + "foo": "bar", + }, + }, + }). + Return(&ledger.Log{}, nil) + }, + expectResults: []BulkElementResult{{}}, + }, + { + name: "revert transaction", + bulk: []BulkElement{{ + Action: ActionRevertTransaction, + Data: RevertTransactionRequest{ + ID: 1, + }, + }}, + expectations: func(mockLedger *LedgerController) { + mockLedger.EXPECT(). + RevertTransaction(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.RevertTransaction]{ + Input: ledgercontroller.RevertTransaction{ + TransactionID: 1, + }, + }). + Return(&ledger.Log{}, &ledger.RevertedTransaction{}, nil) + }, + expectResults: []BulkElementResult{{ + Data: ledger.Transaction{}, + }}, + }, + { + name: "delete metadata on transaction", + bulk: []BulkElement{{ + Action: ActionDeleteMetadata, + Data: DeleteMetadataRequest{ + TargetID: json.RawMessage(`1`), + TargetType: "TRANSACTION", + Key: "foo", + }, + }}, + expectations: func(mockLedger *LedgerController) { + mockLedger.EXPECT(). + DeleteTransactionMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.DeleteTransactionMetadata]{ + Input: ledgercontroller.DeleteTransactionMetadata{ + TransactionID: 1, + Key: "foo", + }, + }). + Return(&ledger.Log{}, nil) + }, + expectResults: []BulkElementResult{{}}, + }, + { + name: "delete metadata on account", + bulk: []BulkElement{{ + Action: ActionDeleteMetadata, + Data: DeleteMetadataRequest{ + TargetID: json.RawMessage(`"world"`), + TargetType: "ACCOUNT", + Key: "foo", + }, + }}, + expectations: func(mockLedger *LedgerController) { + mockLedger.EXPECT(). + DeleteAccountMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.DeleteAccountMetadata]{ + Input: ledgercontroller.DeleteAccountMetadata{ + Address: "world", + Key: "foo", + }, + }). + Return(&ledger.Log{}, nil) + }, + expectResults: []BulkElementResult{{}}, + }, + { + name: "error in the middle", + bulk: []BulkElement{{ + Action: ActionAddMetadata, + Data: AddMetadataRequest{ + TargetID: json.RawMessage(`"world"`), + TargetType: "ACCOUNT", + Metadata: metadata.Metadata{ + "foo": "bar", + }, + }, + }, { + Action: ActionAddMetadata, + Data: AddMetadataRequest{ + TargetID: json.RawMessage(`"world"`), + TargetType: "ACCOUNT", + Metadata: metadata.Metadata{ + "foo2": "bar2", + }, + }, + }, { + Action: ActionAddMetadata, + Data: AddMetadataRequest{ + TargetID: json.RawMessage(`"world"`), + TargetType: "ACCOUNT", + Metadata: metadata.Metadata{ + "foo3": "bar3", + }, + }, + }}, + expectations: func(mockLedger *LedgerController) { + mockLedger.EXPECT(). + SaveAccountMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.SaveAccountMetadata]{ + Input: ledgercontroller.SaveAccountMetadata{ + Address: "world", + Metadata: metadata.Metadata{ + "foo": "bar", + }, + }, + }). + Return(&ledger.Log{}, nil) + mockLedger.EXPECT(). + SaveAccountMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.SaveAccountMetadata]{ + Input: ledgercontroller.SaveAccountMetadata{ + Address: "world", + Metadata: metadata.Metadata{ + "foo2": "bar2", + }, + }, + }). + Return(nil, errors.New("unexpected error")) + }, + expectResults: []BulkElementResult{{}, { + Error: errors.New("unexpected error"), + }, {}}, + expectError: true, + }, + { + name: "error in the middle with continue on failure", + bulk: []BulkElement{{ + Action: ActionAddMetadata, + Data: AddMetadataRequest{ + TargetID: json.RawMessage(`"world"`), + TargetType: "ACCOUNT", + Metadata: metadata.Metadata{ + "foo": "bar", + }, + }, + }, { + Action: ActionAddMetadata, + Data: AddMetadataRequest{ + TargetID: json.RawMessage(`"world"`), + TargetType: "ACCOUNT", + Metadata: metadata.Metadata{ + "foo2": "bar2", + }, + }, + }, { + Action: ActionAddMetadata, + Data: AddMetadataRequest{ + TargetID: json.RawMessage(`"world"`), + TargetType: "ACCOUNT", + Metadata: metadata.Metadata{ + "foo3": "bar3", + }, + }, + }}, + options: BulkingOptions{ + ContinueOnFailure: true, + }, + expectations: func(mockLedger *LedgerController) { + mockLedger.EXPECT(). + SaveAccountMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.SaveAccountMetadata]{ + Input: ledgercontroller.SaveAccountMetadata{ + Address: "world", + Metadata: metadata.Metadata{ + "foo": "bar", + }, + }, + }). + Return(&ledger.Log{}, nil) + mockLedger.EXPECT(). + SaveAccountMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.SaveAccountMetadata]{ + Input: ledgercontroller.SaveAccountMetadata{ + Address: "world", + Metadata: metadata.Metadata{ + "foo2": "bar2", + }, + }, + }). + Return(nil, errors.New("unexpected error")) + mockLedger.EXPECT(). + SaveAccountMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.SaveAccountMetadata]{ + Input: ledgercontroller.SaveAccountMetadata{ + Address: "world", + Metadata: metadata.Metadata{ + "foo3": "bar3", + }, + }, + }). + Return(&ledger.Log{}, nil) + }, + expectResults: []BulkElementResult{{}, { + Error: errors.New("unexpected error"), + }, {}}, + expectError: true, + }, + { + name: "with atomic", + bulk: []BulkElement{{ + Action: ActionAddMetadata, + Data: AddMetadataRequest{ + TargetID: json.RawMessage(`"world"`), + TargetType: "ACCOUNT", + Metadata: metadata.Metadata{ + "foo": "bar", + }, + }, + }, { + Action: ActionAddMetadata, + Data: AddMetadataRequest{ + TargetID: json.RawMessage(`"world"`), + TargetType: "ACCOUNT", + Metadata: metadata.Metadata{ + "foo2": "bar2", + }, + }, + }}, + options: BulkingOptions{ + Atomic: true, + }, + expectations: func(mockLedger *LedgerController) { + mockLedger.EXPECT(). + BeginTX(gomock.Any(), nil). + Return(mockLedger, nil) + + mockLedger.EXPECT(). + SaveAccountMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.SaveAccountMetadata]{ + Input: ledgercontroller.SaveAccountMetadata{ + Address: "world", + Metadata: metadata.Metadata{ + "foo": "bar", + }, + }, + }). + Return(&ledger.Log{}, nil) + + mockLedger.EXPECT(). + SaveAccountMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.SaveAccountMetadata]{ + Input: ledgercontroller.SaveAccountMetadata{ + Address: "world", + Metadata: metadata.Metadata{ + "foo2": "bar2", + }, + }, + }). + Return(&ledger.Log{}, nil) + + mockLedger.EXPECT(). + Commit(gomock.Any()). + Return(nil) + }, + expectResults: []BulkElementResult{{}, {}}, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + ctx := logging.TestingContext() + + ctrl := gomock.NewController(t) + ledgerController := NewLedgerController(ctrl) + + testCase.expectations(ledgerController) + + bulker := NewBulker(ledgerController) + bulk := make(Bulk, len(testCase.bulk)) + results := make(chan BulkElementResult, len(testCase.bulk)) + + for _, element := range testCase.bulk { + bulk <- element + } + close(bulk) + + require.NoError(t, bulker.Run(ctx, bulk, results, testCase.options)) + }) + } +} diff --git a/internal/api/bulking/elements.go b/internal/api/bulking/elements.go new file mode 100644 index 000000000..44fbf064b --- /dev/null +++ b/internal/api/bulking/elements.go @@ -0,0 +1,128 @@ +package bulking + +import ( + "encoding/json" + "fmt" + "github.com/formancehq/go-libs/v2/metadata" + "github.com/formancehq/go-libs/v2/pointer" + "github.com/formancehq/go-libs/v2/time" + ledger "github.com/formancehq/ledger/internal" + ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" + "reflect" +) + +const ( + ActionCreateTransaction = "CREATE_TRANSACTION" + ActionAddMetadata = "ADD_METADATA" + ActionRevertTransaction = "REVERT_TRANSACTION" + ActionDeleteMetadata = "DELETE_METADATA" +) + +type Bulk chan BulkElement + +type BulkElement struct { + Action string `json:"action"` + IdempotencyKey string `json:"ik"` + Data any `json:"data"` +} + +func (b BulkElement) GetAction() string { + return b.Action +} + +func (b *BulkElement) UnmarshalJSON(data []byte) error { + + type Aux BulkElement + type X struct { + Aux + Data json.RawMessage `json:"data"` + } + x := X{} + if err := json.Unmarshal(data, &x); err != nil { + return err + } + + *b = BulkElement(x.Aux) + + var err error + b.Data, err = UnmarshalBulkElementPayload(x.Action, x.Data) + + return err +} + +func UnmarshalBulkElementPayload(action string, data []byte) (any, error) { + var req any + switch action { + case ActionCreateTransaction: + req = &TransactionRequest{} + case ActionAddMetadata: + req = &AddMetadataRequest{} + case ActionRevertTransaction: + req = &RevertTransactionRequest{} + case ActionDeleteMetadata: + req = &DeleteMetadataRequest{} + } + if err := json.Unmarshal(data, req); err != nil { + return nil, fmt.Errorf("error parsing element: %s", err) + } + + return reflect.ValueOf(req).Elem().Interface(), nil +} + +type BulkElementResult struct { + Error error + Data any `json:"data,omitempty"` + LogID int `json:"logID"` + ElementID int `json:"elementID"` +} + +type AddMetadataRequest struct { + TargetType string `json:"targetType"` + TargetID json.RawMessage `json:"targetId"` + Metadata metadata.Metadata `json:"metadata"` +} + +type RevertTransactionRequest struct { + ID int `json:"id"` + Force bool `json:"force"` + AtEffectiveDate bool `json:"atEffectiveDate"` +} + +type DeleteMetadataRequest struct { + TargetType string `json:"targetType"` + TargetID json.RawMessage `json:"targetId"` + Key string `json:"key"` +} + +type TransactionRequest struct { + Postings ledger.Postings `json:"postings"` + Script ledgercontroller.ScriptV1 `json:"script"` + Timestamp time.Time `json:"timestamp"` + Reference string `json:"reference"` + Metadata metadata.Metadata `json:"metadata" swaggertype:"object"` +} + +func (req TransactionRequest) ToRunScript(allowUnboundedOverdrafts bool) (*ledgercontroller.RunScript, error) { + + if _, err := req.Postings.Validate(); err != nil { + return nil, err + } + + if len(req.Postings) > 0 { + txData := ledger.TransactionData{ + Postings: req.Postings, + Timestamp: req.Timestamp, + Reference: req.Reference, + Metadata: req.Metadata, + } + + return pointer.For(ledgercontroller.TxToScriptData(txData, allowUnboundedOverdrafts)), nil + } + + return &ledgercontroller.RunScript{ + Script: req.Script.ToCore(), + Timestamp: req.Timestamp, + Reference: req.Reference, + Metadata: req.Metadata, + }, nil +} \ No newline at end of file diff --git a/internal/api/bulking/factory.go b/internal/api/bulking/factory.go new file mode 100644 index 000000000..04e13fc5f --- /dev/null +++ b/internal/api/bulking/factory.go @@ -0,0 +1,12 @@ +package bulking + +import "net/http" + +type Handler interface { + GetChannels(w http.ResponseWriter, r *http.Request) (Bulk, chan BulkElementResult, bool) + Terminate(w http.ResponseWriter, r *http.Request) +} + +type HandlerFactory interface { + CreateBulkHandler() Handler +} \ No newline at end of file diff --git a/internal/api/bulking/handler_json.go b/internal/api/bulking/handler_json.go new file mode 100644 index 000000000..6cfd96f2a --- /dev/null +++ b/internal/api/bulking/handler_json.go @@ -0,0 +1,148 @@ +package bulking + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/formancehq/go-libs/v2/api" + "github.com/formancehq/go-libs/v2/collectionutils" + "github.com/formancehq/go-libs/v2/pointer" + "github.com/formancehq/ledger/internal/api/common" + ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" + "net/http" + "slices" +) + +type JsonBulkHandler struct { + bulkMaxSize int + bulkElements []BulkElement + receive chan BulkElementResult +} + +func (h *JsonBulkHandler) GetChannels(w http.ResponseWriter, r *http.Request) (Bulk, chan BulkElementResult, bool) { + h.bulkElements = make([]BulkElement, 0) + if err := json.NewDecoder(r.Body).Decode(&h.bulkElements); err != nil { + api.BadRequest(w, common.ErrValidation, err) + return nil, nil, false + } + + if h.bulkMaxSize != 0 && len(h.bulkElements) > h.bulkMaxSize { + api.WriteErrorResponse(w, http.StatusRequestEntityTooLarge, common.ErrBulkSizeExceeded, fmt.Errorf("bulk size exceeded, max size is %d", h.bulkMaxSize)) + return nil, nil, false + } + + bulk := make(Bulk, len(h.bulkElements)) + for _, element := range h.bulkElements { + bulk <- element + } + close(bulk) + + h.receive = make(chan BulkElementResult, len(h.bulkElements)) + + return bulk, h.receive, true +} + +func (h *JsonBulkHandler) Terminate(w http.ResponseWriter, _ *http.Request) { + results := make([]BulkElementResult, 0, len(h.bulkElements)) + for element := range h.receive { + results = append(results, element) + } + + writeJSONResponse(w, collectionutils.Map(h.bulkElements, BulkElement.GetAction), results, nil) +} + +func NewJSONBulkHandler(bulkMaxSize int) *JsonBulkHandler { + return &JsonBulkHandler{ + bulkMaxSize: bulkMaxSize, + } +} + +type jsonBulkHandlerFactory struct { + bulkMaxSize int +} + +func (j jsonBulkHandlerFactory) CreateBulkHandler() Handler { + return NewJSONBulkHandler(j.bulkMaxSize) +} + +func NewJSONBulkHandlerFactory(bulkMaxSize int) HandlerFactory { + return &jsonBulkHandlerFactory{ + bulkMaxSize: bulkMaxSize, + } +} + +var _ HandlerFactory = (*jsonBulkHandlerFactory)(nil) + +func writeJSONResponse(w http.ResponseWriter, actions []string, results []BulkElementResult, error error) { + for _, result := range results { + if result.Error != nil { + w.WriteHeader(http.StatusBadRequest) + break + } + } + + slices.SortFunc(results, func(a, b BulkElementResult) int { + return a.ElementID - b.ElementID + }) + + mappedResults := make([]APIResult, 0) + for index, result := range results { + var ( + errorCode string + errorDescription string + responseType = actions[index] + ) + + if result.Error != nil { + switch { + case errors.Is(result.Error, &ledgercontroller.ErrInsufficientFunds{}): + errorCode = common.ErrInsufficientFund + case errors.Is(result.Error, &ledgercontroller.ErrInvalidVars{}) || errors.Is(result.Error, ledgercontroller.ErrCompilationFailed{}): + errorCode = common.ErrCompilationFailed + case errors.Is(result.Error, &ledgercontroller.ErrMetadataOverride{}): + errorCode = common.ErrMetadataOverride + case errors.Is(result.Error, ledgercontroller.ErrNoPostings): + errorCode = common.ErrNoPostings + case errors.Is(result.Error, ledgercontroller.ErrTransactionReferenceConflict{}): + errorCode = common.ErrConflict + case errors.Is(result.Error, ledgercontroller.ErrParsing{}): + errorCode = common.ErrInterpreterParse + case errors.Is(result.Error, ledgercontroller.ErrRuntime{}): + errorCode = common.ErrInterpreterRuntime + default: + errorCode = api.ErrorInternal + } + errorDescription = result.Error.Error() + responseType = "ERROR" + } + + mappedResults = append(mappedResults, APIResult{ + ErrorCode: errorCode, + ErrorDescription: errorDescription, + Data: result.Data, + ResponseType: responseType, + LogID: result.LogID, + }) + } + + if err := json.NewEncoder(w).Encode(ComposedErrorResponse{ + BaseResponse: api.BaseResponse[[]APIResult]{ + Data: pointer.For(mappedResults), + }, + ErrorResponse: func() api.ErrorResponse { + ret := api.ErrorResponse{} + if error != nil { + ret.ErrorCode = common.ErrValidation + ret.ErrorMessage = error.Error() + } + return ret + }(), + }); err != nil { + panic(err) + } +} + +type ComposedErrorResponse struct { + api.BaseResponse[[]APIResult] + api.ErrorResponse +} diff --git a/internal/api/bulking/handler_json_test.go b/internal/api/bulking/handler_json_test.go new file mode 100644 index 000000000..fdce30a69 --- /dev/null +++ b/internal/api/bulking/handler_json_test.go @@ -0,0 +1,129 @@ +package bulking + +import ( + "bytes" + "encoding/json" + "github.com/formancehq/go-libs/v2/api" + "github.com/formancehq/go-libs/v2/time" + ledger "github.com/formancehq/ledger/internal" + ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" + "github.com/stretchr/testify/require" + "net/http" + "net/http/httptest" + "testing" +) + +func TestBulkHandlerJSON(t *testing.T) { + + t.Parallel() + + type testCase struct { + name string + bulk []BulkElement + expectedError bool + expectedStatusCode int + } + const maxBulkSize = 3 + + for _, testCase := range []testCase{ + { + name: "nominal", + bulk: []BulkElement{ + { + Action: ActionCreateTransaction, + Data: TransactionRequest{ + Script: ledgercontroller.ScriptV1{ + Script: ledgercontroller.Script{ + Plain: ` +send [USD 100] ( + source = @world + destination = @alice +) +`, + }, + }, + }, + }, + }, + }, + { + name: "bulk exceeded max size", + expectedError: true, + expectedStatusCode: http.StatusRequestEntityTooLarge, + bulk: func() []BulkElement { + ret := make([]BulkElement, 0) + for range maxBulkSize + 1 { + ret = append(ret, BulkElement{ + Action: ActionCreateTransaction, + Data: TransactionRequest{ + Script: ledgercontroller.ScriptV1{ + Script: ledgercontroller.Script{ + Plain: ` +send [USD 100] ( + source = @world + destination = @alice +) +`, + }, + }, + }, + }) + } + + return ret + }(), + }, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + rawData, err := json.Marshal(testCase.bulk) + require.NoError(t, err) + + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(rawData)) + + h := NewJSONBulkHandler(maxBulkSize) + send, receive, ok := h.GetChannels(w, r) + + if testCase.expectedError { + require.False(t, ok) + require.Equal(t, testCase.expectedStatusCode, w.Result().StatusCode) + return + } + + require.True(t, ok) + + for id, element := range testCase.bulk { + select { + case item := <-send: + require.Equal(t, element, item) + + receive <- BulkElementResult{ + Data: ledger.CreatedTransaction{}, + LogID: id + 1, + ElementID: id, + } + case <-time.After(100 * time.Millisecond): + t.Fatal("should have receive an item on the send channel") + } + } + + select { + case _, ok := <-send: + require.False(t, ok) + case <-time.After(100 * time.Millisecond): + t.Fatal("send channel should have been closed since the bulk has been completely consumed") + } + + close(receive) + h.Terminate(w, r) + + require.Equal(t, http.StatusOK, w.Result().StatusCode) + + response, ok := api.DecodeSingleResponse[[]APIResult](t, w.Result().Body) + require.True(t, ok) + require.Len(t, response, len(testCase.bulk)) + }) + } +} diff --git a/internal/api/bulking/handler_text.go b/internal/api/bulking/handler_text.go new file mode 100644 index 000000000..a0a0d9e4c --- /dev/null +++ b/internal/api/bulking/handler_text.go @@ -0,0 +1,90 @@ +package bulking + +import ( + "bufio" + "net/http" +) + +type ScriptStreamBulkHandler struct { + channel Bulk + terminated chan struct{} + receive chan BulkElementResult + results []BulkElementResult + actions []string + err error +} + +func (h *ScriptStreamBulkHandler) GetChannels(_ http.ResponseWriter, r *http.Request) (Bulk, chan BulkElementResult, bool) { + + h.channel = make(Bulk) + h.receive = make(chan BulkElementResult) + h.terminated = make(chan struct{}) + + go func() { + defer close(h.channel) + + scanner := bufio.NewScanner(r.Body) + + for { + select { + case <-r.Context().Done(): + return + default: + nextElement, err := ParseTextStream(scanner) + if err != nil { + h.err = err + return + } + + if nextElement == nil { + // stream terminated + return + } + + h.actions = append(h.actions, nextElement.GetAction()) + h.channel <- *nextElement + } + } + }() + go func() { + defer close(h.terminated) + + for { + select { + case <-r.Context().Done(): + return + case res, ok := <-h.receive: + if !ok { + return + } + h.results = append(h.results, res) + } + } + }() + + return h.channel, h.receive, true +} + +func (h *ScriptStreamBulkHandler) Terminate(w http.ResponseWriter, r *http.Request) { + select { + case <-h.terminated: + writeJSONResponse(w, h.actions, h.results, h.err) + case <-r.Context().Done(): + } +} + +func NewScriptStreamBulkHandler() *ScriptStreamBulkHandler { + return &ScriptStreamBulkHandler{} +} + +type scriptStreamBulkHandlerFactory struct{} + +func (j scriptStreamBulkHandlerFactory) CreateBulkHandler() Handler { + return NewScriptStreamBulkHandler() +} + +func NewScriptStreamBulkHandlerFactory() HandlerFactory { + return &scriptStreamBulkHandlerFactory{} +} + +var _ HandlerFactory = (*scriptStreamBulkHandlerFactory)(nil) diff --git a/internal/api/bulking/handler_text_test.go b/internal/api/bulking/handler_text_test.go new file mode 100644 index 000000000..af1d2562c --- /dev/null +++ b/internal/api/bulking/handler_text_test.go @@ -0,0 +1,98 @@ +package bulking + +import ( + "github.com/formancehq/go-libs/v2/api" + ledger "github.com/formancehq/ledger/internal" + "github.com/stretchr/testify/require" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func TestBulkHandlerText(t *testing.T) { + + t.Parallel() + + type testCase struct { + name string + stream string + expectedError bool + expectedStatusCode int + + expectScriptCount int + } + + for _, testCase := range []testCase{ + { + name: "nominal", + stream: ` +//script +send [USD 100] ( + source = @world + destination = @alice +) +//end +//script +send [USD 100] ( + source = @world + destination = @bob +) +//end +`, + expectScriptCount: 2, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + reader, writer := io.Pipe() + + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodPost, "/", reader) + + h := NewScriptStreamBulkHandler() + send, receive, ok := h.GetChannels(w, r) + + if testCase.expectedError { + require.False(t, ok) + require.Equal(t, testCase.expectedStatusCode, w.Result().StatusCode) + return + } + + require.True(t, ok) + + _, err := writer.Write([]byte(testCase.stream)) + require.NoError(t, err) + + for id := range testCase.expectScriptCount { + select { + case <-send: + case <-time.After(100 * time.Millisecond): + t.Fatal("should have received send channel") + } + select { + case receive <- BulkElementResult{ + Data: ledger.CreatedTransaction{}, + LogID: id + 1, + ElementID: id, + }: + case <-time.After(100 * time.Millisecond): + t.Fatal("should have been able to send on receive channel") + } + } + + require.NoError(t, writer.Close()) + close(receive) + + h.Terminate(w, r) + + require.Equal(t, http.StatusOK, w.Result().StatusCode) + + response, ok := api.DecodeSingleResponse[[]APIResult](t, w.Result().Body) + require.True(t, ok) + require.Len(t, response, testCase.expectScriptCount) + }) + } +} diff --git a/internal/api/bulking/mocks.go b/internal/api/bulking/mocks.go new file mode 100644 index 000000000..f15f7ce6e --- /dev/null +++ b/internal/api/bulking/mocks.go @@ -0,0 +1,2 @@ +//go:generate mockgen -write_source_comment=false -write_package_comment=false -source ../../controller/ledger/controller.go -destination mocks_ledger_controller_test.go -package bulking --mock_names Controller=LedgerController . Controller +package bulking diff --git a/internal/api/bulking/mocks_ledger_controller_test.go b/internal/api/bulking/mocks_ledger_controller_test.go new file mode 100644 index 000000000..7786df0be --- /dev/null +++ b/internal/api/bulking/mocks_ledger_controller_test.go @@ -0,0 +1,384 @@ +// Code generated by MockGen. DO NOT EDIT. +// +// Generated by this command: +// +// mockgen -write_source_comment=false -write_package_comment=false -source ../../controller/ledger/controller.go -destination mocks_ledger_controller_test.go -package bulking --mock_names Controller=LedgerController . Controller +package bulking + +import ( + context "context" + sql "database/sql" + reflect "reflect" + + bunpaginate "github.com/formancehq/go-libs/v2/bun/bunpaginate" + migrations "github.com/formancehq/go-libs/v2/migrations" + ledger "github.com/formancehq/ledger/internal" + ledger0 "github.com/formancehq/ledger/internal/controller/ledger" + gomock "go.uber.org/mock/gomock" +) + +// LedgerController is a mock of Controller interface. +type LedgerController struct { + ctrl *gomock.Controller + recorder *LedgerControllerMockRecorder +} + +// LedgerControllerMockRecorder is the mock recorder for LedgerController. +type LedgerControllerMockRecorder struct { + mock *LedgerController +} + +// NewLedgerController creates a new mock instance. +func NewLedgerController(ctrl *gomock.Controller) *LedgerController { + mock := &LedgerController{ctrl: ctrl} + mock.recorder = &LedgerControllerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *LedgerController) EXPECT() *LedgerControllerMockRecorder { + return m.recorder +} + +// BeginTX mocks base method. +func (m *LedgerController) BeginTX(ctx context.Context, options *sql.TxOptions) (ledger0.Controller, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BeginTX", ctx, options) + ret0, _ := ret[0].(ledger0.Controller) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BeginTX indicates an expected call of BeginTX. +func (mr *LedgerControllerMockRecorder) BeginTX(ctx, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BeginTX", reflect.TypeOf((*LedgerController)(nil).BeginTX), ctx, options) +} + +// Commit mocks base method. +func (m *LedgerController) Commit(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Commit", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// Commit indicates an expected call of Commit. +func (mr *LedgerControllerMockRecorder) Commit(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Commit", reflect.TypeOf((*LedgerController)(nil).Commit), ctx) +} + +// CountAccounts mocks base method. +func (m *LedgerController) CountAccounts(ctx context.Context, query ledger0.ListAccountsQuery) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CountAccounts", ctx, query) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CountAccounts indicates an expected call of CountAccounts. +func (mr *LedgerControllerMockRecorder) CountAccounts(ctx, query any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountAccounts", reflect.TypeOf((*LedgerController)(nil).CountAccounts), ctx, query) +} + +// CountTransactions mocks base method. +func (m *LedgerController) CountTransactions(ctx context.Context, query ledger0.ListTransactionsQuery) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CountTransactions", ctx, query) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CountTransactions indicates an expected call of CountTransactions. +func (mr *LedgerControllerMockRecorder) CountTransactions(ctx, query any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountTransactions", reflect.TypeOf((*LedgerController)(nil).CountTransactions), ctx, query) +} + +// CreateTransaction mocks base method. +func (m *LedgerController) CreateTransaction(ctx context.Context, parameters ledger0.Parameters[ledger0.RunScript]) (*ledger.Log, *ledger.CreatedTransaction, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateTransaction", ctx, parameters) + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(*ledger.CreatedTransaction) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// CreateTransaction indicates an expected call of CreateTransaction. +func (mr *LedgerControllerMockRecorder) CreateTransaction(ctx, parameters any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTransaction", reflect.TypeOf((*LedgerController)(nil).CreateTransaction), ctx, parameters) +} + +// DeleteAccountMetadata mocks base method. +func (m *LedgerController) DeleteAccountMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.DeleteAccountMetadata]) (*ledger.Log, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteAccountMetadata", ctx, parameters) + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteAccountMetadata indicates an expected call of DeleteAccountMetadata. +func (mr *LedgerControllerMockRecorder) DeleteAccountMetadata(ctx, parameters any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAccountMetadata", reflect.TypeOf((*LedgerController)(nil).DeleteAccountMetadata), ctx, parameters) +} + +// DeleteTransactionMetadata mocks base method. +func (m *LedgerController) DeleteTransactionMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.DeleteTransactionMetadata]) (*ledger.Log, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteTransactionMetadata", ctx, parameters) + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteTransactionMetadata indicates an expected call of DeleteTransactionMetadata. +func (mr *LedgerControllerMockRecorder) DeleteTransactionMetadata(ctx, parameters any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTransactionMetadata", reflect.TypeOf((*LedgerController)(nil).DeleteTransactionMetadata), ctx, parameters) +} + +// Export mocks base method. +func (m *LedgerController) Export(ctx context.Context, w ledger0.ExportWriter) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Export", ctx, w) + ret0, _ := ret[0].(error) + return ret0 +} + +// Export indicates an expected call of Export. +func (mr *LedgerControllerMockRecorder) Export(ctx, w any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Export", reflect.TypeOf((*LedgerController)(nil).Export), ctx, w) +} + +// GetAccount mocks base method. +func (m *LedgerController) GetAccount(ctx context.Context, query ledger0.GetAccountQuery) (*ledger.Account, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAccount", ctx, query) + ret0, _ := ret[0].(*ledger.Account) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAccount indicates an expected call of GetAccount. +func (mr *LedgerControllerMockRecorder) GetAccount(ctx, query any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccount", reflect.TypeOf((*LedgerController)(nil).GetAccount), ctx, query) +} + +// GetAggregatedBalances mocks base method. +func (m *LedgerController) GetAggregatedBalances(ctx context.Context, q ledger0.GetAggregatedBalanceQuery) (ledger.BalancesByAssets, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAggregatedBalances", ctx, q) + ret0, _ := ret[0].(ledger.BalancesByAssets) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAggregatedBalances indicates an expected call of GetAggregatedBalances. +func (mr *LedgerControllerMockRecorder) GetAggregatedBalances(ctx, q any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAggregatedBalances", reflect.TypeOf((*LedgerController)(nil).GetAggregatedBalances), ctx, q) +} + +// GetMigrationsInfo mocks base method. +func (m *LedgerController) GetMigrationsInfo(ctx context.Context) ([]migrations.Info, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMigrationsInfo", ctx) + ret0, _ := ret[0].([]migrations.Info) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMigrationsInfo indicates an expected call of GetMigrationsInfo. +func (mr *LedgerControllerMockRecorder) GetMigrationsInfo(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMigrationsInfo", reflect.TypeOf((*LedgerController)(nil).GetMigrationsInfo), ctx) +} + +// GetStats mocks base method. +func (m *LedgerController) GetStats(ctx context.Context) (ledger0.Stats, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetStats", ctx) + ret0, _ := ret[0].(ledger0.Stats) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetStats indicates an expected call of GetStats. +func (mr *LedgerControllerMockRecorder) GetStats(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStats", reflect.TypeOf((*LedgerController)(nil).GetStats), ctx) +} + +// GetTransaction mocks base method. +func (m *LedgerController) GetTransaction(ctx context.Context, query ledger0.GetTransactionQuery) (*ledger.Transaction, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTransaction", ctx, query) + ret0, _ := ret[0].(*ledger.Transaction) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetTransaction indicates an expected call of GetTransaction. +func (mr *LedgerControllerMockRecorder) GetTransaction(ctx, query any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTransaction", reflect.TypeOf((*LedgerController)(nil).GetTransaction), ctx, query) +} + +// GetVolumesWithBalances mocks base method. +func (m *LedgerController) GetVolumesWithBalances(ctx context.Context, q ledger0.GetVolumesWithBalancesQuery) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetVolumesWithBalances", ctx, q) + ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetVolumesWithBalances indicates an expected call of GetVolumesWithBalances. +func (mr *LedgerControllerMockRecorder) GetVolumesWithBalances(ctx, q any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVolumesWithBalances", reflect.TypeOf((*LedgerController)(nil).GetVolumesWithBalances), ctx, q) +} + +// Import mocks base method. +func (m *LedgerController) Import(ctx context.Context, stream chan ledger.Log) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Import", ctx, stream) + ret0, _ := ret[0].(error) + return ret0 +} + +// Import indicates an expected call of Import. +func (mr *LedgerControllerMockRecorder) Import(ctx, stream any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Import", reflect.TypeOf((*LedgerController)(nil).Import), ctx, stream) +} + +// IsDatabaseUpToDate mocks base method. +func (m *LedgerController) IsDatabaseUpToDate(ctx context.Context) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsDatabaseUpToDate", ctx) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsDatabaseUpToDate indicates an expected call of IsDatabaseUpToDate. +func (mr *LedgerControllerMockRecorder) IsDatabaseUpToDate(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsDatabaseUpToDate", reflect.TypeOf((*LedgerController)(nil).IsDatabaseUpToDate), ctx) +} + +// ListAccounts mocks base method. +func (m *LedgerController) ListAccounts(ctx context.Context, query ledger0.ListAccountsQuery) (*bunpaginate.Cursor[ledger.Account], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAccounts", ctx, query) + ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Account]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListAccounts indicates an expected call of ListAccounts. +func (mr *LedgerControllerMockRecorder) ListAccounts(ctx, query any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAccounts", reflect.TypeOf((*LedgerController)(nil).ListAccounts), ctx, query) +} + +// ListLogs mocks base method. +func (m *LedgerController) ListLogs(ctx context.Context, query ledger0.GetLogsQuery) (*bunpaginate.Cursor[ledger.Log], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListLogs", ctx, query) + ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Log]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListLogs indicates an expected call of ListLogs. +func (mr *LedgerControllerMockRecorder) ListLogs(ctx, query any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLogs", reflect.TypeOf((*LedgerController)(nil).ListLogs), ctx, query) +} + +// ListTransactions mocks base method. +func (m *LedgerController) ListTransactions(ctx context.Context, query ledger0.ListTransactionsQuery) (*bunpaginate.Cursor[ledger.Transaction], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListTransactions", ctx, query) + ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Transaction]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListTransactions indicates an expected call of ListTransactions. +func (mr *LedgerControllerMockRecorder) ListTransactions(ctx, query any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTransactions", reflect.TypeOf((*LedgerController)(nil).ListTransactions), ctx, query) +} + +// RevertTransaction mocks base method. +func (m *LedgerController) RevertTransaction(ctx context.Context, parameters ledger0.Parameters[ledger0.RevertTransaction]) (*ledger.Log, *ledger.RevertedTransaction, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RevertTransaction", ctx, parameters) + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(*ledger.RevertedTransaction) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// RevertTransaction indicates an expected call of RevertTransaction. +func (mr *LedgerControllerMockRecorder) RevertTransaction(ctx, parameters any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevertTransaction", reflect.TypeOf((*LedgerController)(nil).RevertTransaction), ctx, parameters) +} + +// Rollback mocks base method. +func (m *LedgerController) Rollback(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Rollback", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// Rollback indicates an expected call of Rollback. +func (mr *LedgerControllerMockRecorder) Rollback(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rollback", reflect.TypeOf((*LedgerController)(nil).Rollback), ctx) +} + +// SaveAccountMetadata mocks base method. +func (m *LedgerController) SaveAccountMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.SaveAccountMetadata]) (*ledger.Log, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SaveAccountMetadata", ctx, parameters) + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SaveAccountMetadata indicates an expected call of SaveAccountMetadata. +func (mr *LedgerControllerMockRecorder) SaveAccountMetadata(ctx, parameters any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveAccountMetadata", reflect.TypeOf((*LedgerController)(nil).SaveAccountMetadata), ctx, parameters) +} + +// SaveTransactionMetadata mocks base method. +func (m *LedgerController) SaveTransactionMetadata(ctx context.Context, parameters ledger0.Parameters[ledger0.SaveTransactionMetadata]) (*ledger.Log, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SaveTransactionMetadata", ctx, parameters) + ret0, _ := ret[0].(*ledger.Log) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SaveTransactionMetadata indicates an expected call of SaveTransactionMetadata. +func (mr *LedgerControllerMockRecorder) SaveTransactionMetadata(ctx, parameters any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveTransactionMetadata", reflect.TypeOf((*LedgerController)(nil).SaveTransactionMetadata), ctx, parameters) +} diff --git a/internal/api/bulking/result.go b/internal/api/bulking/result.go new file mode 100644 index 000000000..3b1fbeafd --- /dev/null +++ b/internal/api/bulking/result.go @@ -0,0 +1,9 @@ +package bulking + +type APIResult struct { + ErrorCode string `json:"errorCode,omitempty"` + ErrorDescription string `json:"errorDescription,omitempty"` + Data any `json:"data,omitempty"` + ResponseType string `json:"responseType"` // Added for sdk generation (discriminator in oneOf) + LogID int `json:"logID"` +} diff --git a/internal/api/bulking/text_stream.go b/internal/api/bulking/text_stream.go new file mode 100644 index 000000000..15d9c7224 --- /dev/null +++ b/internal/api/bulking/text_stream.go @@ -0,0 +1,83 @@ +package bulking + +import ( + "bufio" + "errors" + "fmt" + ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" + "github.com/formancehq/ledger/internal/machine/vm" + "strings" +) + +func ParseTextStream(scanner *bufio.Scanner) (*BulkElement, error) { + + // Read header + for scanner.Scan() { + text := strings.TrimSpace(scanner.Text()) + + switch { + case text == "": + case strings.HasPrefix(text, "//script"): + bulkElement := BulkElement{} + bulkElement.Action = ActionCreateTransaction + text = strings.TrimPrefix(text, "//script") + text = strings.TrimSpace(text) + + if len(text) > 0 { + parts := strings.Split(text, ",") + for _, part := range parts { + parts2 := strings.Split(part, "=") + switch { + case parts2[0] == "ik": + if bulkElement.IdempotencyKey != "" { + return nil, errors.New("invalid header, idempotency key already set") + } + bulkElement.IdempotencyKey = parts2[1] + default: + return nil, errors.New("invalid header, key '" + parts2[0] + "' not recognized") + } + } + } + + // Read body + plain := "" + for scanner.Scan() { + text = scanner.Text() + if text == "//end" { + bulkElement.Data = TransactionRequest{ + Script: ledgercontroller.ScriptV1{ + Script: vm.Script{ + Plain: plain[:len(plain)-1], // remove last \n + }, + }, + } + return &bulkElement, nil + } + plain += text + "\n" + } + + if scanner.Err() != nil { + return nil, fmt.Errorf("error reading script: %w", scanner.Err()) + } + + if scanner.Err() == nil { + bulkElement.Data = TransactionRequest{ + Script: ledgercontroller.ScriptV1{ + Script: vm.Script{ + Plain: plain[:len(plain)-1], // remove last \n + }, + }, + } + return &bulkElement, nil + } + default: + return nil, errors.New("invalid header") + } + } + + if scanner.Err() != nil { + return nil, fmt.Errorf("error while reading script: %w", scanner.Err()) + } + + return nil, nil +} diff --git a/internal/api/bulking/text_stream_test.go b/internal/api/bulking/text_stream_test.go new file mode 100644 index 000000000..128d53599 --- /dev/null +++ b/internal/api/bulking/text_stream_test.go @@ -0,0 +1,180 @@ +package bulking + +import ( + "bufio" + "bytes" + ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" + "github.com/stretchr/testify/require" + "testing" +) + +func TestParseStream(t *testing.T) { + t.Parallel() + + type testCase struct { + name string + stream string + expectedError bool + expectedElements []BulkElement + } + + for _, testCase := range []testCase{ + { + name: "nominal", + expectedElements: []BulkElement{ + { + Action: ActionCreateTransaction, + Data: TransactionRequest{ + Script: ledgercontroller.ScriptV1{ + Script: ledgercontroller.Script{ + Plain: `send [USD 100] ( + source = @world + destination = @alice +)`, + }, + }, + }, + }, + }, + stream: ` +//script +send [USD 100] ( + source = @world + destination = @alice +) +//end`, + }, + { + name: "multiple scripts", + expectedElements: []BulkElement{ + { + Action: ActionCreateTransaction, + Data: TransactionRequest{ + Script: ledgercontroller.ScriptV1{ + Script: ledgercontroller.Script{ + Plain: `send [USD 100] ( + source = @world + destination = @alice +)`, + }, + }, + }, + }, + { + Action: ActionCreateTransaction, + Data: TransactionRequest{ + Script: ledgercontroller.ScriptV1{ + Script: ledgercontroller.Script{ + Plain: `send [USD 100] ( + source = @world + destination = @bob +)`, + }, + }, + }, + }, + }, + stream: ` +//script +send [USD 100] ( + source = @world + destination = @alice +) +//end +//script +send [USD 100] ( + source = @world + destination = @bob +) +//end`, + }, + { + name: "no tags", + stream: ` +send [USD 100] ( + source = @world + destination = @alice +)`, + expectedError: true, + }, + { + name: "no ending tag", + expectedElements: []BulkElement{ + { + Action: ActionCreateTransaction, + Data: TransactionRequest{ + Script: ledgercontroller.ScriptV1{ + Script: ledgercontroller.Script{ + Plain: `send [USD 100] ( + source = @world + destination = @alice +)`, + }, + }, + }, + }, + }, + stream: ` +//script +send [USD 100] ( + source = @world + destination = @alice +)`, + }, + { + name: "script with ik", + expectedElements: []BulkElement{ + { + Action: ActionCreateTransaction, + IdempotencyKey: "foo", + Data: TransactionRequest{ + Script: ledgercontroller.ScriptV1{ + Script: ledgercontroller.Script{ + Plain: `send [USD 100] ( + source = @world + destination = @alice +)`, + }, + }, + }, + }, + }, + stream: ` +//script ik=foo +send [USD 100] ( + source = @world + destination = @alice +) +//end`, + }, + { + name: "script with ik specified twice", + expectedError: true, + stream: ` +//script ik=foo,ik=bar +send [USD 100] ( + source = @world + destination = @alice +) +//end`, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + if testCase.expectedError { + _, err := ParseTextStream(bufio.NewScanner(bytes.NewBufferString(testCase.stream))) + require.Error(t, err) + return + } else { + scanner := bufio.NewScanner(bytes.NewBufferString(testCase.stream)) + for _, element := range testCase.expectedElements { + ret, err := ParseTextStream(scanner) + require.NoError(t, err) + require.NotNil(t, ret) + require.Equal(t, element, *ret) + } + } + }) + } +} diff --git a/internal/api/common/errors.go b/internal/api/common/errors.go index f518b9411..ff8b86e53 100644 --- a/internal/api/common/errors.go +++ b/internal/api/common/errors.go @@ -7,6 +7,25 @@ import ( "net/http" ) +const ( + ErrConflict = "CONFLICT" + ErrInsufficientFund = "INSUFFICIENT_FUND" + ErrValidation = "VALIDATION" + ErrAlreadyRevert = "ALREADY_REVERT" + ErrNoPostings = "NO_POSTINGS" + ErrCompilationFailed = "COMPILATION_FAILED" + ErrMetadataOverride = "METADATA_OVERRIDE" + ErrBulkSizeExceeded = "BULK_SIZE_EXCEEDED" + ErrLedgerAlreadyExists = "LEDGER_ALREADY_EXISTS" + + ErrInterpreterParse = "INTERPRETER_PARSE" + ErrInterpreterRuntime = "INTERPRETER_RUNTIME" + + // v1 only + ErrScriptCompilationFailed = "COMPILATION_FAILED" + ErrScriptMetadataOverride = "METADATA_OVERRIDE" +) + func HandleCommonErrors(w http.ResponseWriter, r *http.Request, err error) { switch { case errors.Is(err, postgres.ErrTooManyClient{}): diff --git a/internal/api/module.go b/internal/api/module.go index d890e634f..d3e54c6b9 100644 --- a/internal/api/module.go +++ b/internal/api/module.go @@ -4,7 +4,7 @@ import ( _ "embed" "github.com/formancehq/go-libs/v2/auth" "github.com/formancehq/go-libs/v2/health" - "github.com/formancehq/ledger/internal/controller/ledger" + "github.com/formancehq/ledger/internal/api/bulking" "github.com/formancehq/ledger/internal/controller/system" "github.com/go-chi/chi/v5" "go.opentelemetry.io/otel/trace" @@ -12,14 +12,14 @@ import ( ) type BulkConfig struct { - MaxSize int + MaxSize int Parallel int } type Config struct { - Version string - Debug bool - Bulk BulkConfig + Version string + Debug bool + Bulk BulkConfig } func Module(cfg Config) fx.Option { @@ -27,17 +27,18 @@ func Module(cfg Config) fx.Option { fx.Provide(func( backend system.Controller, authenticator auth.Authenticator, - tracer trace.TracerProvider, + tracerProvider trace.TracerProvider, ) chi.Router { return NewRouter( backend, authenticator, "develop", cfg.Debug, - WithTracer(tracer.Tracer("api")), + WithTracer(tracerProvider.Tracer("api")), WithBulkMaxSize(cfg.Bulk.MaxSize), - WithBulkerFactory(ledger.NewDefaultBulkerFactory( - ledger.WithParallelism(cfg.Bulk.Parallel), + WithBulkerFactory(bulking.NewDefaultBulkerFactory( + bulking.WithParallelism(cfg.Bulk.Parallel), + bulking.WithTracer(tracerProvider.Tracer("api.bulking")), )), ) }), diff --git a/internal/api/router.go b/internal/api/router.go index 7c66e84a4..940ac8bf4 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -2,7 +2,7 @@ package api import ( "github.com/formancehq/go-libs/v2/api" - ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" + "github.com/formancehq/ledger/internal/api/bulking" "github.com/formancehq/ledger/internal/controller/system" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -66,8 +66,11 @@ func NewRouter( debug, v2.WithTracer(routerOptions.tracer), v2.WithMiddlewares(commonMiddlewares...), - v2.WithBulkMaxSize(routerOptions.bulkMaxSize), v2.WithBulkerFactory(routerOptions.bulkerFactory), + v2.WithBulkHandlerFactories(map[string]bulking.HandlerFactory{ + "application/json": bulking.NewJSONBulkHandlerFactory(routerOptions.bulkMaxSize), + "application/vnd.formance.ledger.api.v2.bulk+script-stream": bulking.NewScriptStreamBulkHandlerFactory(), + }), ) mux.Handle("/v2*", http.StripPrefix("/v2", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { chi.RouteContext(r.Context()).Reset() @@ -88,7 +91,7 @@ func NewRouter( type routerOptions struct { tracer trace.Tracer bulkMaxSize int - bulkerFactory ledgercontroller.BulkerFactory + bulkerFactory bulking.BulkerFactory } type RouterOption func(ro *routerOptions) @@ -105,7 +108,7 @@ func WithBulkMaxSize(bulkMaxSize int) RouterOption { } } -func WithBulkerFactory(bf ledgercontroller.BulkerFactory) RouterOption { +func WithBulkerFactory(bf bulking.BulkerFactory) RouterOption { return func(ro *routerOptions) { ro.bulkerFactory = bf } diff --git a/internal/api/v1/controllers_accounts_add_metadata.go b/internal/api/v1/controllers_accounts_add_metadata.go index 8263f3e06..9e4aeb439 100644 --- a/internal/api/v1/controllers_accounts_add_metadata.go +++ b/internal/api/v1/controllers_accounts_add_metadata.go @@ -18,18 +18,18 @@ func addAccountMetadata(w http.ResponseWriter, r *http.Request) { l := common.LedgerFromContext(r.Context()) address, err := url.PathUnescape(chi.URLParam(r, "address")) if err != nil { - api.BadRequestWithDetails(w, ErrValidation, err, err.Error()) + api.BadRequestWithDetails(w, common.ErrValidation, err, err.Error()) return } if !accounts.ValidateAddress(address) { - api.BadRequest(w, ErrValidation, errors.New("invalid account address format")) + api.BadRequest(w, common.ErrValidation, errors.New("invalid account address format")) return } var m metadata.Metadata if err := json.NewDecoder(r.Body).Decode(&m); err != nil { - api.BadRequest(w, ErrValidation, errors.New("invalid metadata format")) + api.BadRequest(w, common.ErrValidation, errors.New("invalid metadata format")) return } diff --git a/internal/api/v1/controllers_accounts_add_metadata_test.go b/internal/api/v1/controllers_accounts_add_metadata_test.go index 0a8a42750..974b1c6af 100644 --- a/internal/api/v1/controllers_accounts_add_metadata_test.go +++ b/internal/api/v1/controllers_accounts_add_metadata_test.go @@ -2,6 +2,7 @@ package v1 import ( ledger "github.com/formancehq/ledger/internal" + "github.com/formancehq/ledger/internal/api/common" "net/http" "net/http/httptest" "net/url" @@ -63,19 +64,19 @@ func TestAccountsAddMetadata(t *testing.T) { account: "world", body: "invalid - not an object", expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, { name: "malformed account address", account: "%8X%2F", expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, { name: "invalid account address", account: "1\abc", expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, } for _, testCase := range testCases { diff --git a/internal/api/v1/controllers_accounts_count.go b/internal/api/v1/controllers_accounts_count.go index 6501b65e7..9a5bb94be 100644 --- a/internal/api/v1/controllers_accounts_count.go +++ b/internal/api/v1/controllers_accounts_count.go @@ -27,7 +27,7 @@ func countAccounts(w http.ResponseWriter, r *http.Request) { return pointer.For(ledgercontroller.NewListAccountsQuery(*options)), nil }) if err != nil { - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) return } @@ -35,7 +35,7 @@ func countAccounts(w http.ResponseWriter, r *http.Request) { if err != nil { switch { case errors.Is(err, ledgercontroller.ErrInvalidQuery{}) || errors.Is(err, ledgercontroller.ErrMissingFeature{}): - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) default: common.HandleCommonErrors(w, r, err) } diff --git a/internal/api/v1/controllers_accounts_count_test.go b/internal/api/v1/controllers_accounts_count_test.go index 4da6a9603..1ebea8349 100644 --- a/internal/api/v1/controllers_accounts_count_test.go +++ b/internal/api/v1/controllers_accounts_count_test.go @@ -1,6 +1,7 @@ package v1 import ( + "github.com/formancehq/ledger/internal/api/common" "net/http" "net/http/httptest" "net/url" @@ -74,7 +75,7 @@ func TestAccountsCount(t *testing.T) { "pageSize": []string{"nan"}, }, expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, { name: "page size over maximum", @@ -107,7 +108,7 @@ func TestAccountsCount(t *testing.T) { { name: "with invalid query from core point of view", expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, expectBackendCall: true, returnErr: ledgercontroller.ErrInvalidQuery{}, expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ @@ -120,7 +121,7 @@ func TestAccountsCount(t *testing.T) { { name: "with missing feature", expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, expectBackendCall: true, returnErr: ledgercontroller.ErrMissingFeature{}, expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ diff --git a/internal/api/v1/controllers_accounts_delete_metadata.go b/internal/api/v1/controllers_accounts_delete_metadata.go index 2e57831dc..06c6ab88b 100644 --- a/internal/api/v1/controllers_accounts_delete_metadata.go +++ b/internal/api/v1/controllers_accounts_delete_metadata.go @@ -13,7 +13,7 @@ import ( func deleteAccountMetadata(w http.ResponseWriter, r *http.Request) { address, err := url.PathUnescape(chi.URLParam(r, "address")) if err != nil { - api.BadRequestWithDetails(w, ErrValidation, err, err.Error()) + api.BadRequestWithDetails(w, common.ErrValidation, err, err.Error()) return } diff --git a/internal/api/v1/controllers_accounts_delete_metadata_test.go b/internal/api/v1/controllers_accounts_delete_metadata_test.go index 6934cffe7..808292dce 100644 --- a/internal/api/v1/controllers_accounts_delete_metadata_test.go +++ b/internal/api/v1/controllers_accounts_delete_metadata_test.go @@ -3,6 +3,7 @@ package v1 import ( "encoding/json" ledger "github.com/formancehq/ledger/internal" + "github.com/formancehq/ledger/internal/api/common" "net/http" "net/http/httptest" "net/url" @@ -51,7 +52,7 @@ func TestAccountsDeleteMetadata(t *testing.T) { name: "invalid account address", account: "%8X%2F", expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, expectBackendCall: false, }, } { diff --git a/internal/api/v1/controllers_accounts_list.go b/internal/api/v1/controllers_accounts_list.go index ead1ed9b9..9c1c29bac 100644 --- a/internal/api/v1/controllers_accounts_list.go +++ b/internal/api/v1/controllers_accounts_list.go @@ -26,7 +26,7 @@ func listAccounts(w http.ResponseWriter, r *http.Request) { return pointer.For(ledgercontroller.NewListAccountsQuery(*options)), nil }) if err != nil { - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) return } @@ -34,7 +34,7 @@ func listAccounts(w http.ResponseWriter, r *http.Request) { if err != nil { switch { case errors.Is(err, ledgercontroller.ErrMissingFeature{}): - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) default: common.HandleCommonErrors(w, r, err) } diff --git a/internal/api/v1/controllers_accounts_list_test.go b/internal/api/v1/controllers_accounts_list_test.go index 1c8e08b70..8f1677953 100644 --- a/internal/api/v1/controllers_accounts_list_test.go +++ b/internal/api/v1/controllers_accounts_list_test.go @@ -1,6 +1,7 @@ package v1 import ( + "github.com/formancehq/ledger/internal/api/common" "net/http" "net/http/httptest" "net/url" @@ -72,7 +73,7 @@ func TestAccountsList(t *testing.T) { "cursor": []string{"XXX"}, }, expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, { name: "invalid page size", @@ -80,7 +81,7 @@ func TestAccountsList(t *testing.T) { "pageSize": []string{"nan"}, }, expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, { name: "page size over maximum", @@ -105,7 +106,7 @@ func TestAccountsList(t *testing.T) { { name: "with missing feature", expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, returnErr: ledgercontroller.ErrMissingFeature{}, expectBackendCall: true, expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). diff --git a/internal/api/v1/controllers_accounts_read.go b/internal/api/v1/controllers_accounts_read.go index 17fa27dc0..3a56a9f66 100644 --- a/internal/api/v1/controllers_accounts_read.go +++ b/internal/api/v1/controllers_accounts_read.go @@ -18,7 +18,7 @@ func getAccount(w http.ResponseWriter, r *http.Request) { address, err := url.PathUnescape(chi.URLParam(r, "address")) if err != nil { - api.BadRequestWithDetails(w, ErrValidation, err, err.Error()) + api.BadRequestWithDetails(w, common.ErrValidation, err, err.Error()) return } diff --git a/internal/api/v1/controllers_accounts_read_test.go b/internal/api/v1/controllers_accounts_read_test.go index b7ad0f6e5..027415873 100644 --- a/internal/api/v1/controllers_accounts_read_test.go +++ b/internal/api/v1/controllers_accounts_read_test.go @@ -2,6 +2,7 @@ package v1 import ( "bytes" + "github.com/formancehq/ledger/internal/api/common" "net/http" "net/http/httptest" "net/url" @@ -52,7 +53,7 @@ func TestAccountsRead(t *testing.T) { name: "invalid account address", account: "%8X%2F", expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, { name: "with not existing account", diff --git a/internal/api/v1/controllers_balances_aggregates.go b/internal/api/v1/controllers_balances_aggregates.go index d17187cde..dd0147346 100644 --- a/internal/api/v1/controllers_balances_aggregates.go +++ b/internal/api/v1/controllers_balances_aggregates.go @@ -21,7 +21,7 @@ func getBalancesAggregated(w http.ResponseWriter, r *http.Request) { pitFilter, err := getPITFilter(r) if err != nil { - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) return } diff --git a/internal/api/v1/controllers_balances_list.go b/internal/api/v1/controllers_balances_list.go index ee76acc98..fb7217e15 100644 --- a/internal/api/v1/controllers_balances_list.go +++ b/internal/api/v1/controllers_balances_list.go @@ -26,7 +26,7 @@ func getBalances(w http.ResponseWriter, r *http.Request) { return pointer.For(ledgercontroller.NewListAccountsQuery(*options)), nil }) if err != nil { - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) return } diff --git a/internal/api/v1/controllers_logs_list.go b/internal/api/v1/controllers_logs_list.go index 01aa23441..1fd7d1777 100644 --- a/internal/api/v1/controllers_logs_list.go +++ b/internal/api/v1/controllers_logs_list.go @@ -42,7 +42,7 @@ func getLogs(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get(QueryKeyCursor) != "" { err := bunpaginate.UnmarshalCursor(r.URL.Query().Get(QueryKeyCursor), &query) if err != nil { - api.BadRequest(w, ErrValidation, fmt.Errorf("invalid '%s' query param: %w", QueryKeyCursor, err)) + api.BadRequest(w, common.ErrValidation, fmt.Errorf("invalid '%s' query param: %w", QueryKeyCursor, err)) return } } else { diff --git a/internal/api/v1/controllers_logs_list_test.go b/internal/api/v1/controllers_logs_list_test.go index 1fb433d84..44a419535 100644 --- a/internal/api/v1/controllers_logs_list_test.go +++ b/internal/api/v1/controllers_logs_list_test.go @@ -2,6 +2,7 @@ package v1 import ( "encoding/json" + "github.com/formancehq/ledger/internal/api/common" "net/http" "net/http/httptest" "net/url" @@ -64,7 +65,7 @@ func TestGetLogs(t *testing.T) { "cursor": []string{"xxx"}, }, expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, } for _, testCase := range testCases { diff --git a/internal/api/v1/controllers_transactions_add_metadata.go b/internal/api/v1/controllers_transactions_add_metadata.go index 6e09db6ac..50e3142d9 100644 --- a/internal/api/v1/controllers_transactions_add_metadata.go +++ b/internal/api/v1/controllers_transactions_add_metadata.go @@ -19,7 +19,7 @@ func addTransactionMetadata(w http.ResponseWriter, r *http.Request) { var m metadata.Metadata if err := json.NewDecoder(r.Body).Decode(&m); err != nil { - api.BadRequest(w, ErrValidation, errors.New("invalid metadata format")) + api.BadRequest(w, common.ErrValidation, errors.New("invalid metadata format")) return } diff --git a/internal/api/v1/controllers_transactions_add_metadata_test.go b/internal/api/v1/controllers_transactions_add_metadata_test.go index f46c5e971..f33a311dc 100644 --- a/internal/api/v1/controllers_transactions_add_metadata_test.go +++ b/internal/api/v1/controllers_transactions_add_metadata_test.go @@ -2,6 +2,7 @@ package v1 import ( ledger "github.com/formancehq/ledger/internal" + "github.com/formancehq/ledger/internal/api/common" "net/http" "net/http/httptest" "net/url" @@ -38,7 +39,7 @@ func TestTransactionsAddMetadata(t *testing.T) { name: "invalid body", body: "invalid - not an object", expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, } for _, testCase := range testCases { diff --git a/internal/api/v1/controllers_transactions_count.go b/internal/api/v1/controllers_transactions_count.go index 49ba722c7..a36e009f9 100644 --- a/internal/api/v1/controllers_transactions_count.go +++ b/internal/api/v1/controllers_transactions_count.go @@ -13,7 +13,7 @@ func countTransactions(w http.ResponseWriter, r *http.Request) { options, err := getPaginatedQueryOptionsOfPITFilterWithVolumes(r) if err != nil { - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) return } options.QueryBuilder = buildGetTransactionsQuery(r) diff --git a/internal/api/v1/controllers_transactions_create.go b/internal/api/v1/controllers_transactions_create.go index 2e6d89cce..68be88313 100644 --- a/internal/api/v1/controllers_transactions_create.go +++ b/internal/api/v1/controllers_transactions_create.go @@ -63,17 +63,17 @@ func createTransaction(w http.ResponseWriter, r *http.Request) { payload := CreateTransactionRequest{} if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { - api.BadRequest(w, ErrValidation, errors.New("invalid transaction format")) + api.BadRequest(w, common.ErrValidation, errors.New("invalid transaction format")) return } if len(payload.Postings) > 0 && payload.Script.Plain != "" || len(payload.Postings) == 0 && payload.Script.Plain == "" { - api.BadRequest(w, ErrValidation, errors.New("invalid payload: should contain either postings or script")) + api.BadRequest(w, common.ErrValidation, errors.New("invalid payload: should contain either postings or script")) return } else if len(payload.Postings) > 0 { if _, err := payload.Postings.Validate(); err != nil { - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) return } txData := ledger.TransactionData{ @@ -87,16 +87,16 @@ func createTransaction(w http.ResponseWriter, r *http.Request) { if err != nil { switch { case errors.Is(err, &ledgercontroller.ErrInsufficientFunds{}): - api.BadRequest(w, ErrInsufficientFund, err) + api.BadRequest(w, common.ErrInsufficientFund, err) case errors.Is(err, &ledgercontroller.ErrInvalidVars{}) || errors.Is(err, ledgercontroller.ErrCompilationFailed{}): - api.BadRequest(w, ErrScriptCompilationFailed, err) + api.BadRequest(w, common.ErrScriptCompilationFailed, err) case errors.Is(err, &ledgercontroller.ErrMetadataOverride{}): - api.BadRequest(w, ErrScriptMetadataOverride, err) + api.BadRequest(w, common.ErrScriptMetadataOverride, err) case errors.Is(err, ledgercontroller.ErrNoPostings) || errors.Is(err, ledgercontroller.ErrInvalidIdempotencyInput{}): - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) case errors.Is(err, ledgercontroller.ErrTransactionReferenceConflict{}): - api.WriteErrorResponse(w, http.StatusConflict, ErrConflict, err) + api.WriteErrorResponse(w, http.StatusConflict, common.ErrConflict, err) default: common.HandleCommonErrors(w, r, err) } @@ -108,7 +108,7 @@ func createTransaction(w http.ResponseWriter, r *http.Request) { script, err := payload.Script.ToCore() if err != nil { - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) return } @@ -123,15 +123,15 @@ func createTransaction(w http.ResponseWriter, r *http.Request) { if err != nil { switch { case errors.Is(err, &ledgercontroller.ErrInsufficientFunds{}): - api.BadRequest(w, ErrInsufficientFund, err) + api.BadRequest(w, common.ErrInsufficientFund, err) case errors.Is(err, &ledgercontroller.ErrInvalidVars{}) || errors.Is(err, ledgercontroller.ErrCompilationFailed{}) || errors.Is(err, &ledgercontroller.ErrMetadataOverride{}) || errors.Is(err, ledgercontroller.ErrInvalidIdempotencyInput{}) || errors.Is(err, ledgercontroller.ErrNoPostings): - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) case errors.Is(err, ledgercontroller.ErrTransactionReferenceConflict{}): - api.WriteErrorResponse(w, http.StatusConflict, ErrConflict, err) + api.WriteErrorResponse(w, http.StatusConflict, common.ErrConflict, err) default: common.HandleCommonErrors(w, r, err) } diff --git a/internal/api/v1/controllers_transactions_create_test.go b/internal/api/v1/controllers_transactions_create_test.go index caea2047f..b57172226 100644 --- a/internal/api/v1/controllers_transactions_create_test.go +++ b/internal/api/v1/controllers_transactions_create_test.go @@ -2,6 +2,7 @@ package v1 import ( "encoding/json" + "github.com/formancehq/ledger/internal/api/common" "math/big" "net/http" "net/http/httptest" @@ -176,7 +177,7 @@ func TestTransactionsCreate(t *testing.T) { name: "no postings or script", payload: CreateTransactionRequest{}, expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, { name: "postings and script", @@ -200,13 +201,13 @@ func TestTransactionsCreate(t *testing.T) { }, }, expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, { name: "using invalid body", payload: "not a valid payload", expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, } diff --git a/internal/api/v1/controllers_transactions_delete_metadata.go b/internal/api/v1/controllers_transactions_delete_metadata.go index e9129b24d..e2a017e7b 100644 --- a/internal/api/v1/controllers_transactions_delete_metadata.go +++ b/internal/api/v1/controllers_transactions_delete_metadata.go @@ -17,7 +17,7 @@ func deleteTransactionMetadata(w http.ResponseWriter, r *http.Request) { transactionID, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 64) if err != nil { - api.BadRequest(w, ErrValidation, errors.New("invalid transaction ID")) + api.BadRequest(w, common.ErrValidation, errors.New("invalid transaction ID")) return } diff --git a/internal/api/v1/controllers_transactions_list.go b/internal/api/v1/controllers_transactions_list.go index 62b4f89c8..ae1c11fa8 100644 --- a/internal/api/v1/controllers_transactions_list.go +++ b/internal/api/v1/controllers_transactions_list.go @@ -23,7 +23,7 @@ func listTransactions(w http.ResponseWriter, r *http.Request) { return pointer.For(ledgercontroller.NewListTransactionsQuery(*options)), nil }) if err != nil { - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) return } diff --git a/internal/api/v1/controllers_transactions_list_test.go b/internal/api/v1/controllers_transactions_list_test.go index 4fde95fad..45bbaf2cc 100644 --- a/internal/api/v1/controllers_transactions_list_test.go +++ b/internal/api/v1/controllers_transactions_list_test.go @@ -1,6 +1,7 @@ package v1 import ( + "github.com/formancehq/ledger/internal/api/common" "math/big" "net/http" "net/http/httptest" @@ -105,7 +106,7 @@ func TestTransactionsList(t *testing.T) { "cursor": []string{"XXX"}, }, expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, { name: "invalid page size", @@ -113,7 +114,7 @@ func TestTransactionsList(t *testing.T) { "pageSize": []string{"nan"}, }, expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, { name: "page size over maximum", diff --git a/internal/api/v1/controllers_transactions_read.go b/internal/api/v1/controllers_transactions_read.go index d8aa8292a..d1b2dd147 100644 --- a/internal/api/v1/controllers_transactions_read.go +++ b/internal/api/v1/controllers_transactions_read.go @@ -17,7 +17,7 @@ func readTransaction(w http.ResponseWriter, r *http.Request) { txId, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) return } diff --git a/internal/api/v1/controllers_transactions_revert.go b/internal/api/v1/controllers_transactions_revert.go index 5612b836d..fa294862c 100644 --- a/internal/api/v1/controllers_transactions_revert.go +++ b/internal/api/v1/controllers_transactions_revert.go @@ -17,7 +17,7 @@ func revertTransaction(w http.ResponseWriter, r *http.Request) { txID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) return } @@ -32,9 +32,9 @@ func revertTransaction(w http.ResponseWriter, r *http.Request) { if err != nil { switch { case errors.Is(err, &ledgercontroller.ErrInsufficientFunds{}): - api.BadRequest(w, ErrInsufficientFund, err) + api.BadRequest(w, common.ErrInsufficientFund, err) case errors.Is(err, ledgercontroller.ErrAlreadyReverted{}): - api.BadRequest(w, ErrAlreadyRevert, err) + api.BadRequest(w, common.ErrAlreadyRevert, err) case errors.Is(err, ledgercontroller.ErrNotFound): api.NotFound(w, err) default: diff --git a/internal/api/v1/controllers_transactions_revert_test.go b/internal/api/v1/controllers_transactions_revert_test.go index b2603e32e..cf97b1a68 100644 --- a/internal/api/v1/controllers_transactions_revert_test.go +++ b/internal/api/v1/controllers_transactions_revert_test.go @@ -1,6 +1,7 @@ package v1 import ( + "github.com/formancehq/ledger/internal/api/common" "math/big" "net/http" "net/http/httptest" @@ -48,13 +49,13 @@ func TestTransactionsRevert(t *testing.T) { name: "with insufficient fund", returnErr: &ledgercontroller.ErrInsufficientFunds{}, expectStatusCode: http.StatusBadRequest, - expectErrorCode: ErrInsufficientFund, + expectErrorCode: common.ErrInsufficientFund, }, { name: "with already revert", returnErr: &ledgercontroller.ErrAlreadyReverted{}, expectStatusCode: http.StatusBadRequest, - expectErrorCode: ErrAlreadyRevert, + expectErrorCode: common.ErrAlreadyRevert, }, { name: "with transaction not found", diff --git a/internal/api/v1/errors.go b/internal/api/v1/errors.go deleted file mode 100644 index 80bb5828d..000000000 --- a/internal/api/v1/errors.go +++ /dev/null @@ -1,11 +0,0 @@ -package v1 - -const ( - ErrConflict = "CONFLICT" - ErrInsufficientFund = "INSUFFICIENT_FUND" - ErrValidation = "VALIDATION" - ErrAlreadyRevert = "ALREADY_REVERT" - - ErrScriptCompilationFailed = "COMPILATION_FAILED" - ErrScriptMetadataOverride = "METADATA_OVERRIDE" -) diff --git a/internal/api/v1/middleware_auto_create_ledger.go b/internal/api/v1/middleware_auto_create_ledger.go index 42b29d843..24da4a38e 100644 --- a/internal/api/v1/middleware_auto_create_ledger.go +++ b/internal/api/v1/middleware_auto_create_ledger.go @@ -1,6 +1,7 @@ package v1 import ( + "github.com/formancehq/ledger/internal/api/common" "go.opentelemetry.io/otel/trace" "net/http" @@ -34,7 +35,7 @@ func autoCreateMiddleware(backend system.Controller, tracer trace.Tracer) func(h }); err != nil { switch { case errors.Is(err, ledger.ErrInvalidLedgerName{}): - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) default: api.InternalServerError(w, r, err) } diff --git a/internal/api/v2/controllers_accounts_add_metadata.go b/internal/api/v2/controllers_accounts_add_metadata.go index b0a12038a..5f1a28d21 100644 --- a/internal/api/v2/controllers_accounts_add_metadata.go +++ b/internal/api/v2/controllers_accounts_add_metadata.go @@ -18,13 +18,13 @@ func addAccountMetadata(w http.ResponseWriter, r *http.Request) { address, err := url.PathUnescape(chi.URLParam(r, "address")) if err != nil { - api.BadRequestWithDetails(w, ErrValidation, err, err.Error()) + api.BadRequestWithDetails(w, common.ErrValidation, err, err.Error()) return } var m metadata.Metadata if err := json.NewDecoder(r.Body).Decode(&m); err != nil { - api.BadRequest(w, ErrValidation, errors.New("invalid metadata format")) + api.BadRequest(w, common.ErrValidation, errors.New("invalid metadata format")) return } diff --git a/internal/api/v2/controllers_accounts_add_metadata_test.go b/internal/api/v2/controllers_accounts_add_metadata_test.go index 7d6b73fd4..6ae214852 100644 --- a/internal/api/v2/controllers_accounts_add_metadata_test.go +++ b/internal/api/v2/controllers_accounts_add_metadata_test.go @@ -2,6 +2,7 @@ package v2 import ( ledger "github.com/formancehq/ledger/internal" + "github.com/formancehq/ledger/internal/api/common" "net/http" "net/http/httptest" "net/url" @@ -42,13 +43,13 @@ func TestAccountsAddMetadata(t *testing.T) { account: "world", body: "invalid - not an object", expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, { name: "invalid account address", account: "%8X%2F", expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, } for _, testCase := range testCases { diff --git a/internal/api/v2/controllers_accounts_count.go b/internal/api/v2/controllers_accounts_count.go index 1aadff47d..61d26cd61 100644 --- a/internal/api/v2/controllers_accounts_count.go +++ b/internal/api/v2/controllers_accounts_count.go @@ -15,7 +15,7 @@ func countAccounts(w http.ResponseWriter, r *http.Request) { options, err := getPaginatedQueryOptionsOfPITFilterWithVolumes(r) if err != nil { - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) return } @@ -23,7 +23,7 @@ func countAccounts(w http.ResponseWriter, r *http.Request) { if err != nil { switch { case errors.Is(err, ledgercontroller.ErrInvalidQuery{}) || errors.Is(err, ledgercontroller.ErrMissingFeature{}): - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) default: common.HandleCommonErrors(w, r, err) } diff --git a/internal/api/v2/controllers_accounts_count_test.go b/internal/api/v2/controllers_accounts_count_test.go index 91b3c7515..8e04ac775 100644 --- a/internal/api/v2/controllers_accounts_count_test.go +++ b/internal/api/v2/controllers_accounts_count_test.go @@ -2,6 +2,7 @@ package v2 import ( "bytes" + "github.com/formancehq/ledger/internal/api/common" "net/http" "net/http/httptest" "net/url" @@ -74,7 +75,7 @@ func TestAccountsCount(t *testing.T) { "pageSize": []string{"nan"}, }, expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, { name: "page size over maximum", @@ -117,12 +118,12 @@ func TestAccountsCount(t *testing.T) { name: "using invalid query payload", body: `[]`, expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, { name: "with invalid query from core point of view", expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, expectBackendCall: true, returnErr: ledgercontroller.ErrInvalidQuery{}, expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ @@ -135,7 +136,7 @@ func TestAccountsCount(t *testing.T) { { name: "with missing feature", expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, expectBackendCall: true, returnErr: ledgercontroller.ErrMissingFeature{}, expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ diff --git a/internal/api/v2/controllers_accounts_delete_metadata.go b/internal/api/v2/controllers_accounts_delete_metadata.go index fda3ac613..26775a7d5 100644 --- a/internal/api/v2/controllers_accounts_delete_metadata.go +++ b/internal/api/v2/controllers_accounts_delete_metadata.go @@ -14,7 +14,7 @@ import ( func deleteAccountMetadata(w http.ResponseWriter, r *http.Request) { address, err := url.PathUnescape(chi.URLParam(r, "address")) if err != nil { - api.BadRequestWithDetails(w, ErrValidation, err, err.Error()) + api.BadRequestWithDetails(w, common.ErrValidation, err, err.Error()) return } diff --git a/internal/api/v2/controllers_accounts_delete_metadata_test.go b/internal/api/v2/controllers_accounts_delete_metadata_test.go index c3d1fde78..4d6a9a9ea 100644 --- a/internal/api/v2/controllers_accounts_delete_metadata_test.go +++ b/internal/api/v2/controllers_accounts_delete_metadata_test.go @@ -3,6 +3,7 @@ package v2 import ( "encoding/json" ledger "github.com/formancehq/ledger/internal" + "github.com/formancehq/ledger/internal/api/common" "net/http" "net/http/httptest" "net/url" @@ -51,7 +52,7 @@ func TestAccountsDeleteMetadata(t *testing.T) { name: "invalid account address", account: "%8X%2F", expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, expectBackendCall: false, }, } { diff --git a/internal/api/v2/controllers_accounts_list.go b/internal/api/v2/controllers_accounts_list.go index c4edc6a4f..88b3322a0 100644 --- a/internal/api/v2/controllers_accounts_list.go +++ b/internal/api/v2/controllers_accounts_list.go @@ -22,7 +22,7 @@ func listAccounts(w http.ResponseWriter, r *http.Request) { return pointer.For(ledgercontroller.NewListAccountsQuery(*options)), nil }) if err != nil { - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) return } @@ -30,7 +30,7 @@ func listAccounts(w http.ResponseWriter, r *http.Request) { if err != nil { switch { case errors.Is(err, ledgercontroller.ErrInvalidQuery{}) || errors.Is(err, ledgercontroller.ErrMissingFeature{}): - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) default: common.HandleCommonErrors(w, r, err) } diff --git a/internal/api/v2/controllers_accounts_list_test.go b/internal/api/v2/controllers_accounts_list_test.go index ba6319773..b2c81a63b 100644 --- a/internal/api/v2/controllers_accounts_list_test.go +++ b/internal/api/v2/controllers_accounts_list_test.go @@ -2,6 +2,7 @@ package v2 import ( "bytes" + "github.com/formancehq/ledger/internal/api/common" "net/http" "net/http/httptest" "net/url" @@ -85,7 +86,7 @@ func TestAccountsList(t *testing.T) { "cursor": []string{"XXX"}, }, expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, { name: "invalid page size", @@ -93,7 +94,7 @@ func TestAccountsList(t *testing.T) { "pageSize": []string{"nan"}, }, expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, { name: "page size over maximum", @@ -136,12 +137,12 @@ func TestAccountsList(t *testing.T) { name: "using invalid query payload", body: `[]`, expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, { name: "with invalid query from core point of view", expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, expectBackendCall: true, returnErr: ledgercontroller.ErrInvalidQuery{}, expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ @@ -154,7 +155,7 @@ func TestAccountsList(t *testing.T) { { name: "with missing feature", expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, expectBackendCall: true, returnErr: ledgercontroller.ErrMissingFeature{}, expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ diff --git a/internal/api/v2/controllers_accounts_read.go b/internal/api/v2/controllers_accounts_read.go index d1eddd262..cb9f21672 100644 --- a/internal/api/v2/controllers_accounts_read.go +++ b/internal/api/v2/controllers_accounts_read.go @@ -16,7 +16,7 @@ func readAccount(w http.ResponseWriter, r *http.Request) { param, err := url.PathUnescape(chi.URLParam(r, "address")) if err != nil { - api.BadRequestWithDetails(w, ErrValidation, err, err.Error()) + api.BadRequestWithDetails(w, common.ErrValidation, err, err.Error()) return } @@ -29,7 +29,7 @@ func readAccount(w http.ResponseWriter, r *http.Request) { } pitFilter, err := getPITFilter(r) if err != nil { - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) return } query.PITFilter = *pitFilter diff --git a/internal/api/v2/controllers_accounts_read_test.go b/internal/api/v2/controllers_accounts_read_test.go index 93c742c88..8ab40ccc2 100644 --- a/internal/api/v2/controllers_accounts_read_test.go +++ b/internal/api/v2/controllers_accounts_read_test.go @@ -2,6 +2,7 @@ package v2 import ( "bytes" + "github.com/formancehq/ledger/internal/api/common" "net/http" "net/http/httptest" "net/url" @@ -62,7 +63,7 @@ func TestAccountsRead(t *testing.T) { name: "invalid account address", account: "%8X%2F", expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, } for _, testCase := range testCases { diff --git a/internal/api/v2/controllers_balances.go b/internal/api/v2/controllers_balances.go index 66d16fd80..a63646f38 100644 --- a/internal/api/v2/controllers_balances.go +++ b/internal/api/v2/controllers_balances.go @@ -14,13 +14,13 @@ func readBalancesAggregated(w http.ResponseWriter, r *http.Request) { pitFilter, err := getPITFilter(r) if err != nil { - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) return } queryBuilder, err := getQueryBuilder(r) if err != nil { - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) return } @@ -30,7 +30,7 @@ func readBalancesAggregated(w http.ResponseWriter, r *http.Request) { if err != nil { switch { case errors.Is(err, ledgercontroller.ErrInvalidQuery{}) || errors.Is(err, ledgercontroller.ErrMissingFeature{}): - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) default: common.HandleCommonErrors(w, r, err) } diff --git a/internal/api/v2/controllers_bulk.go b/internal/api/v2/controllers_bulk.go index 2fc248e94..105acdb5c 100644 --- a/internal/api/v2/controllers_bulk.go +++ b/internal/api/v2/controllers_bulk.go @@ -1,99 +1,47 @@ package v2 import ( - "encoding/json" - "fmt" - "github.com/formancehq/go-libs/v2/pointer" + "errors" + "github.com/formancehq/ledger/internal/api/bulking" "net/http" - "errors" "github.com/formancehq/go-libs/v2/api" "github.com/formancehq/ledger/internal/api/common" - ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" ) -func bulkHandler(bulkerFactory ledgercontroller.BulkerFactory, bulkMaxSize int) http.HandlerFunc { +func bulkHandler(bulkerFactory bulking.BulkerFactory, bulkHandlerFactories map[string]bulking.HandlerFactory) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - b := ledgercontroller.Bulk{} - if err := json.NewDecoder(r.Body).Decode(&b); err != nil { - api.BadRequest(w, ErrValidation, err) + + contentType := r.Header.Get("Content-Type") + if contentType == "" { + contentType = "application/json" + } + bulkHandlerFactory, ok := bulkHandlerFactories[contentType] + if !ok { + api.BadRequest(w, common.ErrValidation, errors.New("unsupported content type: "+contentType)) return } - if bulkMaxSize != 0 && len(b) > bulkMaxSize { - api.WriteErrorResponse(w, http.StatusRequestEntityTooLarge, ErrBulkSizeExceeded, fmt.Errorf("bulk size exceeded, max size is %d", bulkMaxSize)) + bulkHandler := bulkHandlerFactory.CreateBulkHandler() + send, receive, ok := bulkHandler.GetChannels(w, r) + if !ok { return } - w.Header().Set("Content-Type", "application/json") + l := common.LedgerFromContext(r.Context()) - ledgerController := common.LedgerFromContext(r.Context()) - - results, err := bulkerFactory.CreateBulker(ledgerController).Run(r.Context(), b, - ledgercontroller.WithContinueOnFailure(api.QueryParamBool(r, "continueOnFailure")), - ledgercontroller.WithAtomic(api.QueryParamBool(r, "atomic")), - ledgercontroller.WithParallel(api.QueryParamBool(r, "parallel")), + err := bulkerFactory.CreateBulker(l).Run(r.Context(), send, receive, + bulking.BulkingOptions{ + ContinueOnFailure: api.QueryParamBool(r, "continueOnFailure"), + Atomic: api.QueryParamBool(r, "atomic"), + Parallel: api.QueryParamBool(r, "parallel"), + }, ) if err != nil { api.InternalServerError(w, r, err) return } - if results.HasErrors() { - w.WriteHeader(http.StatusBadRequest) - } - - mappedResults := make([]Result, 0) - for ind, result := range results { - var ( - errorCode string - errorDescription string - responseType = b[ind].Action - ) - if result.Error != nil { - switch { - case errors.Is(result.Error, &ledgercontroller.ErrInsufficientFunds{}): - errorCode = ErrInsufficientFund - case errors.Is(result.Error, &ledgercontroller.ErrInvalidVars{}) || errors.Is(err, ledgercontroller.ErrCompilationFailed{}): - errorCode = ErrCompilationFailed - case errors.Is(result.Error, &ledgercontroller.ErrMetadataOverride{}): - errorCode = ErrMetadataOverride - case errors.Is(result.Error, ledgercontroller.ErrNoPostings): - errorCode = ErrNoPostings - case errors.Is(result.Error, ledgercontroller.ErrTransactionReferenceConflict{}): - errorCode = ErrConflict - case errors.Is(result.Error, ledgercontroller.ErrParsing{}): - errorCode = ErrInterpreterParse - case errors.Is(result.Error, ledgercontroller.ErrRuntime{}): - errorCode = ErrInterpreterRuntime - default: - errorCode = api.ErrorInternal - } - errorDescription = result.Error.Error() - responseType = "ERROR" - } - - mappedResults = append(mappedResults, Result{ - ErrorCode: errorCode, - ErrorDescription: errorDescription, - Data: result.Data, - ResponseType: responseType, - LogID: result.LogID, - }) - } - - if err := json.NewEncoder(w).Encode(api.BaseResponse[[]Result]{ - Data: pointer.For(mappedResults), - }); err != nil { - panic(err) - } + bulkHandler.Terminate(w, r) } } - -type Result struct { - ErrorCode string `json:"errorCode,omitempty"` - ErrorDescription string `json:"errorDescription,omitempty"` - Data any `json:"data,omitempty"` - ResponseType string `json:"responseType"` // Added for sdk generation (discriminator in oneOf) - LogID int `json:"logID"` -} diff --git a/internal/api/v2/controllers_bulk_test.go b/internal/api/v2/controllers_bulk_test.go index cea9bb27a..308d21104 100644 --- a/internal/api/v2/controllers_bulk_test.go +++ b/internal/api/v2/controllers_bulk_test.go @@ -3,6 +3,7 @@ package v2 import ( "bytes" "fmt" + "github.com/formancehq/ledger/internal/api/bulking" "math/big" "net/http" "net/http/httptest" @@ -35,7 +36,7 @@ func TestBulk(t *testing.T) { body string expectations func(mockLedger *LedgerController) expectError bool - expectResults []Result + expectResults []bulking.APIResult } testCases := []bulkTestCase{ @@ -77,7 +78,7 @@ func TestBulk(t *testing.T) { }, }, nil) }, - expectResults: []Result{{ + expectResults: []bulking.APIResult{{ Data: map[string]any{ "postings": []any{ map[string]any{ @@ -92,7 +93,7 @@ func TestBulk(t *testing.T) { "reverted": false, "id": float64(0), }, - ResponseType: ledgercontroller.ActionCreateTransaction, + ResponseType: bulking.ActionCreateTransaction, }}, }, { @@ -119,8 +120,8 @@ func TestBulk(t *testing.T) { }). Return(&ledger.Log{}, nil) }, - expectResults: []Result{{ - ResponseType: ledgercontroller.ActionAddMetadata, + expectResults: []bulking.APIResult{{ + ResponseType: bulking.ActionAddMetadata, }}, }, { @@ -147,8 +148,8 @@ func TestBulk(t *testing.T) { }). Return(&ledger.Log{}, nil) }, - expectResults: []Result{{ - ResponseType: ledgercontroller.ActionAddMetadata, + expectResults: []bulking.APIResult{{ + ResponseType: bulking.ActionAddMetadata, }}, }, { @@ -168,7 +169,7 @@ func TestBulk(t *testing.T) { }). Return(&ledger.Log{}, &ledger.RevertedTransaction{}, nil) }, - expectResults: []Result{{ + expectResults: []bulking.APIResult{{ Data: map[string]any{ "id": float64(0), "metadata": nil, @@ -176,7 +177,7 @@ func TestBulk(t *testing.T) { "reverted": false, "timestamp": "0001-01-01T00:00:00Z", }, - ResponseType: ledgercontroller.ActionRevertTransaction, + ResponseType: bulking.ActionRevertTransaction, }}, }, { @@ -199,8 +200,8 @@ func TestBulk(t *testing.T) { }). Return(&ledger.Log{}, nil) }, - expectResults: []Result{{ - ResponseType: ledgercontroller.ActionDeleteMetadata, + expectResults: []bulking.APIResult{{ + ResponseType: bulking.ActionDeleteMetadata, }}, }, { @@ -259,15 +260,15 @@ func TestBulk(t *testing.T) { }). Return(nil, errors.New("unexpected error")) }, - expectResults: []Result{{ - ResponseType: ledgercontroller.ActionAddMetadata, + expectResults: []bulking.APIResult{{ + ResponseType: bulking.ActionAddMetadata, }, { ErrorCode: api.ErrorInternal, ErrorDescription: "unexpected error", ResponseType: "ERROR", }, { ErrorCode: api.ErrorInternal, - ErrorDescription: "canceled", + ErrorDescription: "context canceled", ResponseType: "ERROR", }}, expectError: true, @@ -341,14 +342,14 @@ func TestBulk(t *testing.T) { }). Return(&ledger.Log{}, nil) }, - expectResults: []Result{{ - ResponseType: ledgercontroller.ActionAddMetadata, + expectResults: []bulking.APIResult{{ + ResponseType: bulking.ActionAddMetadata, }, { ResponseType: "ERROR", ErrorCode: api.ErrorInternal, ErrorDescription: "unexpected error", }, { - ResponseType: ledgercontroller.ActionAddMetadata, + ResponseType: bulking.ActionAddMetadata, }}, expectError: true, }, @@ -410,15 +411,16 @@ func TestBulk(t *testing.T) { Commit(gomock.Any()). Return(nil) }, - expectResults: []Result{{ - ResponseType: ledgercontroller.ActionAddMetadata, + expectResults: []bulking.APIResult{{ + ResponseType: bulking.ActionAddMetadata, }, { - ResponseType: ledgercontroller.ActionAddMetadata, + ResponseType: bulking.ActionAddMetadata, }}, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { + t.Parallel() systemController, ledgerController := newTestingSystemController(t, true) testCase.expectations(ledgerController) @@ -439,8 +441,8 @@ func TestBulk(t *testing.T) { require.Equal(t, http.StatusOK, rec.Code) } - ret, _ := api.DecodeSingleResponse[[]Result](t, rec.Body) - ret = collectionutils.Map(ret, func(from Result) Result { + ret, _ := api.DecodeSingleResponse[[]bulking.APIResult](t, rec.Body) + ret = collectionutils.Map(ret, func(from bulking.APIResult) bulking.APIResult { switch data := from.Data.(type) { case map[string]any: delete(data, "insertedAt") diff --git a/internal/api/v2/controllers_ledgers_create.go b/internal/api/v2/controllers_ledgers_create.go index 488b6d1d9..3bba4617f 100644 --- a/internal/api/v2/controllers_ledgers_create.go +++ b/internal/api/v2/controllers_ledgers_create.go @@ -26,7 +26,7 @@ func createLedger(systemController system.Controller) http.HandlerFunc { if len(data) > 0 { if err := json.Unmarshal(data, &configuration); err != nil { - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) return } } @@ -36,9 +36,9 @@ func createLedger(systemController system.Controller) http.HandlerFunc { case errors.Is(err, system.ErrInvalidLedgerConfiguration{}) || errors.Is(err, ledger.ErrInvalidLedgerName{}) || errors.Is(err, ledger.ErrInvalidBucketName{}): - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) case errors.Is(err, system.ErrLedgerAlreadyExists): - api.BadRequest(w, ErrLedgerAlreadyExists, err) + api.BadRequest(w, common.ErrLedgerAlreadyExists, err) default: common.HandleCommonErrors(w, r, err) } diff --git a/internal/api/v2/controllers_ledgers_create_test.go b/internal/api/v2/controllers_ledgers_create_test.go index 4b0874292..79fa67f1e 100644 --- a/internal/api/v2/controllers_ledgers_create_test.go +++ b/internal/api/v2/controllers_ledgers_create_test.go @@ -3,6 +3,7 @@ package v2 import ( "bytes" "encoding/json" + "github.com/formancehq/ledger/internal/api/common" "net/http" "net/http/httptest" "os" @@ -55,28 +56,28 @@ func TestLedgersCreate(t *testing.T) { expectedBackendCall: true, returnErr: system.ErrLedgerAlreadyExists, expectStatusCode: http.StatusBadRequest, - expectErrorCode: ErrLedgerAlreadyExists, + expectErrorCode: common.ErrLedgerAlreadyExists, }, { name: "invalid ledger name", expectedBackendCall: true, returnErr: ledger.ErrInvalidLedgerName{}, expectStatusCode: http.StatusBadRequest, - expectErrorCode: ErrValidation, + expectErrorCode: common.ErrValidation, }, { name: "invalid bucket name", expectedBackendCall: true, returnErr: ledger.ErrInvalidBucketName{}, expectStatusCode: http.StatusBadRequest, - expectErrorCode: ErrValidation, + expectErrorCode: common.ErrValidation, }, { name: "invalid ledger configuration", expectedBackendCall: true, returnErr: system.ErrInvalidLedgerConfiguration{}, expectStatusCode: http.StatusBadRequest, - expectErrorCode: ErrValidation, + expectErrorCode: common.ErrValidation, }, { name: "unexpected error", diff --git a/internal/api/v2/controllers_ledgers_list.go b/internal/api/v2/controllers_ledgers_list.go index 6ef87fd35..8f2a70343 100644 --- a/internal/api/v2/controllers_ledgers_list.go +++ b/internal/api/v2/controllers_ledgers_list.go @@ -24,7 +24,7 @@ func listLedgers(b system.Controller) http.HandlerFunc { return pointer.For(ledgercontroller.NewListLedgersQuery(pageSize)), nil }) if err != nil { - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) return } @@ -32,7 +32,7 @@ func listLedgers(b system.Controller) http.HandlerFunc { if err != nil { switch { case errors.Is(err, ledgercontroller.ErrInvalidQuery{}) || errors.Is(err, ledgercontroller.ErrMissingFeature{}): - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) default: common.HandleCommonErrors(w, r, err) } diff --git a/internal/api/v2/controllers_ledgers_list_test.go b/internal/api/v2/controllers_ledgers_list_test.go index ca8627348..b72785af5 100644 --- a/internal/api/v2/controllers_ledgers_list_test.go +++ b/internal/api/v2/controllers_ledgers_list_test.go @@ -2,6 +2,7 @@ package v2 import ( "encoding/json" + "github.com/formancehq/ledger/internal/api/common" "net/http" "net/http/httptest" "net/url" @@ -53,7 +54,7 @@ func TestListLedgers(t *testing.T) { "pageSize": {"-1"}, }, expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, expectBackendCall: false, }, { @@ -67,7 +68,7 @@ func TestListLedgers(t *testing.T) { { name: "with invalid query from core point of view", expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, expectBackendCall: true, returnErr: ledgercontroller.ErrInvalidQuery{}, expectQuery: ledgercontroller.NewListLedgersQuery(DefaultPageSize), @@ -75,7 +76,7 @@ func TestListLedgers(t *testing.T) { { name: "with missing feature", expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, expectBackendCall: true, returnErr: ledgercontroller.ErrMissingFeature{}, expectQuery: ledgercontroller.NewListLedgersQuery(DefaultPageSize), diff --git a/internal/api/v2/controllers_ledgers_update_metadata.go b/internal/api/v2/controllers_ledgers_update_metadata.go index d7f487775..c18cd2e90 100644 --- a/internal/api/v2/controllers_ledgers_update_metadata.go +++ b/internal/api/v2/controllers_ledgers_update_metadata.go @@ -17,7 +17,7 @@ func updateLedgerMetadata(systemController systemcontroller.Controller) http.Han m := metadata.Metadata{} if err := json.NewDecoder(r.Body).Decode(&m); err != nil { - api.BadRequest(w, ErrValidation, errors.New("invalid format")) + api.BadRequest(w, common.ErrValidation, errors.New("invalid format")) return } diff --git a/internal/api/v2/controllers_logs_list.go b/internal/api/v2/controllers_logs_list.go index 8fa863c02..231c0f278 100644 --- a/internal/api/v2/controllers_logs_list.go +++ b/internal/api/v2/controllers_logs_list.go @@ -20,7 +20,7 @@ func listLogs(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get(QueryKeyCursor) != "" { err := bunpaginate.UnmarshalCursor(r.URL.Query().Get(QueryKeyCursor), &query) if err != nil { - api.BadRequest(w, ErrValidation, fmt.Errorf("invalid '%s' query param", QueryKeyCursor)) + api.BadRequest(w, common.ErrValidation, fmt.Errorf("invalid '%s' query param", QueryKeyCursor)) return } } else { @@ -28,13 +28,13 @@ func listLogs(w http.ResponseWriter, r *http.Request) { pageSize, err := bunpaginate.GetPageSize(r) if err != nil { - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) return } qb, err := getQueryBuilder(r) if err != nil { - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) return } @@ -48,7 +48,7 @@ func listLogs(w http.ResponseWriter, r *http.Request) { if err != nil { switch { case errors.Is(err, ledgercontroller.ErrInvalidQuery{}): - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) default: common.HandleCommonErrors(w, r, err) } diff --git a/internal/api/v2/controllers_logs_list_test.go b/internal/api/v2/controllers_logs_list_test.go index a301a7694..c06db4189 100644 --- a/internal/api/v2/controllers_logs_list_test.go +++ b/internal/api/v2/controllers_logs_list_test.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "github.com/formancehq/ledger/internal/api/common" "net/http" "net/http/httptest" "net/url" @@ -70,7 +71,7 @@ func TestGetLogs(t *testing.T) { "cursor": []string{"xxx"}, }, expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, { name: "using invalid page size", @@ -78,19 +79,19 @@ func TestGetLogs(t *testing.T) { "pageSize": []string{"-1"}, }, expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, { name: "using malformed query", body: `[]`, expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, { name: "with invalid query", expectStatusCode: http.StatusBadRequest, expectQuery: ledgercontroller.NewPaginatedQueryOptions[any](nil), - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, expectBackendCall: true, returnErr: ledgercontroller.ErrInvalidQuery{}, }, diff --git a/internal/api/v2/controllers_transactions_add_metadata.go b/internal/api/v2/controllers_transactions_add_metadata.go index 9b1118b72..c64c4b997 100644 --- a/internal/api/v2/controllers_transactions_add_metadata.go +++ b/internal/api/v2/controllers_transactions_add_metadata.go @@ -19,13 +19,13 @@ func addTransactionMetadata(w http.ResponseWriter, r *http.Request) { var m metadata.Metadata if err := json.NewDecoder(r.Body).Decode(&m); err != nil { - api.BadRequest(w, ErrValidation, errors.New("invalid metadata format")) + api.BadRequest(w, common.ErrValidation, errors.New("invalid metadata format")) return } txID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) return } diff --git a/internal/api/v2/controllers_transactions_add_metadata_test.go b/internal/api/v2/controllers_transactions_add_metadata_test.go index 8f108cd9b..1aaa90a50 100644 --- a/internal/api/v2/controllers_transactions_add_metadata_test.go +++ b/internal/api/v2/controllers_transactions_add_metadata_test.go @@ -2,6 +2,7 @@ package v2 import ( "fmt" + "github.com/formancehq/ledger/internal/api/common" "net/http" "net/http/httptest" "net/url" @@ -45,13 +46,13 @@ func TestTransactionsAddMetadata(t *testing.T) { name: "invalid body", body: "invalid - not an object", expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, { name: "invalid id", id: "abc", expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, { name: "not found", diff --git a/internal/api/v2/controllers_transactions_count.go b/internal/api/v2/controllers_transactions_count.go index d256d2973..3388d07fc 100644 --- a/internal/api/v2/controllers_transactions_count.go +++ b/internal/api/v2/controllers_transactions_count.go @@ -14,7 +14,7 @@ func countTransactions(w http.ResponseWriter, r *http.Request) { options, err := getPaginatedQueryOptionsOfPITFilterWithVolumes(r) if err != nil { - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) return } @@ -23,7 +23,7 @@ func countTransactions(w http.ResponseWriter, r *http.Request) { if err != nil { switch { case errors.Is(err, ledgercontroller.ErrInvalidQuery{}) || errors.Is(err, ledgercontroller.ErrMissingFeature{}): - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) default: common.HandleCommonErrors(w, r, err) } diff --git a/internal/api/v2/controllers_transactions_count_test.go b/internal/api/v2/controllers_transactions_count_test.go index 52fc52e14..a7a53a4a0 100644 --- a/internal/api/v2/controllers_transactions_count_test.go +++ b/internal/api/v2/controllers_transactions_count_test.go @@ -3,6 +3,7 @@ package v2 import ( "bytes" "fmt" + "github.com/formancehq/ledger/internal/api/common" "net/http" "net/http/httptest" "net/url" @@ -138,7 +139,7 @@ func TestTransactionsCount(t *testing.T) { { name: "with invalid query from core point of view", expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, expectBackendCall: true, returnErr: ledgercontroller.ErrInvalidQuery{}, expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ @@ -150,7 +151,7 @@ func TestTransactionsCount(t *testing.T) { { name: "with missing feature", expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, expectBackendCall: true, returnErr: ledgercontroller.ErrMissingFeature{}, expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ diff --git a/internal/api/v2/controllers_transactions_create.go b/internal/api/v2/controllers_transactions_create.go index 523882fbc..beac0032f 100644 --- a/internal/api/v2/controllers_transactions_create.go +++ b/internal/api/v2/controllers_transactions_create.go @@ -2,6 +2,7 @@ package v2 import ( "encoding/json" + "github.com/formancehq/ledger/internal/api/bulking" "net/http" ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" @@ -16,24 +17,24 @@ import ( func createTransaction(w http.ResponseWriter, r *http.Request) { l := common.LedgerFromContext(r.Context()) - payload := ledgercontroller.TransactionRequest{} + payload := bulking.TransactionRequest{} if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { - api.BadRequest(w, ErrValidation, errors.New("invalid transaction format")) + api.BadRequest(w, common.ErrValidation, errors.New("invalid transaction format")) return } if len(payload.Postings) > 0 && payload.Script.Plain != "" { - api.BadRequest(w, ErrValidation, errors.New("cannot pass postings and numscript in the same request")) + api.BadRequest(w, common.ErrValidation, errors.New("cannot pass postings and numscript in the same request")) return } if len(payload.Postings) == 0 && payload.Script.Plain == "" { - api.BadRequest(w, ErrNoPostings, errors.New("you need to pass either a posting array or a numscript script")) + api.BadRequest(w, common.ErrNoPostings, errors.New("you need to pass either a posting array or a numscript script")) return } runScript, err := payload.ToRunScript(api.QueryParamBool(r, "force")) if err != nil { - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) return } @@ -41,21 +42,21 @@ func createTransaction(w http.ResponseWriter, r *http.Request) { if err != nil { switch { case errors.Is(err, &ledgercontroller.ErrInsufficientFunds{}): - api.BadRequest(w, ErrInsufficientFund, err) + api.BadRequest(w, common.ErrInsufficientFund, err) case errors.Is(err, &ledgercontroller.ErrInvalidVars{}) || errors.Is(err, ledgercontroller.ErrCompilationFailed{}): - api.BadRequest(w, ErrCompilationFailed, err) + api.BadRequest(w, common.ErrCompilationFailed, err) case errors.Is(err, &ledgercontroller.ErrMetadataOverride{}): - api.BadRequest(w, ErrMetadataOverride, err) + api.BadRequest(w, common.ErrMetadataOverride, err) case errors.Is(err, ledgercontroller.ErrNoPostings): - api.BadRequest(w, ErrNoPostings, err) + api.BadRequest(w, common.ErrNoPostings, err) case errors.Is(err, ledgercontroller.ErrTransactionReferenceConflict{}): - api.WriteErrorResponse(w, http.StatusConflict, ErrConflict, err) + api.WriteErrorResponse(w, http.StatusConflict, common.ErrConflict, err) case errors.Is(err, ledgercontroller.ErrInvalidIdempotencyInput{}): - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) case errors.Is(err, ledgercontroller.ErrParsing{}): - api.BadRequest(w, ErrInterpreterParse, err) + api.BadRequest(w, common.ErrInterpreterParse, err) case errors.Is(err, ledgercontroller.ErrRuntime{}): - api.BadRequest(w, ErrInterpreterRuntime, err) + api.BadRequest(w, common.ErrInterpreterRuntime, err) default: common.HandleCommonErrors(w, r, err) } diff --git a/internal/api/v2/controllers_transactions_create_test.go b/internal/api/v2/controllers_transactions_create_test.go index e6ada7517..38ddafd10 100644 --- a/internal/api/v2/controllers_transactions_create_test.go +++ b/internal/api/v2/controllers_transactions_create_test.go @@ -1,6 +1,8 @@ package v2 import ( + "github.com/formancehq/ledger/internal/api/bulking" + "github.com/formancehq/ledger/internal/api/common" "math/big" "net/http" "net/http/httptest" @@ -35,7 +37,7 @@ func TestTransactionCreate(t *testing.T) { testCases := []testCase{ { name: "using plain numscript", - payload: ledgercontroller.TransactionRequest{ + payload: bulking.TransactionRequest{ Script: ledgercontroller.ScriptV1{ Script: ledgercontroller.Script{ Plain: `XXX`, @@ -52,7 +54,7 @@ func TestTransactionCreate(t *testing.T) { }, { name: "using plain numscript with variables", - payload: ledgercontroller.TransactionRequest{ + payload: bulking.TransactionRequest{ Script: ledgercontroller.ScriptV1{ Script: ledgercontroller.Script{ Plain: `vars { @@ -89,7 +91,7 @@ func TestTransactionCreate(t *testing.T) { { name: "using plain numscript with variables (legacy format)", expectControllerCall: true, - payload: ledgercontroller.TransactionRequest{ + payload: bulking.TransactionRequest{ Script: ledgercontroller.ScriptV1{ Script: ledgercontroller.Script{ Plain: `vars { @@ -128,7 +130,7 @@ func TestTransactionCreate(t *testing.T) { { name: "using plain numscript and dry run", expectControllerCall: true, - payload: ledgercontroller.TransactionRequest{ + payload: bulking.TransactionRequest{ Script: ledgercontroller.ScriptV1{ Script: ledgercontroller.Script{ Plain: `send ( @@ -155,7 +157,7 @@ func TestTransactionCreate(t *testing.T) { { name: "using JSON postings", expectControllerCall: true, - payload: ledgercontroller.TransactionRequest{ + payload: bulking.TransactionRequest{ Postings: []ledger.Posting{ ledger.NewPosting("world", "bank", "USD", big.NewInt(100)), }, @@ -170,7 +172,7 @@ func TestTransactionCreate(t *testing.T) { queryParams: url.Values{ "dryRun": []string{"true"}, }, - payload: ledgercontroller.TransactionRequest{ + payload: bulking.TransactionRequest{ Postings: []ledger.Posting{ ledger.NewPosting("world", "bank", "USD", big.NewInt(100)), }, @@ -182,16 +184,16 @@ func TestTransactionCreate(t *testing.T) { }, { name: "no postings or script", - payload: ledgercontroller.TransactionRequest{ + payload: bulking.TransactionRequest{ Metadata: map[string]string{}, }, expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrNoPostings, + expectedErrorCode: common.ErrNoPostings, returnError: errors.New("you need to pass either a posting array or a numscript script"), }, { name: "postings and script", - payload: ledgercontroller.TransactionRequest{ + payload: bulking.TransactionRequest{ Postings: ledger.Postings{ { Source: "world", @@ -211,18 +213,18 @@ func TestTransactionCreate(t *testing.T) { }, }, expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, { name: "using invalid body", payload: "not a valid payload", expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, { name: "with insufficient funds", expectControllerCall: true, - payload: ledgercontroller.TransactionRequest{ + payload: bulking.TransactionRequest{ Script: ledgercontroller.ScriptV1{ Script: ledgercontroller.Script{ Plain: `XXX`, @@ -237,22 +239,22 @@ func TestTransactionCreate(t *testing.T) { }, returnError: &ledgercontroller.ErrInsufficientFunds{}, expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrInsufficientFund, + expectedErrorCode: common.ErrInsufficientFund, }, { name: "using JSON postings and negative amount", - payload: ledgercontroller.TransactionRequest{ + payload: bulking.TransactionRequest{ Postings: []ledger.Posting{ ledger.NewPosting("world", "bank", "USD", big.NewInt(-100)), }, }, expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, { expectControllerCall: true, name: "numscript and negative amount", - payload: ledgercontroller.TransactionRequest{ + payload: bulking.TransactionRequest{ Script: ledgercontroller.ScriptV1{ Script: ledgercontroller.Script{ Plain: `send [COIN -100] ( @@ -263,7 +265,7 @@ func TestTransactionCreate(t *testing.T) { }, }, expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrCompilationFailed, + expectedErrorCode: common.ErrCompilationFailed, expectedRunScript: ledgercontroller.RunScript{ Script: ledgercontroller.Script{ Plain: `send [COIN -100] ( @@ -278,7 +280,7 @@ func TestTransactionCreate(t *testing.T) { { name: "numscript and compilation failed", expectControllerCall: true, - payload: ledgercontroller.TransactionRequest{ + payload: bulking.TransactionRequest{ Script: ledgercontroller.ScriptV1{ Script: ledgercontroller.Script{ Plain: `send [COIN XXX] ( @@ -289,7 +291,7 @@ func TestTransactionCreate(t *testing.T) { }, }, expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrCompilationFailed, + expectedErrorCode: common.ErrCompilationFailed, expectedRunScript: ledgercontroller.RunScript{ Script: ledgercontroller.Script{ Plain: `send [COIN XXX] ( @@ -304,7 +306,7 @@ func TestTransactionCreate(t *testing.T) { { name: "numscript and no postings", expectControllerCall: true, - payload: ledgercontroller.TransactionRequest{ + payload: bulking.TransactionRequest{ Script: ledgercontroller.ScriptV1{ Script: ledgercontroller.Script{ Plain: `vars {}`, @@ -312,7 +314,7 @@ func TestTransactionCreate(t *testing.T) { }, }, expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrNoPostings, + expectedErrorCode: common.ErrNoPostings, expectedRunScript: ledgercontroller.RunScript{ Script: ledgercontroller.Script{ Plain: `vars {}`, @@ -324,7 +326,7 @@ func TestTransactionCreate(t *testing.T) { { name: "numscript and metadata override", expectControllerCall: true, - payload: ledgercontroller.TransactionRequest{ + payload: bulking.TransactionRequest{ Script: ledgercontroller.ScriptV1{ Script: ledgercontroller.Script{ Plain: `send [COIN 100] ( @@ -340,7 +342,7 @@ func TestTransactionCreate(t *testing.T) { }, }, expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrMetadataOverride, + expectedErrorCode: common.ErrMetadataOverride, expectedRunScript: ledgercontroller.RunScript{ Script: ledgercontroller.Script{ Plain: `send [COIN 100] ( @@ -360,7 +362,7 @@ func TestTransactionCreate(t *testing.T) { { name: "unexpected error", expectControllerCall: true, - payload: ledgercontroller.TransactionRequest{ + payload: bulking.TransactionRequest{ Script: ledgercontroller.ScriptV1{ Script: ledgercontroller.Script{ Plain: `send [COIN 100] ( diff --git a/internal/api/v2/controllers_transactions_delete_metadata.go b/internal/api/v2/controllers_transactions_delete_metadata.go index f14067c30..0628eedc7 100644 --- a/internal/api/v2/controllers_transactions_delete_metadata.go +++ b/internal/api/v2/controllers_transactions_delete_metadata.go @@ -19,7 +19,7 @@ func deleteTransactionMetadata(w http.ResponseWriter, r *http.Request) { txID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) return } diff --git a/internal/api/v2/controllers_transactions_list.go b/internal/api/v2/controllers_transactions_list.go index e0ee50f75..b64839dac 100644 --- a/internal/api/v2/controllers_transactions_list.go +++ b/internal/api/v2/controllers_transactions_list.go @@ -31,7 +31,7 @@ func listTransactions(w http.ResponseWriter, r *http.Request) { return pointer.For(q), nil }) if err != nil { - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) return } @@ -39,7 +39,7 @@ func listTransactions(w http.ResponseWriter, r *http.Request) { if err != nil { switch { case errors.Is(err, ledgercontroller.ErrInvalidQuery{}) || errors.Is(err, ledgercontroller.ErrMissingFeature{}): - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) default: common.HandleCommonErrors(w, r, err) } diff --git a/internal/api/v2/controllers_transactions_list_test.go b/internal/api/v2/controllers_transactions_list_test.go index d4fc10e01..8ff17eff3 100644 --- a/internal/api/v2/controllers_transactions_list_test.go +++ b/internal/api/v2/controllers_transactions_list_test.go @@ -3,6 +3,7 @@ package v2 import ( "bytes" "fmt" + "github.com/formancehq/ledger/internal/api/common" "math/big" "net/http" "net/http/httptest" @@ -128,7 +129,7 @@ func TestTransactionsList(t *testing.T) { "cursor": []string{"XXX"}, }, expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, { name: "invalid page size", @@ -136,7 +137,7 @@ func TestTransactionsList(t *testing.T) { "pageSize": []string{"nan"}, }, expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, { name: "page size over maximum", diff --git a/internal/api/v2/controllers_transactions_read.go b/internal/api/v2/controllers_transactions_read.go index 6899c3bb2..ef00f5e00 100644 --- a/internal/api/v2/controllers_transactions_read.go +++ b/internal/api/v2/controllers_transactions_read.go @@ -16,7 +16,7 @@ func readTransaction(w http.ResponseWriter, r *http.Request) { txId, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) return } @@ -30,7 +30,7 @@ func readTransaction(w http.ResponseWriter, r *http.Request) { pitFilter, err := getPITFilter(r) if err != nil { - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) return } query.PITFilter = *pitFilter diff --git a/internal/api/v2/controllers_transactions_revert.go b/internal/api/v2/controllers_transactions_revert.go index aae713ef1..ebfe536fd 100644 --- a/internal/api/v2/controllers_transactions_revert.go +++ b/internal/api/v2/controllers_transactions_revert.go @@ -17,7 +17,7 @@ func revertTransaction(w http.ResponseWriter, r *http.Request) { txId, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) return } @@ -32,9 +32,9 @@ func revertTransaction(w http.ResponseWriter, r *http.Request) { if err != nil { switch { case errors.Is(err, &ledgercontroller.ErrInsufficientFunds{}): - api.BadRequest(w, ErrInsufficientFund, err) + api.BadRequest(w, common.ErrInsufficientFund, err) case errors.Is(err, ledgercontroller.ErrAlreadyReverted{}): - api.BadRequest(w, ErrAlreadyRevert, err) + api.BadRequest(w, common.ErrAlreadyRevert, err) case errors.Is(err, ledgercontroller.ErrNotFound): api.NotFound(w, err) default: diff --git a/internal/api/v2/controllers_transactions_revert_test.go b/internal/api/v2/controllers_transactions_revert_test.go index 8b03710a8..162c401d5 100644 --- a/internal/api/v2/controllers_transactions_revert_test.go +++ b/internal/api/v2/controllers_transactions_revert_test.go @@ -1,6 +1,7 @@ package v2 import ( + "github.com/formancehq/ledger/internal/api/common" "math/big" "net/http" "net/http/httptest" @@ -47,13 +48,13 @@ func TestTransactionsRevert(t *testing.T) { name: "with insufficient fund", returnErr: &ledgercontroller.ErrInsufficientFunds{}, expectStatusCode: http.StatusBadRequest, - expectErrorCode: ErrInsufficientFund, + expectErrorCode: common.ErrInsufficientFund, }, { name: "with already revert", returnErr: &ledgercontroller.ErrAlreadyReverted{}, expectStatusCode: http.StatusBadRequest, - expectErrorCode: ErrAlreadyRevert, + expectErrorCode: common.ErrAlreadyRevert, }, { name: "with transaction not found", diff --git a/internal/api/v2/controllers_volumes.go b/internal/api/v2/controllers_volumes.go index ce6a85253..caef27739 100644 --- a/internal/api/v2/controllers_volumes.go +++ b/internal/api/v2/controllers_volumes.go @@ -30,7 +30,7 @@ func readVolumes(w http.ResponseWriter, r *http.Request) { }) if err != nil { - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) return } @@ -39,7 +39,7 @@ func readVolumes(w http.ResponseWriter, r *http.Request) { if err != nil { switch { case errors.Is(err, ledgercontroller.ErrInvalidQuery{}) || errors.Is(err, ledgercontroller.ErrMissingFeature{}): - api.BadRequest(w, ErrValidation, err) + api.BadRequest(w, common.ErrValidation, err) default: common.HandleCommonErrors(w, r, err) } diff --git a/internal/api/v2/controllers_volumes_test.go b/internal/api/v2/controllers_volumes_test.go index 7dc7777ed..7d5b7a183 100644 --- a/internal/api/v2/controllers_volumes_test.go +++ b/internal/api/v2/controllers_volumes_test.go @@ -2,6 +2,7 @@ package v2 import ( "bytes" + "github.com/formancehq/ledger/internal/api/common" "math/big" "net/http" "net/http/httptest" @@ -74,7 +75,7 @@ func TestGetVolumes(t *testing.T) { name: "using invalid query payload", body: `[]`, expectStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, + expectedErrorCode: common.ErrValidation, }, { name: "using pit", diff --git a/internal/api/v2/errors.go b/internal/api/v2/errors.go deleted file mode 100644 index cdcc2b254..000000000 --- a/internal/api/v2/errors.go +++ /dev/null @@ -1,16 +0,0 @@ -package v2 - -const ( - ErrConflict = "CONFLICT" - ErrInsufficientFund = "INSUFFICIENT_FUND" - ErrValidation = "VALIDATION" - ErrAlreadyRevert = "ALREADY_REVERT" - ErrNoPostings = "NO_POSTINGS" - ErrCompilationFailed = "COMPILATION_FAILED" - ErrMetadataOverride = "METADATA_OVERRIDE" - ErrBulkSizeExceeded = "BULK_SIZE_EXCEEDED" - ErrLedgerAlreadyExists = "LEDGER_ALREADY_EXISTS" - - ErrInterpreterParse = "INTERPRETER_PARSE" - ErrInterpreterRuntime = "INTERPRETER_RUNTIME" -) diff --git a/internal/api/v2/routes.go b/internal/api/v2/routes.go index eea3ef656..5cd635fbc 100644 --- a/internal/api/v2/routes.go +++ b/internal/api/v2/routes.go @@ -1,7 +1,7 @@ package v2 import ( - "github.com/formancehq/ledger/internal/controller/ledger" + "github.com/formancehq/ledger/internal/api/bulking" nooptracer "go.opentelemetry.io/otel/trace/noop" "net/http" @@ -53,7 +53,10 @@ func NewRouter( router.With(common.LedgerMiddleware(systemController, func(r *http.Request) string { return chi.URLParam(r, "ledger") }, routerOptions.tracer, "/_info")).Group(func(router chi.Router) { - router.Post("/_bulk", bulkHandler(routerOptions.bulkerFactory, routerOptions.bulkMaxSize)) + router.Post("/_bulk", bulkHandler( + routerOptions.bulkerFactory, + routerOptions.bulkHandlerFactories, + )) // LedgerController router.Get("/_info", getLedgerInfo) @@ -91,10 +94,10 @@ func NewRouter( } type routerOptions struct { - tracer trace.Tracer - middlewares []func(http.Handler) http.Handler - bulkerFactory ledger.BulkerFactory - bulkMaxSize int + tracer trace.Tracer + middlewares []func(http.Handler) http.Handler + bulkerFactory bulking.BulkerFactory + bulkHandlerFactories map[string]bulking.HandlerFactory } type RouterOption func(ro *routerOptions) @@ -111,13 +114,13 @@ func WithMiddlewares(middlewares ...func(http.Handler) http.Handler) RouterOptio } } -func WithBulkMaxSize(bulkMaxSize int) RouterOption { +func WithBulkHandlerFactories(bulkHandlerFactories map[string]bulking.HandlerFactory) RouterOption { return func(ro *routerOptions) { - ro.bulkMaxSize = bulkMaxSize + ro.bulkHandlerFactories = bulkHandlerFactories } } -func WithBulkerFactory(bulkerFactory ledger.BulkerFactory) RouterOption { +func WithBulkerFactory(bulkerFactory bulking.BulkerFactory) RouterOption { return func(ro *routerOptions) { ro.bulkerFactory = bulkerFactory } @@ -125,5 +128,9 @@ func WithBulkerFactory(bulkerFactory ledger.BulkerFactory) RouterOption { var defaultRouterOptions = []RouterOption{ WithTracer(nooptracer.Tracer{}), - WithBulkerFactory(ledger.NewDefaultBulkerFactory()), + WithBulkerFactory(bulking.NewDefaultBulkerFactory()), + WithBulkHandlerFactories(map[string]bulking.HandlerFactory{ + "application/json": bulking.NewJSONBulkHandlerFactory(100), + "application/vnd.formance.ledger.api.v2.bulk+script-stream": bulking.NewScriptStreamBulkHandlerFactory(), + }), } diff --git a/internal/controller/ledger/bulker.go b/internal/controller/ledger/bulker.go deleted file mode 100644 index 49e620fce..000000000 --- a/internal/controller/ledger/bulker.go +++ /dev/null @@ -1,426 +0,0 @@ -package ledger - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "github.com/alitto/pond" - "github.com/formancehq/go-libs/v2/logging" - "github.com/formancehq/go-libs/v2/metadata" - "github.com/formancehq/go-libs/v2/pointer" - "github.com/formancehq/go-libs/v2/time" - ledger "github.com/formancehq/ledger/internal" - "sync" -) - -const ( - ActionCreateTransaction = "CREATE_TRANSACTION" - ActionAddMetadata = "ADD_METADATA" - ActionRevertTransaction = "REVERT_TRANSACTION" - ActionDeleteMetadata = "DELETE_METADATA" -) - -type Bulk []BulkElement - -type BulkResult []BulkElementResult - -func (r BulkResult) HasErrors() bool { - for _, element := range r { - if element.Error != nil { - return true - } - } - - return false -} - -type BulkElement struct { - Action string `json:"action"` - IdempotencyKey string `json:"ik"` - Data json.RawMessage `json:"data"` -} - -type BulkElementResult struct { - Error error - Data any `json:"data,omitempty"` - LogID int `json:"logID"` -} - -type AddMetadataRequest struct { - TargetType string `json:"targetType"` - TargetID json.RawMessage `json:"targetId"` - Metadata metadata.Metadata `json:"metadata"` -} - -type RevertTransactionRequest struct { - ID int `json:"id"` - Force bool `json:"force"` - AtEffectiveDate bool `json:"atEffectiveDate"` -} - -type DeleteMetadataRequest struct { - TargetType string `json:"targetType"` - TargetID json.RawMessage `json:"targetId"` - Key string `json:"key"` -} - -type TransactionRequest struct { - Postings ledger.Postings `json:"postings"` - Script ScriptV1 `json:"script"` - Timestamp time.Time `json:"timestamp"` - Reference string `json:"reference"` - Metadata metadata.Metadata `json:"metadata" swaggertype:"object"` -} - -func (req *TransactionRequest) ToRunScript(allowUnboundedOverdrafts bool) (*RunScript, error) { - - if _, err := req.Postings.Validate(); err != nil { - return nil, err - } - - if len(req.Postings) > 0 { - txData := ledger.TransactionData{ - Postings: req.Postings, - Timestamp: req.Timestamp, - Reference: req.Reference, - Metadata: req.Metadata, - } - - return pointer.For(TxToScriptData(txData, allowUnboundedOverdrafts)), nil - } - - return &RunScript{ - Script: req.Script.ToCore(), - Timestamp: req.Timestamp, - Reference: req.Reference, - Metadata: req.Metadata, - }, nil -} - -type Bulker struct { - ctrl Controller - parallelism int -} - -func (b *Bulker) run(ctx context.Context, ctrl Controller, bulk Bulk, continueOnFailure, parallel bool) (BulkResult, error) { - - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - parallelism := 1 - if parallel && b.parallelism != 0 { - parallelism = b.parallelism - } - - wp := pond.New(parallelism, len(bulk), pond.Context(ctx)) - results := sync.Map{} - - for index, element := range bulk { - wp.Submit(func() { - ret, logID, err := b.processElement(ctx, ctrl, element) - if err != nil { - results.Store(index, BulkElementResult{ - Error: err, - }) - - if !continueOnFailure { - cancel() - } - - return - } - - results.Store(index, BulkElementResult{ - Data: ret, - LogID: logID, - }) - }) - } - - wp.StopAndWait() - - finalResults := make(BulkResult, 0, len(bulk)) - for index := range bulk { - v, ok := results.Load(index) - if ok { - finalResults = append(finalResults, v.(BulkElementResult)) - continue - } - finalResults = append(finalResults, BulkElementResult{ - Error: errors.New("canceled"), - }) - } - - return finalResults, nil -} - -func (b *Bulker) Run(ctx context.Context, bulk Bulk, providedOptions ...BulkingOption) (BulkResult, error) { - - bulkOptions := BulkingOptions{} - for _, option := range providedOptions { - option(&bulkOptions) - } - - for i, element := range bulk { - switch element.Action { - case ActionCreateTransaction: - req := &TransactionRequest{} - if err := json.Unmarshal(element.Data, req); err != nil { - return nil, fmt.Errorf("error parsing element %d: %s", i, err) - } - case ActionAddMetadata: - req := &AddMetadataRequest{} - if err := json.Unmarshal(element.Data, req); err != nil { - return nil, fmt.Errorf("error parsing element %d: %s", i, err) - } - case ActionRevertTransaction: - req := &RevertTransactionRequest{} - if err := json.Unmarshal(element.Data, req); err != nil { - return nil, fmt.Errorf("error parsing element %d: %s", i, err) - } - case ActionDeleteMetadata: - req := &DeleteMetadataRequest{} - if err := json.Unmarshal(element.Data, req); err != nil { - return nil, fmt.Errorf("error parsing element %d: %s", i, err) - } - } - } - - ctrl := b.ctrl - if bulkOptions.Atomic { - var err error - ctrl, err = ctrl.BeginTX(ctx, nil) - if err != nil { - return nil, fmt.Errorf("error starting transaction: %s", err) - } - } - - results, err := b.run(ctx, ctrl, bulk, bulkOptions.ContinueOnFailure, bulkOptions.Parallel) - if err != nil { - if bulkOptions.Atomic { - if rollbackErr := ctrl.Rollback(ctx); rollbackErr != nil { - logging.FromContext(ctx).Errorf("failed to rollback transaction: %v", rollbackErr) - } - } - - return nil, fmt.Errorf("error running bulk: %s", err) - } - - if bulkOptions.Atomic { - if results.HasErrors() { - if rollbackErr := ctrl.Rollback(ctx); rollbackErr != nil { - logging.FromContext(ctx).Errorf("failed to rollback transaction: %v", rollbackErr) - } - } else { - if err := ctrl.Commit(ctx); err != nil { - return nil, fmt.Errorf("error committing transaction: %s", err) - } - } - } - - return results, err -} - -func (b *Bulker) processElement(ctx context.Context, ctrl Controller, element BulkElement) (any, int, error) { - - switch element.Action { - case ActionCreateTransaction: - req := &TransactionRequest{} - if err := json.Unmarshal(element.Data, req); err != nil { - return nil, 0, fmt.Errorf("error parsing element: %s", err) - } - rs, err := req.ToRunScript(false) - if err != nil { - return nil, 0, fmt.Errorf("error parsing element: %s", err) - } - - log, createTransactionResult, err := ctrl.CreateTransaction(ctx, Parameters[RunScript]{ - DryRun: false, - IdempotencyKey: element.IdempotencyKey, - Input: *rs, - }) - if err != nil { - return nil, 0, err - } - - return createTransactionResult.Transaction, log.ID, nil - case ActionAddMetadata: - req := &AddMetadataRequest{} - if err := json.Unmarshal(element.Data, req); err != nil { - return nil, 0, fmt.Errorf("error parsing element: %s", err) - } - - var ( - log *ledger.Log - err error - ) - switch req.TargetType { - case ledger.MetaTargetTypeAccount: - address := "" - if err := json.Unmarshal(req.TargetID, &address); err != nil { - return nil, 0, err - } - log, err = ctrl.SaveAccountMetadata(ctx, Parameters[SaveAccountMetadata]{ - DryRun: false, - IdempotencyKey: element.IdempotencyKey, - Input: SaveAccountMetadata{ - Address: address, - Metadata: req.Metadata, - }, - }) - case ledger.MetaTargetTypeTransaction: - transactionID := 0 - if err := json.Unmarshal(req.TargetID, &transactionID); err != nil { - return nil, 0, err - } - log, err = ctrl.SaveTransactionMetadata(ctx, Parameters[SaveTransactionMetadata]{ - DryRun: false, - IdempotencyKey: element.IdempotencyKey, - Input: SaveTransactionMetadata{ - TransactionID: transactionID, - Metadata: req.Metadata, - }, - }) - default: - return nil, 0, fmt.Errorf("invalid target type: %s", req.TargetType) - } - if err != nil { - return nil, 0, err - } - - return nil, log.ID, nil - case ActionRevertTransaction: - req := &RevertTransactionRequest{} - if err := json.Unmarshal(element.Data, req); err != nil { - return nil, 0, fmt.Errorf("error parsing element: %s", err) - } - - log, revertTransactionResult, err := ctrl.RevertTransaction(ctx, Parameters[RevertTransaction]{ - DryRun: false, - IdempotencyKey: element.IdempotencyKey, - Input: RevertTransaction{ - Force: req.Force, - AtEffectiveDate: req.AtEffectiveDate, - TransactionID: req.ID, - }, - }) - if err != nil { - return nil, 0, err - } - - return revertTransactionResult.RevertedTransaction, log.ID, nil - case ActionDeleteMetadata: - req := &DeleteMetadataRequest{} - if err := json.Unmarshal(element.Data, req); err != nil { - return nil, 0, fmt.Errorf("error parsing element: %s", err) - } - - var ( - log *ledger.Log - err error - ) - switch req.TargetType { - case ledger.MetaTargetTypeAccount: - address := "" - if err := json.Unmarshal(req.TargetID, &address); err != nil { - return nil, 0, err - } - - log, err = ctrl.DeleteAccountMetadata(ctx, Parameters[DeleteAccountMetadata]{ - DryRun: false, - IdempotencyKey: element.IdempotencyKey, - Input: DeleteAccountMetadata{ - Address: address, - Key: req.Key, - }, - }) - case ledger.MetaTargetTypeTransaction: - transactionID := 0 - if err := json.Unmarshal(req.TargetID, &transactionID); err != nil { - return nil, 0, err - } - - log, err = ctrl.DeleteTransactionMetadata(ctx, Parameters[DeleteTransactionMetadata]{ - DryRun: false, - IdempotencyKey: element.IdempotencyKey, - Input: DeleteTransactionMetadata{ - TransactionID: transactionID, - Key: req.Key, - }, - }) - default: - return nil, 0, fmt.Errorf("unsupported target type: %s", req.TargetType) - } - if err != nil { - return nil, 0, err - } - - return nil, log.ID, nil - default: - panic("unreachable") - } -} - -func NewBulker(ctrl Controller, options ...BulkerOption) *Bulker { - ret := &Bulker{ctrl: ctrl} - for _, option := range options { - option(ret) - } - - return ret -} - -type BulkerOption func(bulker *Bulker) - -func WithParallelism(v int) BulkerOption { - return func(options *Bulker) { - options.parallelism = v - } -} - -type BulkingOptions struct { - ContinueOnFailure bool - Atomic bool - Parallel bool -} - -type BulkingOption func(*BulkingOptions) - -func WithContinueOnFailure(v bool) BulkingOption { - return func(options *BulkingOptions) { - options.ContinueOnFailure = v - } -} - -func WithAtomic(v bool) BulkingOption { - return func(options *BulkingOptions) { - options.Atomic = v - } -} - -func WithParallel(v bool) BulkingOption { - return func(options *BulkingOptions) { - options.Parallel = v - } -} - -type BulkerFactory interface { - CreateBulker(ctrl Controller) *Bulker -} - -type DefaultBulkerFactory struct { - Options []BulkerOption -} - -func (d *DefaultBulkerFactory) CreateBulker(ctrl Controller) *Bulker { - return NewBulker(ctrl, d.Options...) -} - -func NewDefaultBulkerFactory(options ...BulkerOption) *DefaultBulkerFactory { - return &DefaultBulkerFactory{ - Options: options, - } -} - -var _ BulkerFactory = (*DefaultBulkerFactory)(nil) diff --git a/internal/storage/bucket/migrations/23-delete-orphan-indices/notes.yaml b/internal/storage/bucket/migrations/23-delete-orphan-indices/notes.yaml new file mode 100644 index 000000000..236c85141 --- /dev/null +++ b/internal/storage/bucket/migrations/23-delete-orphan-indices/notes.yaml @@ -0,0 +1 @@ +name: Delete duplicated indexes diff --git a/internal/storage/bucket/migrations/23-delete-orphan-indices/up.sql b/internal/storage/bucket/migrations/23-delete-orphan-indices/up.sql new file mode 100644 index 000000000..d638dd429 --- /dev/null +++ b/internal/storage/bucket/migrations/23-delete-orphan-indices/up.sql @@ -0,0 +1,2 @@ +drop index "{{ .Schema }}".accounts_address_array; +drop index "{{ .Schema }}".accounts_address_array_length; diff --git a/internal/storage/bucket/migrations/23-delete-orphan-indices/up_tests_after.sql b/internal/storage/bucket/migrations/23-delete-orphan-indices/up_tests_after.sql new file mode 100644 index 000000000..e69de29bb diff --git a/internal/storage/bucket/migrations/23-delete-orphan-indices/up_tests_before.sql b/internal/storage/bucket/migrations/23-delete-orphan-indices/up_tests_before.sql new file mode 100644 index 000000000..e69de29bb diff --git a/internal/storage/ledger/accounts.go b/internal/storage/ledger/accounts.go index 6b1972ce3..01d2d844e 100644 --- a/internal/storage/ledger/accounts.go +++ b/internal/storage/ledger/accounts.go @@ -4,7 +4,10 @@ import ( "context" "fmt" . "github.com/formancehq/go-libs/v2/bun/bunpaginate" + . "github.com/formancehq/go-libs/v2/collectionutils" "github.com/formancehq/ledger/pkg/features" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" "regexp" "github.com/formancehq/ledger/internal/tracing" @@ -281,6 +284,10 @@ func (s *Store) UpdateAccountsMetadata(ctx context.Context, m map[string]metadat s.tracer, s.updateAccountsMetadataHistogram, tracing.NoResult(func(ctx context.Context) error { + + span := trace.SpanFromContext(ctx) + span.SetAttributes(attribute.StringSlice("accounts", Keys(m))) + type AccountWithLedger struct { ledger.Account `bun:",extend"` Ledger string `bun:"ledger,type:varchar"` @@ -297,14 +304,26 @@ func (s *Store) UpdateAccountsMetadata(ctx context.Context, m map[string]metadat }) } - _, err := s.db.NewInsert(). + ret, err := s.db.NewInsert(). Model(&accounts). ModelTableExpr(s.GetPrefixedRelationName("accounts")). On("CONFLICT (ledger, address) DO UPDATE"). Set("metadata = excluded.metadata || accounts.metadata"). Where("not accounts.metadata @> excluded.metadata"). Exec(ctx) - return postgres.ResolveError(err) + + if err != nil { + return postgres.ResolveError(err) + } + + rowsAffected, err := ret.RowsAffected() + if err != nil { + return err + } + + span.SetAttributes(attribute.Int("upserted", int(rowsAffected))) + + return nil }), ) return err @@ -336,7 +355,10 @@ func (s *Store) UpsertAccounts(ctx context.Context, accounts ...*ledger.Account) s.tracer, s.upsertAccountsHistogram, tracing.NoResult(func(ctx context.Context) error { - _, err := s.db.NewInsert(). + span := trace.SpanFromContext(ctx) + span.SetAttributes(attribute.StringSlice("accounts", Map(accounts, (*ledger.Account).GetAddress))) + + ret, err := s.db.NewInsert(). Model(&accounts). ModelTableExpr(s.GetPrefixedRelationName("accounts")). On("conflict (ledger, address) do update"). @@ -351,6 +373,12 @@ func (s *Store) UpsertAccounts(ctx context.Context, accounts ...*ledger.Account) return fmt.Errorf("upserting accounts: %w", postgres.ResolveError(err)) } + rowsAffected, err := ret.RowsAffected() + if err != nil { + return err + } + span.SetAttributes(attribute.Int("upserted", int(rowsAffected))) + return nil }), )) diff --git a/pkg/generate/generator.go b/pkg/generate/generator.go index 6dbcebdfa..11c2c2f43 100644 --- a/pkg/generate/generator.go +++ b/pkg/generate/generator.go @@ -8,7 +8,7 @@ import ( "github.com/dop251/goja" "github.com/formancehq/go-libs/v2/collectionutils" ledger "github.com/formancehq/ledger/internal" - bulking "github.com/formancehq/ledger/internal/controller/ledger" + "github.com/formancehq/ledger/internal/api/bulking" "github.com/formancehq/ledger/pkg/client" "github.com/formancehq/ledger/pkg/client/models/components" "github.com/formancehq/ledger/pkg/client/models/operations" @@ -32,11 +32,7 @@ func (r Action) Apply(ctx context.Context, client *client.V2, l string) ([]compo switch element.Action { case bulking.ActionCreateTransaction: - transactionRequest := &bulking.TransactionRequest{} - err := json.Unmarshal(element.Data, transactionRequest) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal transaction request: %w", err) - } + transactionRequest := element.Data.(bulking.TransactionRequest) bulkElement = components.CreateV2BulkElementCreateTransaction(components.V2BulkElementCreateTransaction{ Data: &components.V2PostTransaction{ @@ -70,11 +66,7 @@ func (r Action) Apply(ctx context.Context, client *client.V2, l string) ([]compo }, }) case bulking.ActionAddMetadata: - addMetadataRequest := &bulking.AddMetadataRequest{} - err := json.Unmarshal(element.Data, addMetadataRequest) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal add metadata request: %w", err) - } + addMetadataRequest := element.Data.(bulking.AddMetadataRequest) var targetID components.V2TargetID switch addMetadataRequest.TargetType { @@ -102,11 +94,7 @@ func (r Action) Apply(ctx context.Context, client *client.V2, l string) ([]compo }, }) case bulking.ActionDeleteMetadata: - deleteMetadataRequest := &bulking.DeleteMetadataRequest{} - err := json.Unmarshal(element.Data, deleteMetadataRequest) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal delete metadata request: %w", err) - } + deleteMetadataRequest := element.Data.(bulking.DeleteMetadataRequest) var targetID components.V2TargetID switch deleteMetadataRequest.TargetType { @@ -134,11 +122,7 @@ func (r Action) Apply(ctx context.Context, client *client.V2, l string) ([]compo }, }) case bulking.ActionRevertTransaction: - revertMetadataRequest := &bulking.RevertTransactionRequest{} - err := json.Unmarshal(element.Data, revertMetadataRequest) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal delete metadata request: %w", err) - } + revertMetadataRequest := element.Data.(bulking.RevertTransactionRequest) bulkElement = components.CreateV2BulkElementRevertTransaction(components.V2BulkElementRevertTransaction{ Data: &components.V2BulkElementRevertTransactionData{ @@ -282,6 +266,10 @@ func NewGenerator(script string, opts ...Option) (*Generator, error) { if err != nil { return nil, err } + payload, err := bulking.UnmarshalBulkElementPayload(action, dataAsJsonRawMessage) + if err != nil { + return nil, err + } rawIK := rawElement["ik"] if rawIK != nil { @@ -294,7 +282,7 @@ func NewGenerator(script string, opts ...Option) (*Generator, error) { elements = append(elements, bulking.BulkElement{ Action: action, IdempotencyKey: ik, - Data: dataAsJsonRawMessage, + Data: payload, }) } diff --git a/test/e2e/api_bulk_test.go b/test/e2e/api_bulk_test.go index 24e3a6fb1..9b3850e1d 100644 --- a/test/e2e/api_bulk_test.go +++ b/test/e2e/api_bulk_test.go @@ -24,274 +24,257 @@ import ( var _ = Context("Ledger engine tests", func() { - for _, data := range []struct { - description string - numscriptRewrite bool - }{ - {"default", false}, - {"numscript rewrite", true}, - } { + var ( + db = UseTemplatedDatabase() + ctx = logging.TestingContext() + events chan *nats.Msg + bulkResponse []components.V2BulkElementResult + bulkMaxSize = 100 + ) - Context(data.description, func() { - var ( - db = UseTemplatedDatabase() - ctx = logging.TestingContext() - events chan *nats.Msg - bulkResponse []components.V2BulkElementResult - bulkMaxSize = 100 - ) - - testServer := NewTestServer(func() Configuration { - return Configuration{ - PostgresConfiguration: db.GetValue().ConnectionOptions(), - Output: GinkgoWriter, - Debug: debug, - NatsURL: natsServer.GetValue().ClientURL(), - BulkMaxSize: bulkMaxSize, - ExperimentalNumscriptRewrite: data.numscriptRewrite, - } - }) - BeforeEach(func() { - err := CreateLedger(ctx, testServer.GetValue(), operations.V2CreateLedgerRequest{ - Ledger: "default", - }) - Expect(err).To(BeNil()) - events = Subscribe(GinkgoT(), testServer.GetValue()) - }) - When("creating a bulk on a ledger", func() { - var ( - now = time.Now().Round(time.Microsecond).UTC() - items []components.V2BulkElement - err error - atomic, parallel bool - ) - BeforeEach(func() { - items = []components.V2BulkElement{ - components.CreateV2BulkElementCreateTransaction(components.V2BulkElementCreateTransaction{ - Data: &components.V2PostTransaction{ - Metadata: map[string]string{}, - Postings: []components.V2Posting{{ - Amount: big.NewInt(100), - Asset: "USD/2", - Destination: "bank", - Source: "world", - }}, - Timestamp: &now, - }, - }), - components.CreateV2BulkElementAddMetadata(components.V2BulkElementAddMetadata{ - Data: &components.Data{ - Metadata: metadata.Metadata{ - "foo": "bar", - "role": "admin", - }, - TargetID: components.CreateV2TargetIDBigint(big.NewInt(1)), - TargetType: components.V2TargetTypeTransaction, - }, - }), - components.CreateV2BulkElementDeleteMetadata(components.V2BulkElementDeleteMetadata{ - Data: &components.V2BulkElementDeleteMetadataData{ - Key: "foo", - TargetID: components.CreateV2TargetIDBigint(big.NewInt(1)), - TargetType: components.V2TargetTypeTransaction, - }, - }), - components.CreateV2BulkElementRevertTransaction(components.V2BulkElementRevertTransaction{ - Data: &components.V2BulkElementRevertTransactionData{ - ID: big.NewInt(1), - }, - }), - } - }) - JustBeforeEach(func() { - bulkResponse, err = CreateBulk(ctx, testServer.GetValue(), operations.V2CreateBulkRequest{ - Atomic: pointer.For(atomic), - Parallel: pointer.For(parallel), - RequestBody: items, - Ledger: "default", - }) - }) - shouldBeOk := func() { - Expect(err).To(Succeed()) - - tx, err := GetTransaction(ctx, testServer.GetValue(), operations.V2GetTransactionRequest{ - ID: big.NewInt(1), - Ledger: "default", - }) - Expect(err).To(Succeed()) - reversedTx, err := GetTransaction(ctx, testServer.GetValue(), operations.V2GetTransactionRequest{ - ID: big.NewInt(2), - Ledger: "default", - }) - Expect(err).To(Succeed()) - - Expect(*tx).To(Equal(components.V2Transaction{ - ID: big.NewInt(1), - Metadata: metadata.Metadata{ - "role": "admin", - }, + testServer := NewTestServer(func() Configuration { + return Configuration{ + PostgresConfiguration: db.GetValue().ConnectionOptions(), + Output: GinkgoWriter, + Debug: debug, + NatsURL: natsServer.GetValue().ClientURL(), + BulkMaxSize: bulkMaxSize, + } + }) + BeforeEach(func() { + err := CreateLedger(ctx, testServer.GetValue(), operations.V2CreateLedgerRequest{ + Ledger: "default", + }) + Expect(err).To(BeNil()) + events = Subscribe(GinkgoT(), testServer.GetValue()) + }) + When("creating a bulk on a ledger", func() { + var ( + now = time.Now().Round(time.Microsecond).UTC() + items []components.V2BulkElement + err error + atomic, parallel bool + ) + BeforeEach(func() { + atomic = false + parallel = false + items = []components.V2BulkElement{ + components.CreateV2BulkElementCreateTransaction(components.V2BulkElementCreateTransaction{ + Data: &components.V2PostTransaction{ + Metadata: map[string]string{}, Postings: []components.V2Posting{{ Amount: big.NewInt(100), Asset: "USD/2", Destination: "bank", Source: "world", }}, - Reverted: true, - RevertedAt: &reversedTx.Timestamp, - Timestamp: now, - InsertedAt: tx.InsertedAt, + Timestamp: &now, + }, + }), + components.CreateV2BulkElementAddMetadata(components.V2BulkElementAddMetadata{ + Data: &components.Data{ + Metadata: metadata.Metadata{ + "foo": "bar", + "role": "admin", + }, + TargetID: components.CreateV2TargetIDBigint(big.NewInt(1)), + TargetType: components.V2TargetTypeTransaction, + }, + }), + components.CreateV2BulkElementDeleteMetadata(components.V2BulkElementDeleteMetadata{ + Data: &components.V2BulkElementDeleteMetadataData{ + Key: "foo", + TargetID: components.CreateV2TargetIDBigint(big.NewInt(1)), + TargetType: components.V2TargetTypeTransaction, + }, + }), + components.CreateV2BulkElementRevertTransaction(components.V2BulkElementRevertTransaction{ + Data: &components.V2BulkElementRevertTransactionData{ + ID: big.NewInt(1), + }, + }), + } + }) + JustBeforeEach(func() { + bulkResponse, err = CreateBulk(ctx, testServer.GetValue(), operations.V2CreateBulkRequest{ + Atomic: pointer.For(atomic), + Parallel: pointer.For(parallel), + RequestBody: items, + Ledger: "default", + }) + }) + shouldBeOk := func() { + Expect(err).To(Succeed()) + + tx, err := GetTransaction(ctx, testServer.GetValue(), operations.V2GetTransactionRequest{ + ID: big.NewInt(1), + Ledger: "default", + }) + Expect(err).To(Succeed()) + reversedTx, err := GetTransaction(ctx, testServer.GetValue(), operations.V2GetTransactionRequest{ + ID: big.NewInt(2), + Ledger: "default", + }) + Expect(err).To(Succeed()) + + Expect(*tx).To(Equal(components.V2Transaction{ + ID: big.NewInt(1), + Metadata: metadata.Metadata{ + "role": "admin", + }, + Postings: []components.V2Posting{{ + Amount: big.NewInt(100), + Asset: "USD/2", + Destination: "bank", + Source: "world", + }}, + Reverted: true, + RevertedAt: &reversedTx.Timestamp, + Timestamp: now, + InsertedAt: tx.InsertedAt, + })) + By("It should send events", func() { + Eventually(events).Should(Receive(Event(ledgerevents.EventTypeCommittedTransactions))) + Eventually(events).Should(Receive(Event(ledgerevents.EventTypeSavedMetadata))) + Eventually(events).Should(Receive(Event(ledgerevents.EventTypeDeletedMetadata))) + Eventually(events).Should(Receive(Event(ledgerevents.EventTypeRevertedTransaction))) + }) + } + It("should be ok", shouldBeOk) + Context("with atomic", func() { + BeforeEach(func() { + atomic = true + }) + It("should be ok", shouldBeOk) + }) + Context("with exceeded batch size", func() { + BeforeEach(func() { + items = make([]components.V2BulkElement, 0) + for i := 0; i < bulkMaxSize+1; i++ { + items = append(items, components.CreateV2BulkElementCreateTransaction(components.V2BulkElementCreateTransaction{ + Data: &components.V2PostTransaction{ + Metadata: map[string]string{}, + Postings: []components.V2Posting{{ + Amount: big.NewInt(100), + Asset: "USD/2", + Destination: "bank", + Source: "world", + }}, + Timestamp: &now, + }, })) - By("It should send events", func() { - Eventually(events).Should(Receive(Event(ledgerevents.EventTypeCommittedTransactions))) - Eventually(events).Should(Receive(Event(ledgerevents.EventTypeSavedMetadata))) - Eventually(events).Should(Receive(Event(ledgerevents.EventTypeDeletedMetadata))) - Eventually(events).Should(Receive(Event(ledgerevents.EventTypeRevertedTransaction))) - }) } - It("should be ok", shouldBeOk) - Context("with atomic", func() { - BeforeEach(func() { - atomic = true - }) - It("should be ok", shouldBeOk) - }) - Context("with exceeded batch size", func() { - BeforeEach(func() { - items = make([]components.V2BulkElement, 0) - for i := 0; i < bulkMaxSize+1; i++ { - items = append(items, components.CreateV2BulkElementCreateTransaction(components.V2BulkElementCreateTransaction{ - Data: &components.V2PostTransaction{ - Metadata: map[string]string{}, - Postings: []components.V2Posting{{ - Amount: big.NewInt(100), - Asset: "USD/2", - Destination: "bank", - Source: "world", - }}, - Timestamp: &now, - }, - })) - } - }) - It("should respond with an error", func() { - Expect(err).To(HaveErrorCode(string(components.V2ErrorsEnumBulkSizeExceeded))) - }) - }) - Context("with parallel", func() { - BeforeEach(func() { - parallel = true - items = make([]components.V2BulkElement, 0) - for i := 0; i < bulkMaxSize; i++ { - items = append(items, components.CreateV2BulkElementCreateTransaction(components.V2BulkElementCreateTransaction{ - Data: &components.V2PostTransaction{ - Metadata: map[string]string{}, - Postings: []components.V2Posting{{ - Amount: big.NewInt(100), - Asset: "USD/2", - Destination: "bank", - Source: "world", - }}, - Timestamp: &now, - }, - })) - } - }) - It("should be ok", func() { - Expect(err).To(BeNil()) - }) - }) }) - When("creating a bulk with an error on a ledger", func() { - var ( - now = time.Now().Round(time.Microsecond).UTC() - err error - atomic bool - ) - JustBeforeEach(func() { - bulkResponse, err = CreateBulk(ctx, testServer.GetValue(), operations.V2CreateBulkRequest{ - Atomic: pointer.For(atomic), - RequestBody: []components.V2BulkElement{ - components.CreateV2BulkElementCreateTransaction(components.V2BulkElementCreateTransaction{ - Data: &components.V2PostTransaction{ - Metadata: map[string]string{}, - Postings: []components.V2Posting{{ - Amount: big.NewInt(100), - Asset: "USD/2", - Destination: "bank", - Source: "world", - }}, - Timestamp: &now, - }, - }), - components.CreateV2BulkElementCreateTransaction(components.V2BulkElementCreateTransaction{ - Data: &components.V2PostTransaction{ - Metadata: map[string]string{}, - Postings: []components.V2Posting{{ - Amount: big.NewInt(200), // Insufficient fund - Asset: "USD/2", - Destination: "user", - Source: "bank", - }}, - Timestamp: &now, - }, - }), + It("should respond with an error", func() { + Expect(err).To(HaveErrorCode(string(components.V2ErrorsEnumBulkSizeExceeded))) + }) + }) + Context("with parallel", func() { + BeforeEach(func() { + parallel = true + items = make([]components.V2BulkElement, 0) + for i := 0; i < bulkMaxSize; i++ { + items = append(items, components.CreateV2BulkElementCreateTransaction(components.V2BulkElementCreateTransaction{ + Data: &components.V2PostTransaction{ + Metadata: map[string]string{}, + Postings: []components.V2Posting{{ + Amount: big.NewInt(100), + Asset: "USD/2", + Destination: "bank", + Source: "world", + }}, + Timestamp: &now, }, - Ledger: "default", - }) - Expect(err).To(Succeed()) - }) - shouldRespondWithAnError := func() { - GinkgoHelper() - - var expectedErr string - // todo: must be fixed before switch to the new implementation - if data.numscriptRewrite { - expectedErr = "INTERPRETER_RUNTIME" - } else { - expectedErr = "INSUFFICIENT_FUND" - } - Expect(bulkResponse[1].Type).To(Equal(components.V2BulkElementResultType("ERROR"))) - Expect(bulkResponse[1].V2BulkElementResultError.ErrorCode).To(Equal(expectedErr)) + })) } - It("should respond with an error", func() { - shouldRespondWithAnError() + }) + It("should be ok", func() { + Expect(err).To(BeNil()) + }) + }) + }) + When("creating a bulk with an error on a ledger", func() { + var ( + now = time.Now().Round(time.Microsecond).UTC() + err error + atomic bool + ) + JustBeforeEach(func() { + bulkResponse, err = CreateBulk(ctx, testServer.GetValue(), operations.V2CreateBulkRequest{ + Atomic: pointer.For(atomic), + RequestBody: []components.V2BulkElement{ + components.CreateV2BulkElementCreateTransaction(components.V2BulkElementCreateTransaction{ + Data: &components.V2PostTransaction{ + Metadata: map[string]string{}, + Postings: []components.V2Posting{{ + Amount: big.NewInt(100), + Asset: "USD/2", + Destination: "bank", + Source: "world", + }}, + Timestamp: &now, + }, + }), + components.CreateV2BulkElementCreateTransaction(components.V2BulkElementCreateTransaction{ + Data: &components.V2PostTransaction{ + Metadata: map[string]string{}, + Postings: []components.V2Posting{{ + Amount: big.NewInt(200), // Insufficient fund + Asset: "USD/2", + Destination: "user", + Source: "bank", + }}, + Timestamp: &now, + }, + }), + }, + Ledger: "default", + }) + Expect(err).To(Succeed()) + }) + shouldRespondWithAnError := func() { + GinkgoHelper() - By("should have created the first item", func() { - txs, err := ListTransactions(ctx, testServer.GetValue(), operations.V2ListTransactionsRequest{ - Ledger: "default", - }) - Expect(err).To(Succeed()) - Expect(txs.Data).To(HaveLen(1)) - }) + Expect(bulkResponse[1].Type).To(Equal(components.V2BulkElementResultType("ERROR"))) + Expect(bulkResponse[1].V2BulkElementResultError.ErrorCode).To(Equal("INSUFFICIENT_FUND")) + } + It("should respond with an error", func() { + shouldRespondWithAnError() - By("Should have sent one event", func() { - Eventually(events).Should(Receive(Event(ledgerevents.EventTypeCommittedTransactions, WithPayload(bus.CommittedTransactions{ - Ledger: "default", - Transactions: []ledger.Transaction{ConvertSDKTxToCoreTX(&bulkResponse[0].V2BulkElementResultCreateTransaction.Data)}, - AccountMetadata: ledger.AccountMetadata{}, - })))) - Eventually(events).ShouldNot(Receive()) - }) + By("should have created the first item", func() { + txs, err := ListTransactions(ctx, testServer.GetValue(), operations.V2ListTransactionsRequest{ + Ledger: "default", }) - Context("with atomic", func() { - BeforeEach(func() { - atomic = true - }) - It("should not commit anything", func() { - shouldRespondWithAnError() + Expect(err).To(Succeed()) + Expect(txs.Data).To(HaveLen(1)) + }) - txs, err := ListTransactions(ctx, testServer.GetValue(), operations.V2ListTransactionsRequest{ - Ledger: "default", - }) - Expect(err).To(Succeed()) - Expect(txs.Data).To(HaveLen(0)) + By("Should have sent one event", func() { + Eventually(events).Should(Receive(Event(ledgerevents.EventTypeCommittedTransactions, WithPayload(bus.CommittedTransactions{ + Ledger: "default", + Transactions: []ledger.Transaction{ConvertSDKTxToCoreTX(&bulkResponse[0].V2BulkElementResultCreateTransaction.Data)}, + AccountMetadata: ledger.AccountMetadata{}, + })))) + Eventually(events).ShouldNot(Receive()) + }) + }) + Context("with atomic", func() { + BeforeEach(func() { + atomic = true + }) + It("should not commit anything", func() { + shouldRespondWithAnError() + + txs, err := ListTransactions(ctx, testServer.GetValue(), operations.V2ListTransactionsRequest{ + Ledger: "default", + }) + Expect(err).To(Succeed()) + Expect(txs.Data).To(HaveLen(0)) - By("Should not have sent any event", func() { - Eventually(events).ShouldNot(Receive()) - }) - }) + By("Should not have sent any event", func() { + Eventually(events).ShouldNot(Receive()) }) }) }) - } + }) }) diff --git a/test/rolling-upgrades/Earthfile b/test/rolling-upgrades/Earthfile index 7a149f1f1..9ca1e7181 100644 --- a/test/rolling-upgrades/Earthfile +++ b/test/rolling-upgrades/Earthfile @@ -61,7 +61,7 @@ cluster-create: RUN vcluster create $CLUSTER_NAME --connect=false --upgrade run: - ARG CLUSTER_NAME=test + ARG CLUSTER_NAME=test-rolling-upgrades WAIT BUILD --pass-args +cluster-create BUILD +image-test --TAG=$CLUSTER_NAME-rolling-upgrade-test diff --git a/test/rolling-upgrades/main_test.go b/test/rolling-upgrades/main_test.go index c4ba2f72f..3abfae74b 100644 --- a/test/rolling-upgrades/main_test.go +++ b/test/rolling-upgrades/main_test.go @@ -63,7 +63,7 @@ func TestK8SRollingUpgrades(t *testing.T) { auto.ConfigMap{ "version": auto.ConfigValue{Value: *latestVersion}, "postgres.uri": auto.ConfigValue{ - Value: "postgres://ledger:ledger@" + pgStackOutputs["service-name"].Value.(string) + ".svc.cluster.local:5432/ledger?sslmode=disable", + Value: "postgres://postgres:postgres@" + pgStackOutputs["service-name"].Value.(string) + ".svc.cluster.local:5432/ledger?sslmode=disable", }, "debug": auto.ConfigValue{Value: "true"}, "image.pullPolicy": auto.ConfigValue{Value: "Always"}, @@ -104,7 +104,7 @@ func TestK8SRollingUpgrades(t *testing.T) { // Let a moment ensure the test image is actually sending requests // We could maybe find a dynamic way to do that - <-time.After(5 * time.Second) + <-time.After(10 * time.Second) err = ledgerStack.SetConfig(ctx, "version", auto.ConfigValue{ Value: *actualVersion, @@ -253,15 +253,14 @@ func deployPostgres(ctx *pulumi.Context) error { Namespace: pulumi.String(namespace), Values: pulumi.Map(map[string]pulumi.Input{ "auth": pulumi.Map{ - "password": pulumi.String("ledger"), - "username": pulumi.String("ledger"), - "database": pulumi.String("ledger"), + "postgresPassword": pulumi.String("postgres"), + "database": pulumi.String("ledger"), }, "primary": pulumi.Map{ "resources": pulumi.Map{ "requests": pulumi.Map{ "memory": pulumi.String("256Mi"), - "cpu": pulumi.String("250m"), + "cpu": pulumi.String("256m"), }, }, }, diff --git a/tools/generator/go.mod b/tools/generator/go.mod index 5467b81c9..942212610 100644 --- a/tools/generator/go.mod +++ b/tools/generator/go.mod @@ -33,6 +33,7 @@ require ( github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731 // indirect github.com/fatih/color v1.18.0 // indirect github.com/formancehq/numscript v0.0.9-0.20241009144012-1150c14a1417 // indirect + github.com/go-chi/chi/v5 v5.1.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect @@ -70,6 +71,8 @@ require ( go.opentelemetry.io/otel/log v0.7.0 // indirect go.opentelemetry.io/otel/metric v1.32.0 // indirect go.opentelemetry.io/otel/trace v1.32.0 // indirect + go.uber.org/dig v1.18.0 // indirect + go.uber.org/fx v1.23.0 // indirect go.uber.org/mock v0.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect From 90cb10b93a4f672815eba291be6149d9bee8fa12 Mon Sep 17 00:00:00 2001 From: Ragot Geoffrey Date: Wed, 27 Nov 2024 15:17:47 +0100 Subject: [PATCH 46/71] chore: update dependencies (#592) --- Earthfile | 2 +- deployments/helm/Earthfile | 2 +- deployments/pulumi/Earthfile | 2 +- go.mod | 42 +++++------ go.sum | 82 +++++++++++----------- internal/storage/bucket/migrations_test.go | 10 +++ test/rolling-upgrades/Earthfile | 2 +- test/rolling-upgrades/go.mod | 22 +++--- test/rolling-upgrades/go.sum | 38 +++++----- tools/generator/Earthfile | 2 +- tools/generator/go.mod | 22 +++--- tools/generator/go.sum | 80 ++++++++++----------- 12 files changed, 160 insertions(+), 146 deletions(-) diff --git a/Earthfile b/Earthfile index ea6d1b265..6f841ce8e 100644 --- a/Earthfile +++ b/Earthfile @@ -1,7 +1,7 @@ VERSION --wildcard-builds 0.8 PROJECT FormanceHQ/ledger -IMPORT github.com/formancehq/earthly:tags/v0.17.1 AS core +IMPORT github.com/formancehq/earthly:tags/v0.19.0 AS core FROM core+base-image diff --git a/deployments/helm/Earthfile b/deployments/helm/Earthfile index f5ce08d71..45622eeb5 100644 --- a/deployments/helm/Earthfile +++ b/deployments/helm/Earthfile @@ -1,7 +1,7 @@ VERSION 0.8 PROJECT FormanceHQ/ledger -IMPORT github.com/formancehq/earthly:tags/v0.17.1 AS core +IMPORT github.com/formancehq/earthly:tags/v0.19.0 AS core FROM core+base-image diff --git a/deployments/pulumi/Earthfile b/deployments/pulumi/Earthfile index 9e19d0cb9..1a0379c50 100644 --- a/deployments/pulumi/Earthfile +++ b/deployments/pulumi/Earthfile @@ -1,7 +1,7 @@ VERSION 0.8 PROJECT FormanceHQ/ledger -IMPORT github.com/formancehq/earthly:tags/v0.17.1 AS core +IMPORT github.com/formancehq/earthly:tags/v0.19.0 AS core FROM core+base-image diff --git a/go.mod b/go.mod index af672e6af..ae74083e8 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/formancehq/ledger -go 1.22.1 +go 1.23 toolchain go1.23.2 @@ -33,9 +33,9 @@ require ( github.com/shomali11/xsql v0.0.0-20190608141458-bf76292144df github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.9.0 - github.com/uptrace/bun v1.2.5 - github.com/uptrace/bun/dialect/pgdialect v1.2.5 + github.com/stretchr/testify v1.10.0 + github.com/uptrace/bun v1.2.6 + github.com/uptrace/bun/dialect/pgdialect v1.2.6 github.com/uptrace/bun/extra/bundebug v1.2.5 github.com/xeipuuv/gojsonschema v1.2.0 github.com/xo/dburl v0.23.2 @@ -45,7 +45,7 @@ require ( go.opentelemetry.io/otel/trace v1.32.0 go.uber.org/fx v1.23.0 go.uber.org/mock v0.5.0 - golang.org/x/oauth2 v0.23.0 + golang.org/x/oauth2 v0.24.0 golang.org/x/sync v0.9.0 ) @@ -53,7 +53,7 @@ require gopkg.in/yaml.v3 v3.0.1 // indirect require ( github.com/hashicorp/go-hclog v1.6.3 // indirect - github.com/jackc/pgxlisten v0.0.0-20241005155529-9d952acd6a6c // indirect + github.com/jackc/pgxlisten v0.0.0-20241106001234-1d6f6656415c // indirect ) require ( @@ -73,7 +73,7 @@ require ( github.com/aws/aws-sdk-go-v2/config v1.28.5 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.17.46 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 // indirect - github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.23 // indirect + github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.24 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect @@ -101,7 +101,7 @@ require ( github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731 // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/formancehq/numscript v0.0.9-0.20241009144012-1150c14a1417 + github.com/formancehq/numscript v0.0.9 github.com/go-chi/chi v4.1.2+incompatible // indirect github.com/go-chi/render v1.0.3 // indirect github.com/go-logr/logr v1.4.2 // indirect @@ -118,7 +118,7 @@ require ( github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/schema v1.4.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -147,7 +147,7 @@ require ( github.com/muhlemmer/httpforwarded v0.1.0 // indirect github.com/nats-io/jwt/v2 v2.7.0 // indirect github.com/nats-io/nats-server/v2 v2.10.22 // indirect - github.com/nats-io/nkeys v0.4.7 // indirect + github.com/nats-io/nkeys v0.4.8 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect @@ -166,13 +166,13 @@ require ( github.com/tklauser/go-sysconf v0.3.14 // indirect github.com/tklauser/numcpus v0.9.0 // indirect github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect - github.com/uptrace/bun/extra/bunotel v1.2.5 // indirect + github.com/uptrace/bun/extra/bunotel v1.2.6 // indirect github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 // indirect github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240816141633-0a40785b4f41 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect @@ -191,23 +191,23 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 // indirect - go.opentelemetry.io/otel/log v0.7.0 // indirect + go.opentelemetry.io/otel/log v0.8.0 // indirect go.opentelemetry.io/otel/sdk v1.32.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/dig v1.18.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.28.0 // indirect - golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect - golang.org/x/net v0.30.0 // indirect + golang.org/x/crypto v0.29.0 // indirect + golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect + golang.org/x/net v0.31.0 // indirect golang.org/x/sys v0.27.0 // indirect golang.org/x/text v0.20.0 // indirect golang.org/x/time v0.7.0 // indirect - golang.org/x/tools v0.26.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 // indirect - google.golang.org/grpc v1.67.1 // indirect - google.golang.org/protobuf v1.35.1 // indirect + golang.org/x/tools v0.27.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 // indirect + google.golang.org/grpc v1.68.0 // indirect + google.golang.org/protobuf v1.35.2 // indirect gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 09b86c360..b526814ed 100644 --- a/go.sum +++ b/go.sum @@ -38,8 +38,8 @@ github.com/aws/aws-sdk-go-v2/credentials v1.17.46 h1:AU7RcriIo2lXjUfHFnFKYsLCwgb github.com/aws/aws-sdk-go-v2/credentials v1.17.46/go.mod h1:1FmYyLGL08KQXQ6mcTlifyFXfJVCNJTVGuQP4m0d/UA= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 h1:sDSXIrlsFSFJtWKLQS4PUWRvrT580rrnuLydJrCQ/yA= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20/go.mod h1:WZ/c+w0ofps+/OUqMwWgnfrgzZH1DZO1RIkktICsqnY= -github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.23 h1:B2qK61ZXCQu8tkD6eG/gUiIt9Vw9tmWFD7Xo02JPdMY= -github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.23/go.mod h1:02rz9vMZsrOX9IwUcpoGZM4jPprFNPmtD6t9Ume9ECY= +github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.24 h1:HfLyPCysN3MqXSQIP83f/0fNTvb8ELXBv76Jaa3LvCs= +github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.24/go.mod h1:WNDtzVHjS5Ct1HJLcVaclQivrWvK3lQWmQkaT7tzr4M= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 h1:4usbeaes3yJnCFC7kfeyhkdkPtoRYPa/hTmCqMpKpLI= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24/go.mod h1:5CI1JemjVwde8m2WG3cz23qHKPOxbpkq0HaoreEgLIY= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 h1:N1zsICrQglfzaBnrfM0Ys00860C+QFwu6u/5+LomP+o= @@ -106,8 +106,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/formancehq/go-libs/v2 v2.0.1-0.20241121194732-b79c48b683f2 h1:JsacSDe6MYGCm04ZY/VJyYTUAWg8jhOBZm6AGyErF/I= github.com/formancehq/go-libs/v2 v2.0.1-0.20241121194732-b79c48b683f2/go.mod h1:ZGHVcAC54ZbPgeoGKy/d31CHy94RMhHQlX+Vui15ST8= -github.com/formancehq/numscript v0.0.9-0.20241009144012-1150c14a1417 h1:LOd5hxnXDIBcehFrpW1OnXk+VSs0yJXeu1iAOO+Hji4= -github.com/formancehq/numscript v0.0.9-0.20241009144012-1150c14a1417/go.mod h1:btuSv05cYwi9BvLRxVs5zrunU+O1vTgigG1T6UsawcY= +github.com/formancehq/numscript v0.0.9 h1:TJxA0dEmVSL0qA04WApgsrs/GDfwttieQkaIe5nd2Ao= +github.com/formancehq/numscript v0.0.9/go.mod h1:btuSv05cYwi9BvLRxVs5zrunU+O1vTgigG1T6UsawcY= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/gkampitakis/ciinfo v0.3.0 h1:gWZlOC2+RYYttL0hBqcoQhM7h1qNkVqvRCV1fOvpAv8= @@ -144,6 +144,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -166,8 +168,8 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 h1:ad0vkEBuk23VJzZR9nkLVG0YAoN9coASF1GusYX6AlU= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0/go.mod h1:igFoXX2ELCW06bol23DWPB5BEWfZISOzSP5K2sbLea0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -192,8 +194,8 @@ github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7Ulw github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= -github.com/jackc/pgxlisten v0.0.0-20241005155529-9d952acd6a6c h1:/u9tWJZ5d+RnlpVuvf352pGb+CzTrJP+r+ETy4JEHyo= -github.com/jackc/pgxlisten v0.0.0-20241005155529-9d952acd6a6c/go.mod h1:EqjCOzkITPCEI0My7BdE2xm3r0fZ7OZycVDP+ki1ASA= +github.com/jackc/pgxlisten v0.0.0-20241106001234-1d6f6656415c h1:bTgmg761ac9Ki27HoLx8IBvc+T+Qj6eptBpKahKIRT4= +github.com/jackc/pgxlisten v0.0.0-20241106001234-1d6f6656415c/go.mod h1:N4E1APLOYrbM11HH5kdqAjDa8RJWVwD3JqWpvH22h64= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jamiealquiza/tachymeter v2.0.0+incompatible h1:mGiF1DGo8l6vnGT8FXNNcIXht/YmjzfraiUprXYwJ6g= @@ -260,8 +262,8 @@ github.com/nats-io/nats-server/v2 v2.10.22 h1:Yt63BGu2c3DdMoBZNcR6pjGQwk/asrKU7V github.com/nats-io/nats-server/v2 v2.10.22/go.mod h1:X/m1ye9NYansUXYFrbcDwUi/blHkrgHh2rgCJaakonk= github.com/nats-io/nats.go v1.37.0 h1:07rauXbVnnJvv1gfIyghFEo6lUcYRY0WXc3x7x0vUxE= github.com/nats-io/nats.go v1.37.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= -github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= -github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= +github.com/nats-io/nkeys v0.4.8 h1:+wee30071y3vCZAYRsnrmIPaOe47A/SkK/UBDPdIV70= +github.com/nats-io/nkeys v0.4.8/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= @@ -325,8 +327,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -341,14 +343,14 @@ github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPD github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI= github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= -github.com/uptrace/bun v1.2.5 h1:gSprL5xiBCp+tzcZHgENzJpXnmQwRM/A6s4HnBF85mc= -github.com/uptrace/bun v1.2.5/go.mod h1:vkQMS4NNs4VNZv92y53uBSHXRqYyJp4bGhMHgaNCQpY= -github.com/uptrace/bun/dialect/pgdialect v1.2.5 h1:dWLUxpjTdglzfBks2x+U2WIi+nRVjuh7Z3DLYVFswJk= -github.com/uptrace/bun/dialect/pgdialect v1.2.5/go.mod h1:stwnlE8/6x8cuQ2aXcZqwDK/d+6jxgO3iQewflJT6C4= +github.com/uptrace/bun v1.2.6 h1:lyGBQAhNiClchb97HA2cBnDeRxwTRLhSIgiFPXVisV8= +github.com/uptrace/bun v1.2.6/go.mod h1:xMgnVFf+/5xsrFBU34HjDJmzZnXbVuNEt/Ih56I8qBU= +github.com/uptrace/bun/dialect/pgdialect v1.2.6 h1:iNd1YLx619K+sZK+dRcWPzluurXYK1QwIkp9FEfNB/8= +github.com/uptrace/bun/dialect/pgdialect v1.2.6/go.mod h1:OL7d3qZLxKYP8kxNhMg3IheN1pDR3UScGjoUP+ivxJQ= github.com/uptrace/bun/extra/bundebug v1.2.5 h1:DsI/gl4jvq5tQ84yPqnlRYIQ4U6AouTqxJ8Y5Oijfz4= github.com/uptrace/bun/extra/bundebug v1.2.5/go.mod h1:JFeYvklf5p92ZILXx4siMe2tEVn5JLelww3IGcJb4yA= -github.com/uptrace/bun/extra/bunotel v1.2.5 h1:kkuuTbrG9d5leYZuSBKhq2gtq346lIrxf98Mig2y128= -github.com/uptrace/bun/extra/bunotel v1.2.5/go.mod h1:rCHLszRZwppWE9cGDodO2FCI1qCrLwDjONp38KD3bA8= +github.com/uptrace/bun/extra/bunotel v1.2.6 h1:6m90acv9hsDuTYRo3oiKCWMatGPmi+feKAx8Y/GPj9A= +github.com/uptrace/bun/extra/bunotel v1.2.6/go.mod h1:QGqnFNJ2H88juh7DmgdPJZVN9bSTpj7UaGllSO9JDKk= github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 h1:H8wwQwTe5sL6x30z71lUgNiwBdeCHQjrphCfLwqIHGo= github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2/go.mod h1:/kR4beFhlz2g+V5ik8jW+3PMiMQAPt29y6K64NNY53c= github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 h1:ZjUj9BLYf9PEqBn8W/OapxhPjVRdC6CsXTdULHsyk5c= @@ -359,8 +361,8 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= -github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240816141633-0a40785b4f41 h1:rnB8ZLMeAr3VcqjfRkAm27qb8y6zFKNfuHvy1Gfe7KI= +github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240816141633-0a40785b4f41/go.mod h1:DbzwytT4g/odXquuOCqroKvtxxldI4nb3nuesHF/Exo= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= @@ -407,8 +409,8 @@ go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 h1:SZmDnHcgp3zwlP go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0/go.mod h1:fdWW0HtZJ7+jNpTKUR0GpMEDP69nR8YBJQxNiVCE3jk= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s= -go.opentelemetry.io/otel/log v0.7.0 h1:d1abJc0b1QQZADKvfe9JqqrfmPYQCz2tUSO+0XZmuV4= -go.opentelemetry.io/otel/log v0.7.0/go.mod h1:2jf2z7uVfnzDNknKTO9G+ahcOAyWcp1fJmk/wJjULRo= +go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk= +go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8= go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= @@ -436,10 +438,10 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= -golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= -golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= +golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -452,10 +454,10 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= -golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= -golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -502,20 +504,20 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= +golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 h1:M0KvPgPmDZHPlbRbaNU1APr28TvwvvdUPlSv7PUvy8g= -google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:dguCy7UOdZhTvLzDyt15+rOrawrpM4q7DD9dQ1P11P4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 h1:XVhgTWWV3kGQlwJHR3upFWZeTsei6Oks1apkZSeonIE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= -google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= -google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 h1:pgr/4QbFyktUv9CtQ/Fq4gzEE6/Xs7iCXbktaGzLHbQ= +google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go.mod h1:+D9ySVjN8nY8YCVjc5O7PZDIdZporIDY3KaGfJunh88= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= +google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= +google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/internal/storage/bucket/migrations_test.go b/internal/storage/bucket/migrations_test.go index 8d787ac98..92d68c5be 100644 --- a/internal/storage/bucket/migrations_test.go +++ b/internal/storage/bucket/migrations_test.go @@ -3,9 +3,11 @@ package bucket_test import ( + "fmt" "github.com/formancehq/go-libs/v2/bun/bunconnect" "github.com/formancehq/go-libs/v2/logging" "github.com/formancehq/go-libs/v2/migrations" + ledger "github.com/formancehq/ledger/internal" "github.com/formancehq/ledger/internal/storage/bucket" "github.com/formancehq/ledger/internal/storage/system" "github.com/google/uuid" @@ -31,6 +33,14 @@ func TestMigrations(t *testing.T) { bucketName := uuid.NewString()[:8] migrator := bucket.GetMigrator(db, bucketName) + for i := 0; i < 5; i++ { + l, err := ledger.New(fmt.Sprintf("ledger%d", i), ledger.Configuration{ + Bucket: bucketName, + }) + require.NoError(t, err) + require.NoError(t, system.New(db).CreateLedger(ctx, l)) + } + err = migrations.TestMigrations(ctx, bucket.MigrationsFS, migrator) require.NoError(t, err) } diff --git a/test/rolling-upgrades/Earthfile b/test/rolling-upgrades/Earthfile index 9ca1e7181..d7c87f0de 100644 --- a/test/rolling-upgrades/Earthfile +++ b/test/rolling-upgrades/Earthfile @@ -1,7 +1,7 @@ VERSION 0.8 PROJECT FormanceHQ/ledger -IMPORT github.com/formancehq/earthly:tags/v0.17.1 AS core +IMPORT github.com/formancehq/earthly:tags/v0.19.0 AS core FROM core+base-image diff --git a/test/rolling-upgrades/go.mod b/test/rolling-upgrades/go.mod index e6fe130ca..548662974 100644 --- a/test/rolling-upgrades/go.mod +++ b/test/rolling-upgrades/go.mod @@ -1,8 +1,8 @@ module github.com/formancehq/ledger/test/rolling-upgrades -go 1.22.1 +go 1.23 -toolchain go1.23.2 +toolchain go1.23.3 replace github.com/formancehq/ledger/pkg/client => ../../pkg/client @@ -13,7 +13,7 @@ require ( github.com/formancehq/ledger v0.0.0-00010101000000-000000000000 github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.12.0 github.com/pulumi/pulumi/sdk/v3 v3.117.0 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 ) require ( @@ -98,22 +98,22 @@ require ( github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/zclconf/go-cty v1.13.2 // indirect go.opentelemetry.io/otel v1.32.0 // indirect - go.opentelemetry.io/otel/log v0.7.0 // indirect + go.opentelemetry.io/otel/log v0.8.0 // indirect go.opentelemetry.io/otel/metric v1.32.0 // indirect go.opentelemetry.io/otel/trace v1.32.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.28.0 // indirect - golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect - golang.org/x/net v0.30.0 // indirect + golang.org/x/crypto v0.29.0 // indirect + golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect + golang.org/x/net v0.31.0 // indirect golang.org/x/sync v0.9.0 // indirect golang.org/x/sys v0.27.0 // indirect - golang.org/x/term v0.25.0 // indirect + golang.org/x/term v0.26.0 // indirect golang.org/x/text v0.20.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 // indirect - google.golang.org/grpc v1.67.1 // indirect - google.golang.org/protobuf v1.35.1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 // indirect + google.golang.org/grpc v1.68.0 // indirect + google.golang.org/protobuf v1.35.2 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/test/rolling-upgrades/go.sum b/test/rolling-upgrades/go.sum index 189d81373..9d7b0bd69 100644 --- a/test/rolling-upgrades/go.sum +++ b/test/rolling-upgrades/go.sum @@ -82,6 +82,8 @@ github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY= github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -212,8 +214,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/texttheater/golang-levenshtein v1.0.1 h1:+cRNoVrfiwufQPhoMzB6N0Yf/Mqajr6t1lOv8GyGE2U= github.com/texttheater/golang-levenshtein v1.0.1/go.mod h1:PYAKrbF5sAiq9wd+H82hs7gNaen0CplQ9uvm6+enD/8= github.com/tweekmonster/luser v0.0.0-20161003172636-3fa38070dbd7 h1:X9dsIWPuuEJlPX//UmRKophhOKCGXc46RVIGuttks68= @@ -235,8 +237,8 @@ github.com/zclconf/go-cty v1.13.2 h1:4GvrUxe/QUDYuJKAav4EYqdM47/kZa672LwmXFmEKT0 github.com/zclconf/go-cty v1.13.2/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= -go.opentelemetry.io/otel/log v0.7.0 h1:d1abJc0b1QQZADKvfe9JqqrfmPYQCz2tUSO+0XZmuV4= -go.opentelemetry.io/otel/log v0.7.0/go.mod h1:2jf2z7uVfnzDNknKTO9G+ahcOAyWcp1fJmk/wJjULRo= +go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk= +go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8= go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= @@ -258,10 +260,10 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= -golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= -golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= +golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -279,8 +281,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -322,8 +324,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= -golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= +golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -345,12 +347,12 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 h1:XVhgTWWV3kGQlwJHR3upFWZeTsei6Oks1apkZSeonIE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= -google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= -google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= +google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= +google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/tools/generator/Earthfile b/tools/generator/Earthfile index 6690189e5..974a4983b 100644 --- a/tools/generator/Earthfile +++ b/tools/generator/Earthfile @@ -1,7 +1,7 @@ VERSION 0.8 PROJECT FormanceHQ/ledger -IMPORT github.com/formancehq/earthly:tags/v0.17.1 AS core +IMPORT github.com/formancehq/earthly:tags/v0.19.0 AS core FROM core+base-image diff --git a/tools/generator/go.mod b/tools/generator/go.mod index 942212610..cf92d0371 100644 --- a/tools/generator/go.mod +++ b/tools/generator/go.mod @@ -1,8 +1,8 @@ module github.com/formancehq/ledger/tools/generator -go 1.22.1 +go 1.23 -toolchain go1.23.2 +toolchain go1.23.3 replace github.com/formancehq/ledger => ../.. @@ -13,8 +13,8 @@ require ( github.com/formancehq/ledger v0.0.0-00010101000000-000000000000 github.com/formancehq/ledger/pkg/client v0.0.0-00010101000000-000000000000 github.com/spf13/cobra v1.8.1 - github.com/stretchr/testify v1.9.0 - golang.org/x/oauth2 v0.23.0 + github.com/stretchr/testify v1.10.0 + golang.org/x/oauth2 v0.24.0 ) require ( @@ -32,7 +32,7 @@ require ( github.com/dop251/goja v0.0.0-20241009100908-5f46f2705ca3 // indirect github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731 // indirect github.com/fatih/color v1.18.0 // indirect - github.com/formancehq/numscript v0.0.9-0.20241009144012-1150c14a1417 // indirect + github.com/formancehq/numscript v0.0.9 // indirect github.com/go-chi/chi/v5 v5.1.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -45,7 +45,7 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.7.1 // indirect - github.com/jackc/pgxlisten v0.0.0-20241005155529-9d952acd6a6c // indirect + github.com/jackc/pgxlisten v0.0.0-20241106001234-1d6f6656415c // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/lithammer/shortuuid/v3 v3.0.7 // indirect @@ -61,14 +61,14 @@ require ( github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect - github.com/uptrace/bun v1.2.5 // indirect + github.com/uptrace/bun v1.2.6 // indirect github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 // indirect github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240816141633-0a40785b4f41 // indirect go.opentelemetry.io/otel v1.32.0 // indirect - go.opentelemetry.io/otel/log v0.7.0 // indirect + go.opentelemetry.io/otel/log v0.8.0 // indirect go.opentelemetry.io/otel/metric v1.32.0 // indirect go.opentelemetry.io/otel/trace v1.32.0 // indirect go.uber.org/dig v1.18.0 // indirect @@ -76,8 +76,8 @@ require ( go.uber.org/mock v0.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.28.0 // indirect - golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect + golang.org/x/crypto v0.29.0 // indirect + golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect golang.org/x/sync v0.9.0 // indirect golang.org/x/sys v0.27.0 // indirect golang.org/x/text v0.20.0 // indirect diff --git a/tools/generator/go.sum b/tools/generator/go.sum index ea6126fb6..5241a1b9e 100644 --- a/tools/generator/go.sum +++ b/tools/generator/go.sum @@ -38,8 +38,8 @@ github.com/aws/aws-sdk-go-v2/credentials v1.17.46 h1:AU7RcriIo2lXjUfHFnFKYsLCwgb github.com/aws/aws-sdk-go-v2/credentials v1.17.46/go.mod h1:1FmYyLGL08KQXQ6mcTlifyFXfJVCNJTVGuQP4m0d/UA= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 h1:sDSXIrlsFSFJtWKLQS4PUWRvrT580rrnuLydJrCQ/yA= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20/go.mod h1:WZ/c+w0ofps+/OUqMwWgnfrgzZH1DZO1RIkktICsqnY= -github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.23 h1:B2qK61ZXCQu8tkD6eG/gUiIt9Vw9tmWFD7Xo02JPdMY= -github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.23/go.mod h1:02rz9vMZsrOX9IwUcpoGZM4jPprFNPmtD6t9Ume9ECY= +github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.24 h1:HfLyPCysN3MqXSQIP83f/0fNTvb8ELXBv76Jaa3LvCs= +github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.24/go.mod h1:WNDtzVHjS5Ct1HJLcVaclQivrWvK3lQWmQkaT7tzr4M= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 h1:4usbeaes3yJnCFC7kfeyhkdkPtoRYPa/hTmCqMpKpLI= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24/go.mod h1:5CI1JemjVwde8m2WG3cz23qHKPOxbpkq0HaoreEgLIY= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 h1:N1zsICrQglfzaBnrfM0Ys00860C+QFwu6u/5+LomP+o= @@ -104,8 +104,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/formancehq/go-libs/v2 v2.0.1-0.20241121194732-b79c48b683f2 h1:JsacSDe6MYGCm04ZY/VJyYTUAWg8jhOBZm6AGyErF/I= github.com/formancehq/go-libs/v2 v2.0.1-0.20241121194732-b79c48b683f2/go.mod h1:ZGHVcAC54ZbPgeoGKy/d31CHy94RMhHQlX+Vui15ST8= -github.com/formancehq/numscript v0.0.9-0.20241009144012-1150c14a1417 h1:LOd5hxnXDIBcehFrpW1OnXk+VSs0yJXeu1iAOO+Hji4= -github.com/formancehq/numscript v0.0.9-0.20241009144012-1150c14a1417/go.mod h1:btuSv05cYwi9BvLRxVs5zrunU+O1vTgigG1T6UsawcY= +github.com/formancehq/numscript v0.0.9 h1:TJxA0dEmVSL0qA04WApgsrs/GDfwttieQkaIe5nd2Ao= +github.com/formancehq/numscript v0.0.9/go.mod h1:btuSv05cYwi9BvLRxVs5zrunU+O1vTgigG1T6UsawcY= github.com/gkampitakis/ciinfo v0.3.0 h1:gWZlOC2+RYYttL0hBqcoQhM7h1qNkVqvRCV1fOvpAv8= github.com/gkampitakis/ciinfo v0.3.0/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= @@ -154,8 +154,8 @@ github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 h1:ad0vkEBuk23VJzZR9nkLVG0YAoN9coASF1GusYX6AlU= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0/go.mod h1:igFoXX2ELCW06bol23DWPB5BEWfZISOzSP5K2sbLea0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= @@ -178,8 +178,8 @@ github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7Ulw github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= -github.com/jackc/pgxlisten v0.0.0-20241005155529-9d952acd6a6c h1:/u9tWJZ5d+RnlpVuvf352pGb+CzTrJP+r+ETy4JEHyo= -github.com/jackc/pgxlisten v0.0.0-20241005155529-9d952acd6a6c/go.mod h1:EqjCOzkITPCEI0My7BdE2xm3r0fZ7OZycVDP+ki1ASA= +github.com/jackc/pgxlisten v0.0.0-20241106001234-1d6f6656415c h1:bTgmg761ac9Ki27HoLx8IBvc+T+Qj6eptBpKahKIRT4= +github.com/jackc/pgxlisten v0.0.0-20241106001234-1d6f6656415c/go.mod h1:N4E1APLOYrbM11HH5kdqAjDa8RJWVwD3JqWpvH22h64= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= @@ -230,8 +230,8 @@ github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/ github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0= github.com/nats-io/nats.go v1.37.0 h1:07rauXbVnnJvv1gfIyghFEo6lUcYRY0WXc3x7x0vUxE= github.com/nats-io/nats.go v1.37.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= -github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= -github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= +github.com/nats-io/nkeys v0.4.8 h1:+wee30071y3vCZAYRsnrmIPaOe47A/SkK/UBDPdIV70= +github.com/nats-io/nkeys v0.4.8/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= @@ -284,8 +284,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -300,14 +300,14 @@ github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPD github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI= github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= -github.com/uptrace/bun v1.2.5 h1:gSprL5xiBCp+tzcZHgENzJpXnmQwRM/A6s4HnBF85mc= -github.com/uptrace/bun v1.2.5/go.mod h1:vkQMS4NNs4VNZv92y53uBSHXRqYyJp4bGhMHgaNCQpY= -github.com/uptrace/bun/dialect/pgdialect v1.2.5 h1:dWLUxpjTdglzfBks2x+U2WIi+nRVjuh7Z3DLYVFswJk= -github.com/uptrace/bun/dialect/pgdialect v1.2.5/go.mod h1:stwnlE8/6x8cuQ2aXcZqwDK/d+6jxgO3iQewflJT6C4= +github.com/uptrace/bun v1.2.6 h1:lyGBQAhNiClchb97HA2cBnDeRxwTRLhSIgiFPXVisV8= +github.com/uptrace/bun v1.2.6/go.mod h1:xMgnVFf+/5xsrFBU34HjDJmzZnXbVuNEt/Ih56I8qBU= +github.com/uptrace/bun/dialect/pgdialect v1.2.6 h1:iNd1YLx619K+sZK+dRcWPzluurXYK1QwIkp9FEfNB/8= +github.com/uptrace/bun/dialect/pgdialect v1.2.6/go.mod h1:OL7d3qZLxKYP8kxNhMg3IheN1pDR3UScGjoUP+ivxJQ= github.com/uptrace/bun/extra/bundebug v1.2.5 h1:DsI/gl4jvq5tQ84yPqnlRYIQ4U6AouTqxJ8Y5Oijfz4= github.com/uptrace/bun/extra/bundebug v1.2.5/go.mod h1:JFeYvklf5p92ZILXx4siMe2tEVn5JLelww3IGcJb4yA= -github.com/uptrace/bun/extra/bunotel v1.2.5 h1:kkuuTbrG9d5leYZuSBKhq2gtq346lIrxf98Mig2y128= -github.com/uptrace/bun/extra/bunotel v1.2.5/go.mod h1:rCHLszRZwppWE9cGDodO2FCI1qCrLwDjONp38KD3bA8= +github.com/uptrace/bun/extra/bunotel v1.2.6 h1:6m90acv9hsDuTYRo3oiKCWMatGPmi+feKAx8Y/GPj9A= +github.com/uptrace/bun/extra/bunotel v1.2.6/go.mod h1:QGqnFNJ2H88juh7DmgdPJZVN9bSTpj7UaGllSO9JDKk= github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 h1:H8wwQwTe5sL6x30z71lUgNiwBdeCHQjrphCfLwqIHGo= github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2/go.mod h1:/kR4beFhlz2g+V5ik8jW+3PMiMQAPt29y6K64NNY53c= github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 h1:ZjUj9BLYf9PEqBn8W/OapxhPjVRdC6CsXTdULHsyk5c= @@ -318,8 +318,8 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= -github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240816141633-0a40785b4f41 h1:rnB8ZLMeAr3VcqjfRkAm27qb8y6zFKNfuHvy1Gfe7KI= +github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240816141633-0a40785b4f41/go.mod h1:DbzwytT4g/odXquuOCqroKvtxxldI4nb3nuesHF/Exo= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= @@ -362,8 +362,8 @@ go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 h1:SZmDnHcgp3zwlP go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0/go.mod h1:fdWW0HtZJ7+jNpTKUR0GpMEDP69nR8YBJQxNiVCE3jk= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s= -go.opentelemetry.io/otel/log v0.7.0 h1:d1abJc0b1QQZADKvfe9JqqrfmPYQCz2tUSO+0XZmuV4= -go.opentelemetry.io/otel/log v0.7.0/go.mod h1:2jf2z7uVfnzDNknKTO9G+ahcOAyWcp1fJmk/wJjULRo= +go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk= +go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8= go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= @@ -386,14 +386,14 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= -golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= -golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= -golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= -golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= +golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -408,16 +408,16 @@ golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= -golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= -google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 h1:M0KvPgPmDZHPlbRbaNU1APr28TvwvvdUPlSv7PUvy8g= -google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:dguCy7UOdZhTvLzDyt15+rOrawrpM4q7DD9dQ1P11P4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 h1:XVhgTWWV3kGQlwJHR3upFWZeTsei6Oks1apkZSeonIE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= -google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= -google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= +golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= +google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 h1:pgr/4QbFyktUv9CtQ/Fq4gzEE6/Xs7iCXbktaGzLHbQ= +google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go.mod h1:+D9ySVjN8nY8YCVjc5O7PZDIdZporIDY3KaGfJunh88= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= +google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= +google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From 0c390583244150751fe6ee0c7d88b57c4c4781c1 Mon Sep 17 00:00:00 2001 From: Ragot Geoffrey Date: Thu, 5 Dec 2024 13:49:25 +0100 Subject: [PATCH 47/71] fix: account encoding at api level (#600) --- internal/account.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/account.go b/internal/account.go index 1fc877fd6..add223520 100644 --- a/internal/account.go +++ b/internal/account.go @@ -17,7 +17,7 @@ type Account struct { Address string `json:"address" bun:"address"` Metadata metadata.Metadata `json:"metadata" bun:"metadata,type:jsonb,default:'{}'"` FirstUsage time.Time `json:"-" bun:"first_usage,nullzero"` - InsertionDate time.Time `json:"_" bun:"insertion_date,nullzero"` + InsertionDate time.Time `json:"-" bun:"insertion_date,nullzero"` UpdatedAt time.Time `json:"-" bun:"updated_at,nullzero"` Volumes VolumesByAssets `json:"volumes,omitempty" bun:"volumes,scanonly"` EffectiveVolumes VolumesByAssets `json:"effectiveVolumes,omitempty" bun:"effective_volumes,scanonly"` From 69f5472f223549e7a01eecca2eef38a6b84c2fe9 Mon Sep 17 00:00:00 2001 From: Ragot Geoffrey Date: Thu, 5 Dec 2024 14:41:07 +0100 Subject: [PATCH 48/71] chore: update dependencies (#592) (#599) --- docs/api/README.md | 4 --- openapi.yaml | 32 ----------------- openapi/v2.yaml | 32 ----------------- test/e2e/api_accounts_list_test.go | 55 ------------------------------ 4 files changed, 123 deletions(-) diff --git a/docs/api/README.md b/docs/api/README.md index a4ad477c0..0ac42e9d2 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -642,7 +642,6 @@ Accept: application/json |---|---|---|---|---| |ledger|path|string|true|Name of the ledger.| |pit|query|string(date-time)|false|none| -|query|query|string|false|Query string to filter accounts. The query string must be a valid JSON object.| |body|body|object|false|none| > Example responses @@ -708,7 +707,6 @@ List accounts from a ledger, sorted by address in descending order. |cursor|query|string|false|Parameter used in pagination requests. Maximum page size is set to 15.| |expand|query|string|false|none| |pit|query|string(date-time)|false|none| -|query|query|string|false|Query string to filter accounts. The query string must be a valid JSON object.| |body|body|object|false|none| #### Detailed descriptions @@ -1056,7 +1054,6 @@ Accept: application/json |---|---|---|---|---| |ledger|path|string|true|Name of the ledger.| |pit|query|string(date-time)|false|none| -|query|query|string|false|Query string to filter transactions. The query string must be a valid JSON object.| |body|body|object|false|none| > Example responses @@ -1118,7 +1115,6 @@ List transactions from a ledger, sorted by id in descending order. |Name|In|Type|Required|Description| |---|---|---|---|---| |ledger|path|string|true|Name of the ledger.| -|query|query|string|false|Query string to filter transactions. The query string must be a valid JSON object.| |pageSize|query|integer(int64)|false|The maximum number of results to return per page.| |cursor|query|string|false|Parameter used in pagination requests. Maximum page size is set to 15.| |expand|query|string|false|none| diff --git a/openapi.yaml b/openapi.yaml index a441ab8e7..28808975b 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1507,14 +1507,6 @@ paths: schema: type: string format: date-time - - name: query - in: query - description: Query string to filter accounts. The query string must be a valid JSON object. - schema: - type: string - example: - $match: - address: users:001 requestBody: content: application/json: @@ -1586,14 +1578,6 @@ paths: schema: type: string format: date-time - - name: query - in: query - description: Query string to filter accounts. The query string must be a valid JSON object. - schema: - type: string - example: - $match: - address: users:001 requestBody: content: application/json: @@ -1823,14 +1807,6 @@ paths: schema: type: string format: date-time - - name: query - in: query - description: Query string to filter transactions. The query string must be a valid JSON object. - schema: - type: string - example: - $match: - account: users:001 requestBody: content: application/json: @@ -1870,14 +1846,6 @@ paths: schema: type: string example: ledger001 - - name: query - in: query - description: Query string to filter transactions. The query string must be a valid JSON object. - schema: - type: string - example: - $match: - account: users:001 - name: pageSize in: query description: | diff --git a/openapi/v2.yaml b/openapi/v2.yaml index c78ffe775..378675367 100644 --- a/openapi/v2.yaml +++ b/openapi/v2.yaml @@ -345,14 +345,6 @@ paths: schema: type: string format: date-time - - name: query - in: query - description: Query string to filter accounts. The query string must be a valid JSON object. - schema: - type: string - example: - "$match": - "address": "users:001" requestBody: content: application/json: @@ -428,14 +420,6 @@ paths: schema: type: string format: date-time - - name: query - in: query - description: Query string to filter accounts. The query string must be a valid JSON object. - schema: - type: string - example: - "$match": - "address": "users:001" requestBody: content: application/json: @@ -676,14 +660,6 @@ paths: schema: type: string format: date-time - - name: query - in: query - description: Query string to filter transactions. The query string must be a valid JSON object. - schema: - type: string - example: - "$match": - "account": "users:001" requestBody: content: application/json: @@ -723,14 +699,6 @@ paths: schema: type: string example: ledger001 - - name: query - in: query - description: Query string to filter transactions. The query string must be a valid JSON object. - schema: - type: string - example: - "$match": - "account": "users:001" - name: pageSize in: query description: | diff --git a/test/e2e/api_accounts_list_test.go b/test/e2e/api_accounts_list_test.go index 7f11cc0ec..f5ebd2610 100644 --- a/test/e2e/api_accounts_list_test.go +++ b/test/e2e/api_accounts_list_test.go @@ -3,7 +3,6 @@ package test_suite import ( - "encoding/json" "fmt" "github.com/formancehq/go-libs/v2/logging" . "github.com/formancehq/go-libs/v2/testing/api" @@ -210,60 +209,6 @@ var _ = Context("Ledger accounts list API tests", func() { Metadata: metadata1, })) }) - It("should be listed on api using address filters on query param", func() { - - filtersAsJSON, err := json.Marshal(map[string]interface{}{ - "$match": map[string]any{ - "address": "foo:", - }, - }) - Expect(err).To(Succeed()) - - response, err := ListAccounts( - ctx, - testServer.GetValue(), - operations.V2ListAccountsRequest{ - Ledger: "default", - Query: pointer.For(string(filtersAsJSON)), - }, - ) - Expect(err).ToNot(HaveOccurred()) - - accountsCursorResponse := response.Data - Expect(accountsCursorResponse).To(HaveLen(2)) - Expect(accountsCursorResponse[0]).To(Equal(components.V2Account{ - Address: "foo:bar", - Metadata: metadata2, - })) - Expect(accountsCursorResponse[1]).To(Equal(components.V2Account{ - Address: "foo:foo", - Metadata: metadata1, - })) - - filtersAsJSON, err = json.Marshal(map[string]interface{}{ - "$match": map[string]any{ - "address": ":foo", - }, - }) - Expect(err).To(Succeed()) - - response, err = ListAccounts( - ctx, - testServer.GetValue(), - operations.V2ListAccountsRequest{ - Ledger: "default", - Query: pointer.For(string(filtersAsJSON)), - }, - ) - Expect(err).ToNot(HaveOccurred()) - - accountsCursorResponse = response.Data - Expect(accountsCursorResponse).To(HaveLen(1)) - Expect(accountsCursorResponse[0]).To(Equal(components.V2Account{ - Address: "foo:foo", - Metadata: metadata1, - })) - }) It("should be listed on api using metadata filters", func() { response, err := ListAccounts( ctx, From a7263c1cca7cd745c79d2c2185ef3005101aa21e Mon Sep 17 00:00:00 2001 From: Ragot Geoffrey Date: Thu, 5 Dec 2024 15:31:14 +0100 Subject: [PATCH 49/71] fix: missing features migrations for existing ledgers (#601) --- internal/README.md | 198 ++++++++++----------- internal/storage/system/migrations.go | 14 ++ internal/storage/system/migrations_test.go | 19 ++ 3 files changed, 132 insertions(+), 99 deletions(-) diff --git a/internal/README.md b/internal/README.md index 026b9e02e..bef2ec79d 100644 --- a/internal/README.md +++ b/internal/README.md @@ -146,7 +146,7 @@ var Zero = big.NewInt(0) ``` -## func ComputeIdempotencyHash +## func [ComputeIdempotencyHash]() ```go func ComputeIdempotencyHash(inputs any) string @@ -155,7 +155,7 @@ func ComputeIdempotencyHash(inputs any) string -## type Account +## type [Account]() @@ -166,7 +166,7 @@ type Account struct { Address string `json:"address" bun:"address"` Metadata metadata.Metadata `json:"metadata" bun:"metadata,type:jsonb,default:'{}'"` FirstUsage time.Time `json:"-" bun:"first_usage,nullzero"` - InsertionDate time.Time `json:"_" bun:"insertion_date,nullzero"` + InsertionDate time.Time `json:"-" bun:"insertion_date,nullzero"` UpdatedAt time.Time `json:"-" bun:"updated_at,nullzero"` Volumes VolumesByAssets `json:"volumes,omitempty" bun:"volumes,scanonly"` EffectiveVolumes VolumesByAssets `json:"effectiveVolumes,omitempty" bun:"effective_volumes,scanonly"` @@ -174,7 +174,7 @@ type Account struct { ``` -### func \(Account\) GetAddress +### func \(Account\) [GetAddress]() ```go func (a Account) GetAddress() string @@ -183,7 +183,7 @@ func (a Account) GetAddress() string -## type AccountMetadata +## type [AccountMetadata]() @@ -192,7 +192,7 @@ type AccountMetadata map[string]metadata.Metadata ``` -## type AccountsVolumes +## type [AccountsVolumes]() @@ -208,7 +208,7 @@ type AccountsVolumes struct { ``` -## type BalancesByAssets +## type [BalancesByAssets]() @@ -217,7 +217,7 @@ type BalancesByAssets map[string]*big.Int ``` -## type BalancesByAssetsByAccounts +## type [BalancesByAssetsByAccounts]() @@ -226,7 +226,7 @@ type BalancesByAssetsByAccounts map[string]BalancesByAssets ``` -## type Configuration +## type [Configuration]() @@ -239,7 +239,7 @@ type Configuration struct { ``` -### func NewDefaultConfiguration +### func [NewDefaultConfiguration]() ```go func NewDefaultConfiguration() Configuration @@ -248,7 +248,7 @@ func NewDefaultConfiguration() Configuration -### func \(\*Configuration\) SetDefaults +### func \(\*Configuration\) [SetDefaults]() ```go func (c *Configuration) SetDefaults() @@ -257,7 +257,7 @@ func (c *Configuration) SetDefaults() -### func \(\*Configuration\) Validate +### func \(\*Configuration\) [Validate]() ```go func (c *Configuration) Validate() error @@ -266,7 +266,7 @@ func (c *Configuration) Validate() error -## type CreatedTransaction +## type [CreatedTransaction]() @@ -278,7 +278,7 @@ type CreatedTransaction struct { ``` -### func \(CreatedTransaction\) GetMemento +### func \(CreatedTransaction\) [GetMemento]() ```go func (p CreatedTransaction) GetMemento() any @@ -287,7 +287,7 @@ func (p CreatedTransaction) GetMemento() any -### func \(CreatedTransaction\) Type +### func \(CreatedTransaction\) [Type]() ```go func (p CreatedTransaction) Type() LogType @@ -296,7 +296,7 @@ func (p CreatedTransaction) Type() LogType -## type DeletedMetadata +## type [DeletedMetadata]() @@ -309,7 +309,7 @@ type DeletedMetadata struct { ``` -### func \(DeletedMetadata\) Type +### func \(DeletedMetadata\) [Type]() ```go func (s DeletedMetadata) Type() LogType @@ -318,7 +318,7 @@ func (s DeletedMetadata) Type() LogType -### func \(\*DeletedMetadata\) UnmarshalJSON +### func \(\*DeletedMetadata\) [UnmarshalJSON]() ```go func (s *DeletedMetadata) UnmarshalJSON(data []byte) error @@ -327,7 +327,7 @@ func (s *DeletedMetadata) UnmarshalJSON(data []byte) error -## type ErrInvalidBucketName +## type [ErrInvalidBucketName]() @@ -338,7 +338,7 @@ type ErrInvalidBucketName struct { ``` -### func \(ErrInvalidBucketName\) Error +### func \(ErrInvalidBucketName\) [Error]() ```go func (e ErrInvalidBucketName) Error() string @@ -347,7 +347,7 @@ func (e ErrInvalidBucketName) Error() string -### func \(ErrInvalidBucketName\) Is +### func \(ErrInvalidBucketName\) [Is]() ```go func (e ErrInvalidBucketName) Is(err error) bool @@ -356,7 +356,7 @@ func (e ErrInvalidBucketName) Is(err error) bool -## type ErrInvalidLedgerName +## type [ErrInvalidLedgerName]() @@ -367,7 +367,7 @@ type ErrInvalidLedgerName struct { ``` -### func \(ErrInvalidLedgerName\) Error +### func \(ErrInvalidLedgerName\) [Error]() ```go func (e ErrInvalidLedgerName) Error() string @@ -376,7 +376,7 @@ func (e ErrInvalidLedgerName) Error() string -### func \(ErrInvalidLedgerName\) Is +### func \(ErrInvalidLedgerName\) [Is]() ```go func (e ErrInvalidLedgerName) Is(err error) bool @@ -385,7 +385,7 @@ func (e ErrInvalidLedgerName) Is(err error) bool -## type Ledger +## type [Ledger]() @@ -401,7 +401,7 @@ type Ledger struct { ``` -### func MustNewWithDefault +### func [MustNewWithDefault]() ```go func MustNewWithDefault(name string) Ledger @@ -410,7 +410,7 @@ func MustNewWithDefault(name string) Ledger -### func New +### func [New]() ```go func New(name string, configuration Configuration) (*Ledger, error) @@ -419,7 +419,7 @@ func New(name string, configuration Configuration) (*Ledger, error) -### func NewWithDefaults +### func [NewWithDefaults]() ```go func NewWithDefaults(name string) (*Ledger, error) @@ -428,7 +428,7 @@ func NewWithDefaults(name string) (*Ledger, error) -### func \(Ledger\) HasFeature +### func \(Ledger\) [HasFeature]() ```go func (l Ledger) HasFeature(feature, value string) bool @@ -437,7 +437,7 @@ func (l Ledger) HasFeature(feature, value string) bool -### func \(Ledger\) WithMetadata +### func \(Ledger\) [WithMetadata]() ```go func (l Ledger) WithMetadata(m metadata.Metadata) Ledger @@ -446,7 +446,7 @@ func (l Ledger) WithMetadata(m metadata.Metadata) Ledger -## type Log +## type [Log]() Log represents atomic actions made on the ledger. @@ -467,7 +467,7 @@ type Log struct { ``` -### func NewLog +### func [NewLog]() ```go func NewLog(payload LogPayload) Log @@ -476,7 +476,7 @@ func NewLog(payload LogPayload) Log -### func \(Log\) ChainLog +### func \(Log\) [ChainLog]() ```go func (l Log) ChainLog(previous *Log) Log @@ -485,7 +485,7 @@ func (l Log) ChainLog(previous *Log) Log -### func \(\*Log\) ComputeHash +### func \(\*Log\) [ComputeHash]() ```go func (l *Log) ComputeHash(previous *Log) @@ -494,7 +494,7 @@ func (l *Log) ComputeHash(previous *Log) -### func \(\*Log\) UnmarshalJSON +### func \(\*Log\) [UnmarshalJSON]() ```go func (l *Log) UnmarshalJSON(data []byte) error @@ -503,7 +503,7 @@ func (l *Log) UnmarshalJSON(data []byte) error -### func \(Log\) WithIdempotencyKey +### func \(Log\) [WithIdempotencyKey]() ```go func (l Log) WithIdempotencyKey(key string) Log @@ -512,7 +512,7 @@ func (l Log) WithIdempotencyKey(key string) Log -## type LogPayload +## type [LogPayload]() @@ -523,7 +523,7 @@ type LogPayload interface { ``` -### func HydrateLog +### func [HydrateLog]() ```go func HydrateLog(_type LogType, data []byte) (LogPayload, error) @@ -532,7 +532,7 @@ func HydrateLog(_type LogType, data []byte) (LogPayload, error) -## type LogType +## type [LogType]() @@ -552,7 +552,7 @@ const ( ``` -### func LogTypeFromString +### func [LogTypeFromString]() ```go func LogTypeFromString(logType string) LogType @@ -561,7 +561,7 @@ func LogTypeFromString(logType string) LogType -### func \(LogType\) MarshalJSON +### func \(LogType\) [MarshalJSON]() ```go func (lt LogType) MarshalJSON() ([]byte, error) @@ -570,7 +570,7 @@ func (lt LogType) MarshalJSON() ([]byte, error) -### func \(\*LogType\) Scan +### func \(\*LogType\) [Scan]() ```go func (lt *LogType) Scan(src interface{}) error @@ -579,7 +579,7 @@ func (lt *LogType) Scan(src interface{}) error -### func \(LogType\) String +### func \(LogType\) [String]() ```go func (lt LogType) String() string @@ -588,7 +588,7 @@ func (lt LogType) String() string -### func \(\*LogType\) UnmarshalJSON +### func \(\*LogType\) [UnmarshalJSON]() ```go func (lt *LogType) UnmarshalJSON(data []byte) error @@ -597,7 +597,7 @@ func (lt *LogType) UnmarshalJSON(data []byte) error -### func \(LogType\) Value +### func \(LogType\) [Value]() ```go func (lt LogType) Value() (driver.Value, error) @@ -606,7 +606,7 @@ func (lt LogType) Value() (driver.Value, error) -## type Memento +## type [Memento]() @@ -617,7 +617,7 @@ type Memento interface { ``` -## type Move +## type [Move]() @@ -638,7 +638,7 @@ type Move struct { ``` -## type Moves +## type [Moves]() @@ -647,7 +647,7 @@ type Moves []*Move ``` -### func \(Moves\) ComputePostCommitEffectiveVolumes +### func \(Moves\) [ComputePostCommitEffectiveVolumes]() ```go func (m Moves) ComputePostCommitEffectiveVolumes() PostCommitVolumes @@ -656,7 +656,7 @@ func (m Moves) ComputePostCommitEffectiveVolumes() PostCommitVolumes -## type PostCommitVolumes +## type [PostCommitVolumes]() @@ -665,7 +665,7 @@ type PostCommitVolumes map[string]VolumesByAssets ``` -### func \(PostCommitVolumes\) AddInput +### func \(PostCommitVolumes\) [AddInput]() ```go func (a PostCommitVolumes) AddInput(account, asset string, input *big.Int) @@ -674,7 +674,7 @@ func (a PostCommitVolumes) AddInput(account, asset string, input *big.Int) -### func \(PostCommitVolumes\) AddOutput +### func \(PostCommitVolumes\) [AddOutput]() ```go func (a PostCommitVolumes) AddOutput(account, asset string, output *big.Int) @@ -683,7 +683,7 @@ func (a PostCommitVolumes) AddOutput(account, asset string, output *big.Int) -### func \(PostCommitVolumes\) Copy +### func \(PostCommitVolumes\) [Copy]() ```go func (a PostCommitVolumes) Copy() PostCommitVolumes @@ -692,7 +692,7 @@ func (a PostCommitVolumes) Copy() PostCommitVolumes -### func \(PostCommitVolumes\) Merge +### func \(PostCommitVolumes\) [Merge]() ```go func (a PostCommitVolumes) Merge(volumes PostCommitVolumes) PostCommitVolumes @@ -701,7 +701,7 @@ func (a PostCommitVolumes) Merge(volumes PostCommitVolumes) PostCommitVolumes -## type Posting +## type [Posting]() @@ -715,7 +715,7 @@ type Posting struct { ``` -### func NewPosting +### func [NewPosting]() ```go func NewPosting(source string, destination string, asset string, amount *big.Int) Posting @@ -724,7 +724,7 @@ func NewPosting(source string, destination string, asset string, amount *big.Int -## type Postings +## type [Postings]() @@ -733,7 +733,7 @@ type Postings []Posting ``` -### func \(Postings\) Reverse +### func \(Postings\) [Reverse]() ```go func (p Postings) Reverse() Postings @@ -742,7 +742,7 @@ func (p Postings) Reverse() Postings -### func \(Postings\) Validate +### func \(Postings\) [Validate]() ```go func (p Postings) Validate() (int, error) @@ -751,7 +751,7 @@ func (p Postings) Validate() (int, error) -## type RevertedTransaction +## type [RevertedTransaction]() @@ -763,7 +763,7 @@ type RevertedTransaction struct { ``` -### func \(RevertedTransaction\) GetMemento +### func \(RevertedTransaction\) [GetMemento]() ```go func (r RevertedTransaction) GetMemento() any @@ -772,7 +772,7 @@ func (r RevertedTransaction) GetMemento() any -### func \(RevertedTransaction\) Type +### func \(RevertedTransaction\) [Type]() ```go func (r RevertedTransaction) Type() LogType @@ -781,7 +781,7 @@ func (r RevertedTransaction) Type() LogType -## type SavedMetadata +## type [SavedMetadata]() @@ -794,7 +794,7 @@ type SavedMetadata struct { ``` -### func \(SavedMetadata\) Type +### func \(SavedMetadata\) [Type]() ```go func (s SavedMetadata) Type() LogType @@ -803,7 +803,7 @@ func (s SavedMetadata) Type() LogType -### func \(\*SavedMetadata\) UnmarshalJSON +### func \(\*SavedMetadata\) [UnmarshalJSON]() ```go func (s *SavedMetadata) UnmarshalJSON(data []byte) error @@ -812,7 +812,7 @@ func (s *SavedMetadata) UnmarshalJSON(data []byte) error -## type Transaction +## type [Transaction]() @@ -833,7 +833,7 @@ type Transaction struct { ``` -### func NewTransaction +### func [NewTransaction]() ```go func NewTransaction() Transaction @@ -842,7 +842,7 @@ func NewTransaction() Transaction -### func \(Transaction\) InvolvedAccounts +### func \(Transaction\) [InvolvedAccounts]() ```go func (tx Transaction) InvolvedAccounts() []string @@ -851,7 +851,7 @@ func (tx Transaction) InvolvedAccounts() []string -### func \(Transaction\) InvolvedDestinations +### func \(Transaction\) [InvolvedDestinations]() ```go func (tx Transaction) InvolvedDestinations() map[string][]string @@ -860,7 +860,7 @@ func (tx Transaction) InvolvedDestinations() map[string][]string -### func \(Transaction\) IsReverted +### func \(Transaction\) [IsReverted]() ```go func (tx Transaction) IsReverted() bool @@ -869,7 +869,7 @@ func (tx Transaction) IsReverted() bool -### func \(Transaction\) JSONSchemaExtend +### func \(Transaction\) [JSONSchemaExtend]() ```go func (Transaction) JSONSchemaExtend(schema *jsonschema.Schema) @@ -878,7 +878,7 @@ func (Transaction) JSONSchemaExtend(schema *jsonschema.Schema) -### func \(Transaction\) MarshalJSON +### func \(Transaction\) [MarshalJSON]() ```go func (tx Transaction) MarshalJSON() ([]byte, error) @@ -887,7 +887,7 @@ func (tx Transaction) MarshalJSON() ([]byte, error) -### func \(Transaction\) Reverse +### func \(Transaction\) [Reverse]() ```go func (tx Transaction) Reverse() Transaction @@ -896,7 +896,7 @@ func (tx Transaction) Reverse() Transaction -### func \(Transaction\) VolumeUpdates +### func \(Transaction\) [VolumeUpdates]() ```go func (tx Transaction) VolumeUpdates() []AccountsVolumes @@ -905,7 +905,7 @@ func (tx Transaction) VolumeUpdates() []AccountsVolumes -### func \(Transaction\) WithInsertedAt +### func \(Transaction\) [WithInsertedAt]() ```go func (tx Transaction) WithInsertedAt(date time.Time) Transaction @@ -914,7 +914,7 @@ func (tx Transaction) WithInsertedAt(date time.Time) Transaction -### func \(Transaction\) WithMetadata +### func \(Transaction\) [WithMetadata]() ```go func (tx Transaction) WithMetadata(m metadata.Metadata) Transaction @@ -923,7 +923,7 @@ func (tx Transaction) WithMetadata(m metadata.Metadata) Transaction -### func \(Transaction\) WithPostCommitEffectiveVolumes +### func \(Transaction\) [WithPostCommitEffectiveVolumes]() ```go func (tx Transaction) WithPostCommitEffectiveVolumes(volumes PostCommitVolumes) Transaction @@ -932,7 +932,7 @@ func (tx Transaction) WithPostCommitEffectiveVolumes(volumes PostCommitVolumes) -### func \(Transaction\) WithPostings +### func \(Transaction\) [WithPostings]() ```go func (tx Transaction) WithPostings(postings ...Posting) Transaction @@ -941,7 +941,7 @@ func (tx Transaction) WithPostings(postings ...Posting) Transaction -### func \(Transaction\) WithReference +### func \(Transaction\) [WithReference]() ```go func (tx Transaction) WithReference(ref string) Transaction @@ -950,7 +950,7 @@ func (tx Transaction) WithReference(ref string) Transaction -### func \(Transaction\) WithRevertedAt +### func \(Transaction\) [WithRevertedAt]() ```go func (tx Transaction) WithRevertedAt(timestamp time.Time) Transaction @@ -959,7 +959,7 @@ func (tx Transaction) WithRevertedAt(timestamp time.Time) Transaction -### func \(Transaction\) WithTimestamp +### func \(Transaction\) [WithTimestamp]() ```go func (tx Transaction) WithTimestamp(ts time.Time) Transaction @@ -968,7 +968,7 @@ func (tx Transaction) WithTimestamp(ts time.Time) Transaction -## type TransactionData +## type [TransactionData]() @@ -983,7 +983,7 @@ type TransactionData struct { ``` -### func NewTransactionData +### func [NewTransactionData]() ```go func NewTransactionData() TransactionData @@ -992,7 +992,7 @@ func NewTransactionData() TransactionData -### func \(TransactionData\) WithPostings +### func \(TransactionData\) [WithPostings]() ```go func (data TransactionData) WithPostings(postings ...Posting) TransactionData @@ -1001,7 +1001,7 @@ func (data TransactionData) WithPostings(postings ...Posting) TransactionData -## type Transactions +## type [Transactions]() @@ -1012,7 +1012,7 @@ type Transactions struct { ``` -## type Volumes +## type [Volumes]() @@ -1024,7 +1024,7 @@ type Volumes struct { ``` -### func NewEmptyVolumes +### func [NewEmptyVolumes]() ```go func NewEmptyVolumes() Volumes @@ -1033,7 +1033,7 @@ func NewEmptyVolumes() Volumes -### func NewVolumesInt64 +### func [NewVolumesInt64]() ```go func NewVolumesInt64(input, output int64) Volumes @@ -1042,7 +1042,7 @@ func NewVolumesInt64(input, output int64) Volumes -### func \(Volumes\) Balance +### func \(Volumes\) [Balance]() ```go func (v Volumes) Balance() *big.Int @@ -1051,7 +1051,7 @@ func (v Volumes) Balance() *big.Int -### func \(Volumes\) Copy +### func \(Volumes\) [Copy]() ```go func (v Volumes) Copy() Volumes @@ -1060,7 +1060,7 @@ func (v Volumes) Copy() Volumes -### func \(Volumes\) JSONSchemaExtend +### func \(Volumes\) [JSONSchemaExtend]() ```go func (Volumes) JSONSchemaExtend(schema *jsonschema.Schema) @@ -1069,7 +1069,7 @@ func (Volumes) JSONSchemaExtend(schema *jsonschema.Schema) -### func \(Volumes\) MarshalJSON +### func \(Volumes\) [MarshalJSON]() ```go func (v Volumes) MarshalJSON() ([]byte, error) @@ -1078,7 +1078,7 @@ func (v Volumes) MarshalJSON() ([]byte, error) -### func \(\*Volumes\) Scan +### func \(\*Volumes\) [Scan]() ```go func (v *Volumes) Scan(src interface{}) error @@ -1087,7 +1087,7 @@ func (v *Volumes) Scan(src interface{}) error -### func \(Volumes\) Value +### func \(Volumes\) [Value]() ```go func (v Volumes) Value() (driver.Value, error) @@ -1096,7 +1096,7 @@ func (v Volumes) Value() (driver.Value, error) -## type VolumesByAssets +## type [VolumesByAssets]() @@ -1105,7 +1105,7 @@ type VolumesByAssets map[string]Volumes ``` -### func \(VolumesByAssets\) Balances +### func \(VolumesByAssets\) [Balances]() ```go func (v VolumesByAssets) Balances() BalancesByAssets @@ -1114,7 +1114,7 @@ func (v VolumesByAssets) Balances() BalancesByAssets -## type VolumesWithBalance +## type [VolumesWithBalance]() @@ -1127,7 +1127,7 @@ type VolumesWithBalance struct { ``` -## type VolumesWithBalanceByAssetByAccount +## type [VolumesWithBalanceByAssetByAccount]() @@ -1140,7 +1140,7 @@ type VolumesWithBalanceByAssetByAccount struct { ``` -## type VolumesWithBalanceByAssets +## type [VolumesWithBalanceByAssets]() diff --git a/internal/storage/system/migrations.go b/internal/storage/system/migrations.go index eed37430d..0de325056 100644 --- a/internal/storage/system/migrations.go +++ b/internal/storage/system/migrations.go @@ -5,6 +5,7 @@ import ( "database/sql" "github.com/formancehq/go-libs/v2/platform/postgres" "github.com/formancehq/go-libs/v2/time" + "github.com/formancehq/ledger/pkg/features" "github.com/formancehq/go-libs/v2/migrations" "github.com/uptrace/bun" @@ -201,6 +202,19 @@ func GetMigrator(db *bun.DB, options ...migrations.Option) *migrations.Migrator }) }, }, + migrations.Migration{ + Name: "Configure features for old ledgers", + Up: func(ctx context.Context, db bun.IDB) error { + return db.RunInTx(ctx, &sql.TxOptions{}, func(ctx context.Context, tx bun.Tx) error { + _, err := tx.ExecContext(ctx, ` + update _system.ledgers + set features = ? + where features is null; + `, features.DefaultFeatures) + return err + }) + }, + }, ) return migrator diff --git a/internal/storage/system/migrations_test.go b/internal/storage/system/migrations_test.go index 044fb533e..0b25b7bbb 100644 --- a/internal/storage/system/migrations_test.go +++ b/internal/storage/system/migrations_test.go @@ -6,6 +6,7 @@ import ( "context" "fmt" "github.com/formancehq/go-libs/v2/testing/migrations" + "github.com/formancehq/ledger/pkg/features" "os" "testing" @@ -37,6 +38,7 @@ func TestMigrations(t *testing.T) { test := migrations.NewMigrationTest(t, GetMigrator(db), db) test.Append(8, addIdOnLedgerTable) + test.Append(14, addDefaultFeatures) test.Run() } @@ -80,3 +82,20 @@ var addIdOnLedgerTable = migrations.Hook{ require.Equal(t, int64(4), newLedger["id"]) }, } + +var addDefaultFeatures = migrations.Hook{ + After: func(ctx context.Context, t *testing.T, db bun.IDB) { + type x struct { + Features map[string]string `bun:"features"` + } + model := make([]x, 0) + err := db.NewSelect(). + ModelTableExpr("_system.ledgers"). + Scan(ctx, &model) + require.NoError(t, err) + + for _, m := range model { + require.EqualValues(t, features.DefaultFeatures, m.Features) + } + }, +} From b69289b201c699f632f0baccc659c149298fba31 Mon Sep 17 00:00:00 2001 From: Ragot Geoffrey Date: Thu, 5 Dec 2024 15:32:17 +0100 Subject: [PATCH 50/71] chore: remove some infrastructure tests (moved to dedicated repository) (#596) * feat: some pulumi split * wip * feat: add dataset gen * wip * wip * feat: test CI * chore: clean * wip --- .github/workflows/main.yml | 40 -- Earthfile | 2 +- deployments/helm/.helmignore | 23 - deployments/helm/Chart.yaml | 24 - deployments/helm/Earthfile | 12 - deployments/helm/README.md | 5 - deployments/helm/templates/NOTES.txt | 22 - deployments/helm/templates/_helpers.tpl | 62 -- deployments/helm/templates/deployment.yaml | 86 --- deployments/helm/templates/hpa.yaml | 32 - deployments/helm/templates/ingress.yaml | 61 -- deployments/helm/templates/service.yaml | 15 - .../helm/templates/serviceaccount.yaml | 13 - .../helm/templates/tests/test-connection.yaml | 15 - deployments/helm/values.yaml | 107 ---- deployments/pulumi/Earthfile | 30 + deployments/pulumi/go.mod | 147 ++++- deployments/pulumi/go.sum | 443 +++++++++++-- deployments/pulumi/main.go | 41 +- deployments/pulumi/main_test.go | 98 +++ deployments/pulumi/pkg/component.go | 604 ++++++++++++++++++ pkg/generate/set.go | 69 +- test/rolling-upgrades/Earthfile | 129 ---- test/rolling-upgrades/README.md | 55 -- test/rolling-upgrades/go.mod | 121 ---- test/rolling-upgrades/go.sum | 373 ----------- test/rolling-upgrades/main_test.go | 286 --------- 27 files changed, 1318 insertions(+), 1597 deletions(-) delete mode 100644 deployments/helm/.helmignore delete mode 100644 deployments/helm/Chart.yaml delete mode 100644 deployments/helm/Earthfile delete mode 100644 deployments/helm/README.md delete mode 100644 deployments/helm/templates/NOTES.txt delete mode 100644 deployments/helm/templates/_helpers.tpl delete mode 100644 deployments/helm/templates/deployment.yaml delete mode 100644 deployments/helm/templates/hpa.yaml delete mode 100644 deployments/helm/templates/ingress.yaml delete mode 100644 deployments/helm/templates/service.yaml delete mode 100644 deployments/helm/templates/serviceaccount.yaml delete mode 100644 deployments/helm/templates/tests/test-connection.yaml delete mode 100644 deployments/helm/values.yaml create mode 100644 deployments/pulumi/main_test.go create mode 100644 deployments/pulumi/pkg/component.go delete mode 100644 test/rolling-upgrades/Earthfile delete mode 100644 test/rolling-upgrades/README.md delete mode 100644 test/rolling-upgrades/go.mod delete mode 100644 test/rolling-upgrades/go.sum delete mode 100644 test/rolling-upgrades/main_test.go diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8c3160804..ea60d8b64 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -78,46 +78,6 @@ jobs: env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - TestsDeployments: - runs-on: "formance-runner" - if: github.event_name == 'pull_request' - concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-deployments-tests - cancel-in-progress: false - steps: - - uses: 'actions/checkout@v4' - with: - fetch-depth: 0 - - name: Setup Env - uses: ./.github/actions/env - with: - token: ${{ secrets.NUMARY_GITHUB_TOKEN }} - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: "NumaryBot" - password: ${{ secrets.NUMARY_GITHUB_TOKEN }} - - run: > - earthly - --allow-privileged - --no-output - --push - --secret GITHUB_TOKEN=$GITHUB_TOKEN - --secret KUBE_APISERVER=$KUBE_APISERVER - --secret KUBE_TOKEN=$KUBE_TOKEN - --secret PULUMI_ACCESS_TOKEN=$PULUMI_ACCESS_TOKEN - ${{ contains(github.event.pull_request.labels.*.name, 'no-cache') && '--no-cache' || '' }} - ./test/rolling-upgrades+run - --CLUSTER_NAME ledger-${{ github.event.number }} - --NO_CLEANUP=${{ contains(github.event.pull_request.labels.*.name, 'no-cleanup') && 'true' || 'false' }} - --NO_CLEANUP_ON_FAILURE=true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - KUBE_APISERVER: ${{ secrets.FORMANCE_DEV_KUBE_API_SERVER_ADDRESS }} - KUBE_TOKEN: ${{ secrets.FORMANCE_DEV_KUBE_TOKEN }} - PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }} - GoReleaser: runs-on: "formance-runner" if: contains(github.event.pull_request.labels.*.name, 'build-images') || github.ref == 'refs/heads/main' || github.event_name == 'merge_group' diff --git a/Earthfile b/Earthfile index 6f841ce8e..4f36f999c 100644 --- a/Earthfile +++ b/Earthfile @@ -142,8 +142,8 @@ pre-commit: BUILD +generate-client BUILD +export-docs-events - BUILD ./test/*+pre-commit BUILD ./tools/*+pre-commit + BUILD ./deployments/*+pre-commit openapi: FROM node:20-alpine diff --git a/deployments/helm/.helmignore b/deployments/helm/.helmignore deleted file mode 100644 index 0e8a0eb36..000000000 --- a/deployments/helm/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*.orig -*~ -# Various IDEs -.project -.idea/ -*.tmproj -.vscode/ diff --git a/deployments/helm/Chart.yaml b/deployments/helm/Chart.yaml deleted file mode 100644 index 7fba0ef93..000000000 --- a/deployments/helm/Chart.yaml +++ /dev/null @@ -1,24 +0,0 @@ -apiVersion: v2 -name: ledger -description: A Helm chart for Kubernetes - -# A chart can be either an 'application' or a 'library' chart. -# -# Application charts are a collection of templates that can be packaged into versioned archives -# to be deployed. -# -# Library charts provide useful utilities or functions for the chart developer. They're included as -# a dependency of application charts to inject those utilities and functions into the rendering -# pipeline. Library charts do not define any templates and therefore cannot be deployed. -type: application - -# This is the chart version. This version number should be incremented each time you make changes -# to the chart and its templates, including the app version. -# Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.0 - -# This is the version number of the application being deployed. This version number should be -# incremented each time you make changes to the application. Versions are not expected to -# follow Semantic Versioning. They should reflect the version the application is using. -# It is recommended to use it with quotes. -appVersion: "latest" diff --git a/deployments/helm/Earthfile b/deployments/helm/Earthfile deleted file mode 100644 index 45622eeb5..000000000 --- a/deployments/helm/Earthfile +++ /dev/null @@ -1,12 +0,0 @@ -VERSION 0.8 -PROJECT FormanceHQ/ledger - -IMPORT github.com/formancehq/earthly:tags/v0.19.0 AS core - -FROM core+base-image - -sources: - WORKDIR /src - COPY *.yaml . - COPY --dir templates . - SAVE ARTIFACT /src diff --git a/deployments/helm/README.md b/deployments/helm/README.md deleted file mode 100644 index 1fe5d4963..000000000 --- a/deployments/helm/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Helm - -> [!WARNING] -> This chart is used for testing only. It is not intended for production use. -> It can be broken or removed at any time. \ No newline at end of file diff --git a/deployments/helm/templates/NOTES.txt b/deployments/helm/templates/NOTES.txt deleted file mode 100644 index 319f01bda..000000000 --- a/deployments/helm/templates/NOTES.txt +++ /dev/null @@ -1,22 +0,0 @@ -1. Get the application URL by running these commands: -{{- if .Values.ingress.enabled }} -{{- range $host := .Values.ingress.hosts }} - {{- range .paths }} - http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} - {{- end }} -{{- end }} -{{- else if contains "NodePort" .Values.service.type }} - export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "chart.fullname" . }}) - export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") - echo http://$NODE_IP:$NODE_PORT -{{- else if contains "LoadBalancer" .Values.service.type }} - NOTE: It may take a few minutes for the LoadBalancer IP to be available. - You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "chart.fullname" . }}' - export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "chart.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") - echo http://$SERVICE_IP:{{ .Values.service.port }} -{{- else if contains "ClusterIP" .Values.service.type }} - export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "chart.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") - export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") - echo "Visit http://127.0.0.1:8080 to use your application" - kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT -{{- end }} diff --git a/deployments/helm/templates/_helpers.tpl b/deployments/helm/templates/_helpers.tpl deleted file mode 100644 index 7ba5edc27..000000000 --- a/deployments/helm/templates/_helpers.tpl +++ /dev/null @@ -1,62 +0,0 @@ -{{/* -Expand the name of the chart. -*/}} -{{- define "chart.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -If release name contains chart name it will be used as a full name. -*/}} -{{- define "chart.fullname" -}} -{{- if .Values.fullnameOverride }} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- $name := default .Chart.Name .Values.nameOverride }} -{{- if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} -{{- end }} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "chart.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Common labels -*/}} -{{- define "chart.labels" -}} -helm.sh/chart: {{ include "chart.chart" . }} -{{ include "chart.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end }} - -{{/* -Selector labels -*/}} -{{- define "chart.selectorLabels" -}} -app.kubernetes.io/name: {{ include "chart.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{/* -Create the name of the service account to use -*/}} -{{- define "chart.serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- default (include "chart.fullname" .) .Values.serviceAccount.name }} -{{- else }} -{{- default "default" .Values.serviceAccount.name }} -{{- end }} -{{- end }} diff --git a/deployments/helm/templates/deployment.yaml b/deployments/helm/templates/deployment.yaml deleted file mode 100644 index 727c34a5a..000000000 --- a/deployments/helm/templates/deployment.yaml +++ /dev/null @@ -1,86 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "chart.fullname" . }} - labels: - {{- include "chart.labels" . | nindent 4 }} -spec: - {{- if not .Values.autoscaling.enabled }} - replicas: {{ .Values.replicaCount }} - {{- end }} - selector: - matchLabels: - {{- include "chart.selectorLabels" . | nindent 6 }} - template: - metadata: - {{- with .Values.podAnnotations }} - annotations: - {{- toYaml . | nindent 8 }} - {{- end }} - labels: - {{- include "chart.labels" . | nindent 8 }} - {{- with .Values.podLabels }} - {{- toYaml . | nindent 8 }} - {{- end }} - spec: - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - serviceAccountName: {{ include "chart.serviceAccountName" . }} - securityContext: - {{- toYaml .Values.podSecurityContext | nindent 8 }} - containers: - - name: {{ .Chart.Name }} - securityContext: - {{- toYaml .Values.securityContext | nindent 12 }} - image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - env: - - name: POSTGRES_URI - value: {{ .Values.postgres.uri }} - - name: BIND - value: ":{{ .Values.service.port }}" - - name: DEBUG - value: "{{ .Values.debug }}" - - name: EXPERIMENTAL_FEATURES - value: "{{ .Values.experimentalFeatures }}" - {{- if not (eq .Values.gracePeriod "") }} - # https://freecontent.manning.com/handling-client-requests-properly-with-kubernetes/ - - name: GRACE_PERIOD - value: "{{ .Values.gracePeriod }}" - {{- end }} - ports: - - name: http - containerPort: {{ .Values.service.port }} - protocol: TCP - livenessProbe: - httpGet: - path: /_healthcheck - port: http - readinessProbe: - httpGet: - path: /_healthcheck - port: http - resources: - {{- toYaml .Values.resources | nindent 12 }} - {{- with .Values.volumeMounts }} - volumeMounts: - {{- toYaml . | nindent 12 }} - {{- end }} - {{- with .Values.volumes }} - volumes: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} diff --git a/deployments/helm/templates/hpa.yaml b/deployments/helm/templates/hpa.yaml deleted file mode 100644 index a91f61bd5..000000000 --- a/deployments/helm/templates/hpa.yaml +++ /dev/null @@ -1,32 +0,0 @@ -{{- if .Values.autoscaling.enabled }} -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: {{ include "chart.fullname" . }} - labels: - {{- include "chart.labels" . | nindent 4 }} -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: {{ include "chart.fullname" . }} - minReplicas: {{ .Values.autoscaling.minReplicas }} - maxReplicas: {{ .Values.autoscaling.maxReplicas }} - metrics: - {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} - {{- end }} - {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} - - type: Resource - resource: - name: memory - target: - type: Utilization - averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} - {{- end }} -{{- end }} diff --git a/deployments/helm/templates/ingress.yaml b/deployments/helm/templates/ingress.yaml deleted file mode 100644 index 63c1311c9..000000000 --- a/deployments/helm/templates/ingress.yaml +++ /dev/null @@ -1,61 +0,0 @@ -{{- if .Values.ingress.enabled -}} -{{- $fullName := include "chart.fullname" . -}} -{{- $svcPort := .Values.service.port -}} -{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} - {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} - {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} - {{- end }} -{{- end }} -{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} -apiVersion: networking.k8s.io/v1 -{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} -apiVersion: networking.k8s.io/v1beta1 -{{- else -}} -apiVersion: extensions/v1beta1 -{{- end }} -kind: Ingress -metadata: - name: {{ $fullName }} - labels: - {{- include "chart.labels" . | nindent 4 }} - {{- with .Values.ingress.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} - ingressClassName: {{ .Values.ingress.className }} - {{- end }} - {{- if .Values.ingress.tls }} - tls: - {{- range .Values.ingress.tls }} - - hosts: - {{- range .hosts }} - - {{ . | quote }} - {{- end }} - secretName: {{ .secretName }} - {{- end }} - {{- end }} - rules: - {{- range .Values.ingress.hosts }} - - host: {{ .host | quote }} - http: - paths: - {{- range .paths }} - - path: {{ .path }} - {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} - pathType: {{ .pathType }} - {{- end }} - backend: - {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} - service: - name: {{ $fullName }} - port: - number: {{ $svcPort }} - {{- else }} - serviceName: {{ $fullName }} - servicePort: {{ $svcPort }} - {{- end }} - {{- end }} - {{- end }} -{{- end }} diff --git a/deployments/helm/templates/service.yaml b/deployments/helm/templates/service.yaml deleted file mode 100644 index dfc5b3a33..000000000 --- a/deployments/helm/templates/service.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "chart.fullname" . }} - labels: - {{- include "chart.labels" . | nindent 4 }} -spec: - type: {{ .Values.service.type }} - ports: - - port: {{ .Values.service.port }} - targetPort: http - protocol: TCP - name: http - selector: - {{- include "chart.selectorLabels" . | nindent 4 }} diff --git a/deployments/helm/templates/serviceaccount.yaml b/deployments/helm/templates/serviceaccount.yaml deleted file mode 100644 index 1df935010..000000000 --- a/deployments/helm/templates/serviceaccount.yaml +++ /dev/null @@ -1,13 +0,0 @@ -{{- if .Values.serviceAccount.create -}} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "chart.serviceAccountName" . }} - labels: - {{- include "chart.labels" . | nindent 4 }} - {{- with .Values.serviceAccount.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -automountServiceAccountToken: {{ .Values.serviceAccount.automount }} -{{- end }} diff --git a/deployments/helm/templates/tests/test-connection.yaml b/deployments/helm/templates/tests/test-connection.yaml deleted file mode 100644 index 8dfed872d..000000000 --- a/deployments/helm/templates/tests/test-connection.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: "{{ include "chart.fullname" . }}-test-connection" - labels: - {{- include "chart.labels" . | nindent 4 }} - annotations: - "helm.sh/hook": test -spec: - containers: - - name: wget - image: busybox - command: ['wget'] - args: ['{{ include "chart.fullname" . }}:{{ .Values.service.port }}'] - restartPolicy: Never diff --git a/deployments/helm/values.yaml b/deployments/helm/values.yaml deleted file mode 100644 index 71cd80f1c..000000000 --- a/deployments/helm/values.yaml +++ /dev/null @@ -1,107 +0,0 @@ -# Default values for chart. -# This is a YAML-formatted file. -# Declare variables to be passed into your templates. - -replicaCount: 1 - -image: - repository: ghcr.io/formancehq/ledger - pullPolicy: IfNotPresent - # Overrides the image tag whose default is the chart appVersion. - tag: "" - -imagePullSecrets: [] -nameOverride: "" -fullnameOverride: "" - -serviceAccount: - # Specifies whether a service account should be created - create: true - # Automatically mount a ServiceAccount's API credentials? - automount: true - # Annotations to add to the service account - annotations: {} - # The name of the service account to use. - # If not set and create is true, a name is generated using the fullname template - name: "" - -podAnnotations: {} -podLabels: {} - -podSecurityContext: {} - # fsGroup: 2000 - -securityContext: {} - # capabilities: - # drop: - # - ALL - # readOnlyRootFilesystem: true - # runAsNonRoot: true - # runAsUser: 1000 - -service: - type: ClusterIP - port: 8080 - -ingress: - enabled: false - className: "" - annotations: {} - # kubernetes.io/ingress.class: nginx - # kubernetes.io/tls-acme: "true" - hosts: - - host: chart-example.local - paths: - - path: / - pathType: ImplementationSpecific - tls: [] - # - secretName: chart-example-tls - # hosts: - # - chart-example.local - -resources: {} - # We usually recommend not to specify default resources and to leave this as a conscious - # choice for the user. This also increases chances charts run on environments with little - # resources, such as Minikube. If you do want to specify resources, uncomment the following - # lines, adjust them as necessary, and remove the curly braces after 'resources:'. - # limits: - # cpu: 100m - # memory: 128Mi - # requests: - # cpu: 100m - # memory: 128Mi - -autoscaling: - enabled: false - minReplicas: 1 - maxReplicas: 100 - targetCPUUtilizationPercentage: 80 - # targetMemoryUtilizationPercentage: 80 - -# Additional volumes on the output Deployment definition. -volumes: [] -# - name: foo -# secret: -# secretName: mysecret -# optional: false - -# Additional volumeMounts on the output Deployment definition. -volumeMounts: [] -# - name: foo -# mountPath: "/etc/foo" -# readOnly: true - -nodeSelector: {} - -tolerations: [] - -affinity: {} - -postgres: - uri: "" - -debug: false - -gracePeriod: "5s" - -experimentalFeatures: false \ No newline at end of file diff --git a/deployments/pulumi/Earthfile b/deployments/pulumi/Earthfile index 1a0379c50..6c750b957 100644 --- a/deployments/pulumi/Earthfile +++ b/deployments/pulumi/Earthfile @@ -5,7 +5,37 @@ IMPORT github.com/formancehq/earthly:tags/v0.19.0 AS core FROM core+base-image +CACHE --sharing=shared --id go-mod-cache /go/pkg/mod +CACHE --sharing=shared --id go-cache /root/.cache/go-build +CACHE --sharing=shared --id golangci-cache /root/.cache/golangci-lint + sources: + FROM core+builder-image WORKDIR /src COPY *.go go.* Pulumi.yaml . + COPY --dir pkg . SAVE ARTIFACT /src + +tidy: + FROM +sources + CACHE --id go-mod-cache /go/pkg/mod + CACHE --id go-cache /root/.cache/go-build + RUN go mod tidy + + SAVE ARTIFACT go.mod AS LOCAL go.mod + SAVE ARTIFACT go.sum AS LOCAL go.sum + +lint: + FROM +tidy + CACHE --id go-mod-cache /go/pkg/mod + CACHE --id go-cache /root/.cache/go-build + CACHE --id golangci-cache /root/.cache/golangci-lint + + RUN golangci-lint run --fix --build-tags it --timeout 5m + + SAVE ARTIFACT main.go AS LOCAL main.go + SAVE ARTIFACT pkg AS LOCAL pkg + +pre-commit: + BUILD +tidy + BUILD +lint \ No newline at end of file diff --git a/deployments/pulumi/go.mod b/deployments/pulumi/go.mod index f7c0a198b..6131897eb 100644 --- a/deployments/pulumi/go.mod +++ b/deployments/pulumi/go.mod @@ -1,23 +1,60 @@ module github.com/formancehq/ledger/deployments/pulumi -go 1.22 +go 1.23.1 + +toolchain go1.23.3 require ( + github.com/formancehq/go-libs/v2 v2.0.1-0.20241202204934-bf7e2f5dccef + github.com/google/uuid v1.6.0 github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.18.3 - github.com/pulumi/pulumi/sdk/v3 v3.137.0 + github.com/pulumi/pulumi/pkg/v3 v3.142.0 + github.com/pulumi/pulumi/sdk/v3 v3.142.0 + github.com/stretchr/testify v1.10.0 ) require ( - dario.cat/mergo v1.0.0 // indirect + cloud.google.com/go v0.112.1 // indirect + cloud.google.com/go/compute/metadata v0.5.0 // indirect + cloud.google.com/go/iam v1.1.6 // indirect + cloud.google.com/go/kms v1.15.7 // indirect + cloud.google.com/go/logging v1.9.0 // indirect + cloud.google.com/go/longrunning v0.5.5 // indirect + cloud.google.com/go/storage v1.39.1 // indirect + dario.cat/mergo v1.0.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect github.com/BurntSushi/toml v1.2.1 // indirect - github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.0.0 // indirect + github.com/ThreeDotsLabs/watermill v1.4.1 // indirect github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect github.com/agext/levenshtein v1.2.3 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect + github.com/aws/aws-sdk-go v1.50.36 // indirect + github.com/aws/aws-sdk-go-v2 v1.32.6 // indirect + github.com/aws/aws-sdk-go-v2/config v1.28.6 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.47 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.6 // indirect + github.com/aws/aws-sdk-go-v2/service/kms v1.30.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.24.7 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.33.2 // indirect + github.com/aws/smithy-go v1.22.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/blang/semver v3.5.1+incompatible // indirect + github.com/cenkalti/backoff/v3 v3.2.2 // indirect github.com/charmbracelet/bubbles v0.16.1 // indirect github.com/charmbracelet/bubbletea v0.25.0 // indirect github.com/charmbracelet/lipgloss v0.7.1 // indirect @@ -25,67 +62,137 @@ require ( github.com/cloudflare/circl v1.3.7 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/deckarep/golang-set/v2 v2.5.0 // indirect github.com/djherbis/times v1.5.0 // indirect + github.com/edsrzf/mmap-go v1.1.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-git/go-git/v5 v5.12.0 // indirect + github.com/go-jose/go-jose/v3 v3.0.3 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gofrs/uuid v4.2.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/glog v1.2.0 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/golang/glog v1.2.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/google/uuid v1.6.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/s2a-go v0.1.7 // indirect + github.com/google/wire v0.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/googleapis/gax-go/v2 v2.12.2 // indirect + github.com/gorilla/mux v1.8.1 // indirect github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/hcl/v2 v2.17.0 // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 // indirect + github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect + github.com/hashicorp/go-sockaddr v1.0.6 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hashicorp/hcl/v2 v2.22.0 // indirect + github.com/hashicorp/vault/api v1.12.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/lithammer/shortuuid/v3 v3.0.7 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.2 // indirect + github.com/natefinch/atomic v1.0.1 // indirect + github.com/nxadm/tail v1.4.11 // indirect + github.com/oklog/ulid v1.3.1 // indirect github.com/opentracing/basictracer-go v1.1.0 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pgavlin/fx v0.1.6 // indirect + github.com/pgavlin/goldmark v1.1.33-0.20200616210433-b5eb04559386 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pkg/term v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 // indirect github.com/pulumi/esc v0.10.0 // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 // indirect + github.com/segmentio/asm v1.2.0 // indirect + github.com/segmentio/encoding v0.3.5 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.2.2 // indirect github.com/spf13/cast v1.4.1 // indirect - github.com/spf13/cobra v1.8.0 // indirect + github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/texttheater/golang-levenshtein v1.0.1 // indirect github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect github.com/uber/jaeger-lib v2.4.1+incompatible // indirect + github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 // indirect + github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/zclconf/go-cty v1.13.2 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 // indirect + go.opentelemetry.io/otel v1.32.0 // indirect + go.opentelemetry.io/otel/log v0.6.0 // indirect + go.opentelemetry.io/otel/metric v1.32.0 // indirect + go.opentelemetry.io/otel/trace v1.32.0 // indirect go.uber.org/atomic v1.9.0 // indirect - golang.org/x/crypto v0.25.0 // indirect - golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 // indirect - golang.org/x/mod v0.18.0 // indirect - golang.org/x/net v0.27.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/term v0.22.0 // indirect - golang.org/x/text v0.16.0 // indirect - golang.org/x/tools v0.22.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240311173647-c811ad7063a7 // indirect - google.golang.org/grpc v1.63.2 // indirect - google.golang.org/protobuf v1.33.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + gocloud.dev v0.37.0 // indirect + gocloud.dev/secrets/hashivault v0.37.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect + golang.org/x/mod v0.21.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/oauth2 v0.23.0 // indirect + golang.org/x/sync v0.9.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.20.0 // indirect + golang.org/x/time v0.7.0 // indirect + golang.org/x/tools v0.26.0 // indirect + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect + google.golang.org/api v0.169.0 // indirect + google.golang.org/genproto v0.0.0-20240311173647-c811ad7063a7 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 // indirect + google.golang.org/grpc v1.67.1 // indirect + google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/frand v1.4.2 // indirect diff --git a/deployments/pulumi/go.sum b/deployments/pulumi/go.sum index 7b4d7c8ab..450566e57 100644 --- a/deployments/pulumi/go.sum +++ b/deployments/pulumi/go.sum @@ -1,14 +1,44 @@ -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM= +cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4= +cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= +cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= +cloud.google.com/go/iam v1.1.6 h1:bEa06k05IO4f4uJonbB5iAgKTPpABy1ayxaIZV/GHVc= +cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI= +cloud.google.com/go/kms v1.15.7 h1:7caV9K3yIxvlQPAcaFffhlT7d1qpxjB1wHBtjWa13SM= +cloud.google.com/go/kms v1.15.7/go.mod h1:ub54lbsa6tDkUwnu4W7Yt1aAIFLnspgh0kPGToDukeI= +cloud.google.com/go/logging v1.9.0 h1:iEIOXFO9EmSiTjDmfpbRjOxECO7R8C7b8IXUGOj7xZw= +cloud.google.com/go/logging v1.9.0/go.mod h1:1Io0vnZv4onoUnsVUQY3HZ3Igb1nBchky0A0y7BBBhE= +cloud.google.com/go/longrunning v0.5.5 h1:GOE6pZFdSrTb4KAiKnXsJBtlE6mEyaW44oKyMILWnOg= +cloud.google.com/go/longrunning v0.5.5/go.mod h1:WV2LAxD8/rg5Z1cNW6FJ/ZpX4E4VnDnoTk0yawPBB7s= +cloud.google.com/go/storage v1.39.1 h1:MvraqHKhogCOTXTlct/9C3K3+Uy2jBmFYb3/Sp6dVtY= +cloud.google.com/go/storage v1.39.1/go.mod h1:xK6xZmxZmo+fyP7+DEF6FhNc24/JAe95OLyOHCXFH1o= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg= +github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0 h1:m/sWOGCREuSBqg2htVQTBY8nOZpyajYztF0vUvSZTuM= +github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0/go.mod h1:Pu5Zksi2KrU7LPbZbNINx6fuVrUp/ffvpxdDj+i8LeE= +github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 h1:FbH3BbSb4bvGluTesZZ+ttN/MDsnMmQP36OSnDuSXqw= +github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1/go.mod h1:9V2j0jn9jDEkCkv8w/bKTNppX/d0FVA1ud77xCIP4KA= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/ThreeDotsLabs/watermill v1.4.1 h1:gjP6yZH+otMPjV0KsV07pl9TeMm9UQV/gqiuiuG5Drs= +github.com/ThreeDotsLabs/watermill v1.4.1/go.mod h1:lBnrLbxOjeMRgcJbv+UiZr8Ylz8RkJ4m6i/VN/Nk+to= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= @@ -17,15 +47,64 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aws/aws-sdk-go v1.50.36 h1:PjWXHwZPuTLMR1NIb8nEjLucZBMzmf84TLoLbD8BZqk= +github.com/aws/aws-sdk-go v1.50.36/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= +github.com/aws/aws-sdk-go-v2 v1.32.6 h1:7BokKRgRPuGmKkFMhEg/jSul+tB9VvXhcViILtfG8b4= +github.com/aws/aws-sdk-go-v2 v1.32.6/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 h1:x6xsQXGSmW6frevwDA+vi/wqhp1ct18mVXYN08/93to= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2/go.mod h1:lPprDr1e6cJdyYeGXnRaJoP4Md+cDBvi2eOj00BlGmg= +github.com/aws/aws-sdk-go-v2/config v1.28.6 h1:D89IKtGrs/I3QXOLNTH93NJYtDhm8SYa9Q5CsPShmyo= +github.com/aws/aws-sdk-go-v2/config v1.28.6/go.mod h1:GDzxJ5wyyFSCoLkS+UhGB0dArhb9mI+Co4dHtoTxbko= +github.com/aws/aws-sdk-go-v2/credentials v1.17.47 h1:48bA+3/fCdi2yAwVt+3COvmatZ6jUDNkDTIsqDiMUdw= +github.com/aws/aws-sdk-go-v2/credentials v1.17.47/go.mod h1:+KdckOejLW3Ks3b0E3b5rHsr2f9yuORBum0WPnE5o5w= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.21 h1:AmoU1pziydclFT/xRV+xXE/Vb8fttJCLRPv8oAkprc0= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.21/go.mod h1:AjUdLYe4Tgs6kpH4Bv7uMZo7pottoyHMn4eTcIcneaY= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.15 h1:7Zwtt/lP3KNRkeZre7soMELMGNoBrutx8nobg1jKWmo= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.15/go.mod h1:436h2adoHb57yd+8W+gYPrrA9U/R/SuAuOO42Ushzhw= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25 h1:s/fF4+yDQDoElYhfIVvSNyeCydfbuTKzhxSXDXCPasU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25/go.mod h1:IgPfDv5jqFIzQSNbUEMoitNooSMXjRSDkhXv8jiROvU= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25 h1:ZntTCl5EsYnhN/IygQEUugpdwbhdkom9uHcbCftiGgA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25/go.mod h1:DBdPrgeocww+CSl1C8cEV8PN1mHMBhuCDLpXezyvWkE= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5 h1:81KE7vaZzrl7yHBYHVEzYB8sypz11NMOZ40YlWvPxsU= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5/go.mod h1:LIt2rg7Mcgn09Ygbdh/RdIm0rQ+3BNkbP1gyVMFtRK0= +github.com/aws/aws-sdk-go-v2/service/iam v1.31.4 h1:eVm30ZIDv//r6Aogat9I88b5YX1xASSLcEDqHYRPVl0= +github.com/aws/aws-sdk-go-v2/service/iam v1.31.4/go.mod h1:aXWImQV0uTW35LM0A/T4wEg6R1/ReXUu4SM6/lUHYK0= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7 h1:ZMeFZ5yk+Ek+jNr1+uwCd2tG89t6oTS5yVWpa6yy2es= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7/go.mod h1:mxV05U+4JiHqIpGqqYXOHLPKUC6bDXC44bsUhNjOEwY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.6 h1:50+XsN70RS7dwJ2CkVNXzj7U2L1HKP8nqTd3XWEXBN4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.6/go.mod h1:WqgLmwY7so32kG01zD8CPTJWVWM+TzJoOVHwTg4aPug= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5 h1:f9RyWNtS8oH7cZlbn+/JNPpjUk5+5fLd5lM9M0i49Ys= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5/go.mod h1:h5CoMZV2VF297/VLhRhO1WF+XYWOzXo+4HsObA4HjBQ= +github.com/aws/aws-sdk-go-v2/service/kms v1.30.1 h1:SBn4I0fJXF9FYOVRSVMWuhvEKoAHDikjGpS3wlmw5DE= +github.com/aws/aws-sdk-go-v2/service/kms v1.30.1/go.mod h1:2snWQJQUKsbN66vAawJuOGX7dr37pfOq9hb0tZDGIqQ= +github.com/aws/aws-sdk-go-v2/service/s3 v1.53.1 h1:6cnno47Me9bRykw9AEv9zkXE+5or7jz8TsskTTccbgc= +github.com/aws/aws-sdk-go-v2/service/s3 v1.53.1/go.mod h1:qmdkIIAC+GCLASF7R2whgNrJADz0QZPX+Seiw/i4S3o= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.7 h1:rLnYAfXQ3YAccocshIH5mzNNwZBkBo+bP6EhIxak6Hw= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.7/go.mod h1:ZHtuQJ6t9A/+YDuxOLnbryAmITtr8UysSny3qcyvJTc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.6 h1:JnhTZR3PiYDNKlXy50/pNeix9aGMo6lLpXwJ1mw8MD4= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.6/go.mod h1:URronUEGfXZN1VpdktPSD1EkAL9mfrV+2F4sjH38qOY= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.2 h1:s4074ZO1Hk8qv65GqNXqDjmkf4HSQqJukaLuuW0TpDA= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.2/go.mod h1:mVggCnIWoM09jP71Wh+ea7+5gAp53q+49wDFs1SW5z8= +github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= +github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M= +github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= @@ -34,26 +113,45 @@ github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZ github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= github.com/cheggaaa/pb v1.0.29 h1:FckUN5ngEk2LpvuG0fw1GEFx6LtyY2pWI/Z2QgCnEYo= github.com/cheggaaa/pb v1.0.29/go.mod h1:W40334L7FMC5JKWldsTWbdGjLo0RxUKK73K+TuPxX30= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set/v2 v2.5.0 h1:hn6cEZtQ0h3J8kFrHR/NrzyOoTnjgW1+FmNJzQ7y/sA= +github.com/deckarep/golang-set/v2 v2.5.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/djherbis/times v1.5.0 h1:79myA211VwPhFTqUk8xehWrsEO+zcIZj0zT8mXPVARU= github.com/djherbis/times v1.5.0/go.mod h1:5q7FDLvbNg1L/KaBmPcWlVR9NmoKo3+ucqUA3ijQhA0= +github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= +github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241202204934-bf7e2f5dccef h1:lY7BAdH/Duk7HOEjmm3s3YGx8eHucR0sImvWfCkgwYE= +github.com/formancehq/go-libs/v2 v2.0.1-0.20241202204934-bf7e2f5dccef/go.mod h1:XAMOzh543EEt4JKneVVsaM+cZ0TKDhNJuw02+vqJp2k= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= @@ -64,32 +162,111 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= +github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= +github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= +github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= +github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68= -github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY= +github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/go-replayers/grpcreplay v1.1.0 h1:S5+I3zYyZ+GQz68OfbURDdt/+cSMqCK1wrvNx7WBzTE= +github.com/google/go-replayers/grpcreplay v1.1.0/go.mod h1:qzAvJ8/wi57zq7gWqaE6AwLM6miiXUQwP1S+I9icmhk= +github.com/google/go-replayers/httpreplay v1.2.0 h1:VM1wEyyjaoU53BwrOnaf9VhAyQQEEioJvFYxYcLRKzk= +github.com/google/go-replayers/httpreplay v1.2.0/go.mod h1:WahEFFZZ7a1P4VM1qEeHy+tME4bwyqPcwWbNlUI1Mcg= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= +github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI= +github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.12.2 h1:mhN09QQW1jEWeMF74zGR81R30z4VJzjZsfkUhuHF+DA= +github.com/googleapis/gax-go/v2 v2.12.2/go.mod h1:61M8vcyyXR2kqKFxKrfA22jaA8JGF7Dc8App1U3H6jc= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/hcl/v2 v2.17.0 h1:z1XvSUyXd1HP10U4lrLg5e0JMVz6CPaJvAgxM0KNZVY= -github.com/hashicorp/hcl/v2 v2.17.0/go.mod h1:gJyW2PTShkJqQBKpAmPO3yxMxIuoXkOF2TpqXzrQyx4= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 h1:iBt4Ew4XEGLfh6/bPk4rSYmuZJGizr6/x/AEizP0CQc= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8/go.mod h1:aiJI+PIApBRQG7FZTEBx5GiiX+HbOHilUdNxUZi4eV0= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= +github.com/hashicorp/go-sockaddr v1.0.6 h1:RSG8rKU28VTUTvEKghe5gIhIQpv8evvNpnDEyqO4u9I= +github.com/hashicorp/go-sockaddr v1.0.6/go.mod h1:uoUUmtwU7n9Dv3O4SNLeFvg0SxQ3lyjsj6+CCykpaxI= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/hcl/v2 v2.22.0 h1:hkZ3nCtqeJsDhPRFz5EA9iwcG1hNWGePOTw6oyul12M= +github.com/hashicorp/hcl/v2 v2.22.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= +github.com/hashicorp/vault/api v1.12.0 h1:meCpJSesvzQyao8FCOgk2fGdoADAnbDu2WPJN1lDLJ4= +github.com/hashicorp/vault/api v1.12.0/go.mod h1:si+lJCYO7oGkIoNPAN8j3azBLTn9SjMGS+jFaHd1Cck= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= @@ -102,13 +279,22 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8= +github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= @@ -117,10 +303,23 @@ github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= @@ -129,8 +328,14 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= -github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= -github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= +github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/gomega v1.36.0 h1:Pb12RlruUtj4XUuPUqeEWc6j5DkVVVA49Uf6YLfC95Y= +github.com/onsi/gomega v1.36.0/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/opentracing/basictracer-go v1.1.0 h1:Oa1fTSBvAl8pa3U+IJYqrKm0NALwH9OsgwOqDv4xJW0= github.com/opentracing/basictracer-go v1.1.0/go.mod h1:V2HZueSJEp879yv285Aap1BS69fQMD+MNP1mRs6mBQc= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= @@ -138,22 +343,30 @@ github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+ github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/pgavlin/fx v0.1.6 h1:r9jEg69DhNoCd3Xh0+5mIbdbS3PqWrVWujkY76MFRTU= github.com/pgavlin/fx v0.1.6/go.mod h1:KWZJ6fqBBSh8GxHYqwYCf3rYE7Gp2p0N8tJp8xv9u9M= +github.com/pgavlin/goldmark v1.1.33-0.20200616210433-b5eb04559386 h1:LoCV5cscNVWyK5ChN/uCoIFJz8jZD63VQiGJIRgr6uo= +github.com/pgavlin/goldmark v1.1.33-0.20200616210433-b5eb04559386/go.mod h1:MRxHTJrf9FhdfNQ8Hdeh9gmHevC9RJE/fu8M3JIGjoE= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/term v1.1.0 h1:xIAAdCMh3QIAy+5FrE8Ad8XoDhEU4ufwbaSozViP9kk= github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 h1:vkHw5I/plNdTr435cARxCW6q9gc0S/Yxz7Mkd38pOb0= github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231/go.mod h1:murToZ2N9hNJzewjHBgfFdXhZKjY3z5cYC1VXk+lbFE= github.com/pulumi/esc v0.10.0 h1:jzBKzkLVW0mePeanDRfqSQoCJ5yrkux0jIwAkUxpRKE= github.com/pulumi/esc v0.10.0/go.mod h1:2Bfa+FWj/xl8CKqRTWbWgDX0SOD4opdQgvYSURTGK2c= github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.18.3 h1:quqoGsLbF7lpGpGU4mi5WfVLIAo4gfvoQeYYmemx1Dg= github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.18.3/go.mod h1:9dBA6+rtpKmyZB3k1XryUOHDOuNdoTODFKEEZZCtrz8= -github.com/pulumi/pulumi/sdk/v3 v3.137.0 h1:bxhYpOY7Z4xt+VmezEpHuhjpOekkaMqOjzxFg/1OhCw= -github.com/pulumi/pulumi/sdk/v3 v3.137.0/go.mod h1:PvKsX88co8XuwuPdzolMvew5lZV+4JmZfkeSjj7A6dI= +github.com/pulumi/pulumi/pkg/v3 v3.142.0 h1:UE8TFyXrlxvPrATpd3Kl3En34KrFIFWOxxNAodywPNU= +github.com/pulumi/pulumi/pkg/v3 v3.142.0/go.mod h1:3k6WwRIT7veiDnk3Yo2NtqEYX+4dgLCrMIFvEOnjQqI= +github.com/pulumi/pulumi/sdk/v3 v3.142.0 h1:SmcVddGuvwAh3g3XUVQQ5gVRQUKH1yZ6iETpDNHIHlw= +github.com/pulumi/pulumi/sdk/v3 v3.142.0/go.mod h1:PvKsX88co8XuwuPdzolMvew5lZV+4JmZfkeSjj7A6dI= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= @@ -161,45 +374,99 @@ github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI= github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 h1:TToq11gyfNlrMFZiYujSekIsPd9AmsA2Bj/iv+s4JHE= github.com/santhosh-tekuri/jsonschema/v5 v5.0.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= +github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/segmentio/encoding v0.3.5 h1:UZEiaZ55nlXGDL92scoVuw00RmiRCazIEmvPSbSvt8Y= +github.com/segmentio/encoding v0.3.5/go.mod h1:n0JeuIqEQrQoPDGsjo8UNd1iA0U8d8+oHAA4E3G3OxM= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= -github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/texttheater/golang-levenshtein v1.0.1 h1:+cRNoVrfiwufQPhoMzB6N0Yf/Mqajr6t1lOv8GyGE2U= github.com/texttheater/golang-levenshtein v1.0.1/go.mod h1:PYAKrbF5sAiq9wd+H82hs7gNaen0CplQ9uvm6+enD/8= github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= +github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 h1:H8wwQwTe5sL6x30z71lUgNiwBdeCHQjrphCfLwqIHGo= +github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2/go.mod h1:/kR4beFhlz2g+V5ik8jW+3PMiMQAPt29y6K64NNY53c= +github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 h1:3/aHKUq7qaFMWxyQV0W2ryNgg8x8rVeKVA20KJUkfS0= +github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2/go.mod h1:Zit4b8AQXaXvA68+nzmbyDzqiyFRISyw1JiD5JqUBjw= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zclconf/go-cty v1.13.2 h1:4GvrUxe/QUDYuJKAav4EYqdM47/kZa672LwmXFmEKT0= github.com/zclconf/go-cty v1.13.2/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= +go.opentelemetry.io/otel/log v0.6.0 h1:nH66tr+dmEgW5y+F9LanGJUBYPrRgP4g2EkmPE3LeK8= +go.opentelemetry.io/otel/log v0.6.0/go.mod h1:KdySypjQHhP069JX0z/t26VHwa8vSwzgaKmXtIB3fJM= +go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= +go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= +go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +gocloud.dev v0.37.0 h1:XF1rN6R0qZI/9DYjN16Uy0durAmSlf58DHOcb28GPro= +gocloud.dev v0.37.0/go.mod h1:7/O4kqdInCNsc6LqgmuFnS0GRew4XNNYWpA44yQnwco= +gocloud.dev/secrets/hashivault v0.37.0 h1:5ehGtUBP29DFAgAs6bPw7fVSgqQ3TxaoK2xVcLp1x+c= +gocloud.dev/secrets/hashivault v0.37.0/go.mod h1:4ClUWjBfP8wLdGts56acjHz3mWLuATMoH9vi74FjIv8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -207,43 +474,70 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= -golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 h1:LoYXNGAShUG3m/ehNk4iFctuhGX/+R1ZpfJ4/ia80JM= -golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -251,23 +545,37 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -275,40 +583,83 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= -golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240311173647-c811ad7063a7 h1:8EeVk1VKMD+GD/neyEHGmz7pFblqPjHoi+PGQIlLx2s= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240311173647-c811ad7063a7/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= -google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= -google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +google.golang.org/api v0.169.0 h1:QwWPy71FgMWqJN/l6jVlFHUa29a7dcUy02I8o799nPY= +google.golang.org/api v0.169.0/go.mod h1:gpNOiMA2tZ4mf5R9Iwf4rK/Dcz0fbdIgWYWVoxmsyLg= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20240311173647-c811ad7063a7 h1:ImUcDPHjTrAqNhlOkSocDLfG9rrNHH7w7uoKWPaWZ8s= +google.golang.org/genproto v0.0.0-20240311173647-c811ad7063a7/go.mod h1:/3XmxOjePkvmKrHuBy4zNFw7IzxJXtAgdpXi8Ll990U= +google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 h1:M0KvPgPmDZHPlbRbaNU1APr28TvwvvdUPlSv7PUvy8g= +google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:dguCy7UOdZhTvLzDyt15+rOrawrpM4q7DD9dQ1P11P4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 h1:XVhgTWWV3kGQlwJHR3upFWZeTsei6Oks1apkZSeonIE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= lukechampine.com/frand v1.4.2 h1:RzFIpOvkMXuPMBb9maa4ND4wjBn71E1Jpf8BzJHMaVw= lukechampine.com/frand v1.4.2/go.mod h1:4S/TM2ZgrKejMcKMbeLjISpJMO+/eZ1zu3vYX9dtj3s= pgregory.net/rapid v0.6.1 h1:4eyrDxyht86tT4Ztm+kvlyNBLIk071gR+ZQdhphc9dQ= diff --git a/deployments/pulumi/main.go b/deployments/pulumi/main.go index 1e062d20d..972f227bb 100644 --- a/deployments/pulumi/main.go +++ b/deployments/pulumi/main.go @@ -3,17 +3,16 @@ package main import ( "errors" "fmt" - helm "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v3" + "github.com/formancehq/ledger/deployments/pulumi/pkg" "github.com/pulumi/pulumi/sdk/v3/go/pulumi" "github.com/pulumi/pulumi/sdk/v3/go/pulumi/config" ) func main() { - pulumi.Run(deployLedger) + pulumi.Run(deploy) } -func deployLedger(ctx *pulumi.Context) error { - +func deploy(ctx *pulumi.Context) error { conf := config.New(ctx, "") postgresURI := conf.Require("postgres.uri") @@ -42,32 +41,18 @@ func deployLedger(ctx *pulumi.Context) error { replicaCount, _ := conf.TryInt("replicaCount") experimentalFeatures, _ := conf.TryBool("experimentalFeatures") - rel, err := helm.NewRelease(ctx, "ledger", &helm.ReleaseArgs{ - Chart: pulumi.String("../helm"), + _, err = pulumi_ledger.NewComponent(ctx, "ledger", &pulumi_ledger.ComponentArgs{ Namespace: pulumi.String(namespace), - CreateNamespace: pulumi.BoolPtr(true), - Timeout: pulumi.IntPtr(timeout), - Values: pulumi.Map(map[string]pulumi.Input{ - "image": pulumi.Map{ - "repository": pulumi.String("ghcr.io/formancehq/ledger"), - "tag": pulumi.String(version), - "pullPolicy": pulumi.String(imagePullPolicy), - }, - "postgres": pulumi.Map{ - "uri": pulumi.String(postgresURI), - }, - "debug": pulumi.Bool(debug), - "replicaCount": pulumi.Int(replicaCount), - "experimentalFeatures": pulumi.Bool(experimentalFeatures), - }), + Timeout: pulumi.Int(timeout), + Tag: pulumi.String(version), + ImagePullPolicy: pulumi.String(imagePullPolicy), + Postgres: pulumi_ledger.PostgresArgs{ + URI: pulumi.String(postgresURI), + }, + Debug: pulumi.Bool(debug), + ReplicaCount: pulumi.Int(replicaCount), + ExperimentalFeatures: pulumi.Bool(experimentalFeatures), }) - if err != nil { - return err - } - - ctx.Export("service-name", rel.Status.Name()) - ctx.Export("service-namespace", rel.Status.Namespace()) - ctx.Export("service-port", pulumi.Int(8080)) return err } diff --git a/deployments/pulumi/main_test.go b/deployments/pulumi/main_test.go new file mode 100644 index 000000000..c3f21130c --- /dev/null +++ b/deployments/pulumi/main_test.go @@ -0,0 +1,98 @@ +package main + +import ( + "fmt" + "github.com/formancehq/go-libs/v2/logging" + "github.com/google/uuid" + corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1" + "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v3" + metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/meta/v1" + "github.com/pulumi/pulumi/pkg/v3/testing/integration" + "github.com/pulumi/pulumi/sdk/v3/go/auto" + "github.com/pulumi/pulumi/sdk/v3/go/auto/optdestroy" + "github.com/pulumi/pulumi/sdk/v3/go/auto/optup" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" + "github.com/stretchr/testify/require" + "os" + "testing" +) + +func TestProgram(t *testing.T) { + + ctx := logging.TestingContext() + stackName := "ledger-tests-pulumi-" + uuid.NewString()[:8] + + stack, err := auto.UpsertStackInlineSource(ctx, stackName, "ledger-tests-pulumi-postgres", deployPostgres(stackName)) + require.NoError(t, err) + + t.Log("Deploy pg stack") + up, err := stack.Up(ctx, optup.ProgressStreams(os.Stdout), optup.ErrorProgressStreams(os.Stderr)) + require.NoError(t, err) + + t.Cleanup(func() { + t.Log("Destroy stack") + _, err := stack.Destroy(ctx, optdestroy.Remove(), optdestroy.ProgressStreams(os.Stdout), optdestroy.ErrorProgressStreams(os.Stderr)) + require.NoError(t, err) + }) + + postgresURI := up.Outputs["uri"].Value.(string) + + t.Log("Test program") + integration.ProgramTest(t, &integration.ProgramTestOptions{ + Quick: true, + SkipRefresh: true, + Dir: ".", + Config: map[string]string{ + "namespace": stackName, + "postgres.uri": postgresURI, + "timeout": "30", + }, + Stdout: os.Stdout, + Stderr: os.Stderr, + Verbose: testing.Verbose(), + }) +} + +func deployPostgres(stackName string) func(ctx *pulumi.Context) error { + return func(ctx *pulumi.Context) error { + namespace, err := corev1.NewNamespace(ctx, "namespace", &corev1.NamespaceArgs{ + Metadata: &metav1.ObjectMetaArgs{ + Name: pulumi.String(stackName), + }, + }) + if err != nil { + return fmt.Errorf("creating namespace") + } + + rel, err := helm.NewRelease(ctx, "postgres", &helm.ReleaseArgs{ + Chart: pulumi.String("oci://registry-1.docker.io/bitnamicharts/postgresql"), + Version: pulumi.String("16.1.1"), + Namespace: namespace.Metadata.Name(), + Values: pulumi.Map(map[string]pulumi.Input{ + "auth": pulumi.Map{ + "postgresPassword": pulumi.String("postgres"), + "database": pulumi.String("ledger"), + }, + "primary": pulumi.Map{ + "resources": pulumi.Map{ + "requests": pulumi.Map{ + "memory": pulumi.String("256Mi"), + "cpu": pulumi.String("256m"), + }, + }, + }, + }), + CreateNamespace: pulumi.BoolPtr(true), + }) + if err != nil { + return fmt.Errorf("installing release") + } + + ctx.Export("uri", pulumi.Sprintf( + "postgres://postgres:postgres@%s-postgresql.%s:5432/ledger?sslmode=disable", + rel.Status.Name().Elem(), + rel.Status.Namespace().Elem(), + )) + return nil + } +} diff --git a/deployments/pulumi/pkg/component.go b/deployments/pulumi/pkg/component.go new file mode 100644 index 000000000..5964c6855 --- /dev/null +++ b/deployments/pulumi/pkg/component.go @@ -0,0 +1,604 @@ +package pulumi_ledger + +import ( + "fmt" + "github.com/formancehq/go-libs/v2/collectionutils" + appsv1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/apps/v1" + batchv1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/batch/v1" + corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1" + metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/meta/v1" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" + "github.com/pulumi/pulumi/sdk/v3/go/pulumix" + "slices" + "time" +) + +var ErrPostgresURIRequired = fmt.Errorf("postgresURI is required") + +type Component struct { + pulumi.ResourceState + + ServiceName pulumix.Output[string] + ServiceNamespace pulumix.Output[string] + ServicePort pulumix.Output[int] + ServiceInternalURL pulumix.Output[string] + Migrations pulumix.Output[*batchv1.Job] +} + +type PostgresArgs struct { + URI pulumix.Input[string] + AWSEnableIAM pulumix.Input[bool] + MaxIdleConns pulumix.Input[*int] + MaxOpenConns pulumix.Input[*int] + ConnMaxIdleTime pulumix.Input[*time.Duration] +} + +type OtelTracesArgs struct { + OtelTracesBatch pulumix.Input[bool] + OtelTracesExporterFlag pulumix.Input[string] + OtelTracesExporterJaegerEndpoint pulumix.Input[string] + OtelTracesExporterJaegerUser pulumix.Input[string] + OtelTracesExporterJaegerPassword pulumix.Input[string] + OtelTracesExporterOTLPMode pulumix.Input[string] + OtelTracesExporterOTLPEndpoint pulumix.Input[string] + OtelTracesExporterOTLPInsecure pulumix.Input[bool] +} + +type OtelMetricsArgs struct { + OtelMetricsExporterPushInterval pulumix.Input[*time.Duration] + OtelMetricsRuntime pulumix.Input[bool] + OtelMetricsRuntimeMinimumReadMemStatsInterval pulumix.Input[*time.Duration] + OtelMetricsExporter pulumix.Input[string] + OtelMetricsKeepInMemory pulumix.Input[bool] + OtelMetricsExporterOTLPMode pulumix.Input[string] + OtelMetricsExporterOTLPEndpoint pulumix.Input[string] + OtelMetricsExporterOTLPInsecure pulumix.Input[bool] +} + +type OtelArgs struct { + ResourceAttributes pulumix.Input[map[string]string] + ServiceName pulumix.Input[string] + + Traces *OtelTracesArgs + Metrics *OtelMetricsArgs +} + +type ComponentArgs struct { + Postgres PostgresArgs + Otel *OtelArgs + Namespace pulumix.Input[string] + Timeout pulumix.Input[int] + Tag pulumix.Input[string] + ImagePullPolicy pulumix.Input[string] + Debug pulumix.Input[bool] + ReplicaCount pulumix.Input[int] + GracePeriod pulumix.Input[string] + AutoUpgrade pulumix.Input[bool] + WaitUpgrade pulumix.Input[bool] + BallastSizeInBytes pulumix.Input[int] + NumscriptCacheMaxCount pulumix.Input[int] + BulkMaxSize pulumix.Input[int] + BulkParallel pulumix.Input[int] + TerminationGracePeriodSeconds pulumix.Input[*int] + + ExperimentalFeatures pulumix.Input[bool] + ExperimentalNumscriptInterpreter pulumix.Input[bool] +} + +func NewComponent(ctx *pulumi.Context, name string, args *ComponentArgs, opts ...pulumi.ResourceOption) (*Component, error) { + cmp := &Component{} + err := ctx.RegisterComponentResource("Formance:Ledger", name, cmp, opts...) + if err != nil { + return nil, err + } + + namespace := pulumix.Val[string]("") + if args.Namespace != nil { + namespace = args.Namespace.ToOutput(ctx.Context()) + } + + if args.Postgres.URI == nil { + return nil, ErrPostgresURIRequired + } + postgresURI := pulumix.ApplyErr(args.Postgres.URI, func(postgresURI string) (string, error) { + if postgresURI == "" { + return "", ErrPostgresURIRequired + } + + return postgresURI, nil + }) + + debug := pulumix.Val(true) + if args.Debug != nil { + debug = args.Debug.ToOutput(ctx.Context()) + } + + gracePeriod := pulumix.Val("0s") + if args.GracePeriod != nil { + gracePeriod = args.GracePeriod.ToOutput(ctx.Context()) + } + + tag := pulumix.Val("latest") + if args.Tag != nil { + tag = pulumix.Apply(args.Tag, func(tag string) string { + if tag == "" { + return "latest" + } + return tag + }) + } + ledgerImage := pulumi.Sprintf("ghcr.io/formancehq/ledger:%s", tag) + + autoUpgrade := pulumix.Val(true) + if args.AutoUpgrade != nil { + autoUpgrade = args.AutoUpgrade.ToOutput(ctx.Context()) + } + + waitUpgrade := pulumix.Val(true) + if args.WaitUpgrade != nil { + waitUpgrade = args.WaitUpgrade.ToOutput(ctx.Context()) + } + + imagePullPolicy := pulumix.Val("") + if args.ImagePullPolicy != nil { + imagePullPolicy = args.ImagePullPolicy.ToOutput(ctx.Context()) + } + + envVars := corev1.EnvVarArray{ + corev1.EnvVarArgs{ + Name: pulumi.String("POSTGRES_URI"), + Value: postgresURI.Untyped().(pulumi.StringOutput), + }, + corev1.EnvVarArgs{ + Name: pulumi.String("BIND"), + Value: pulumi.String(":8080"), + }, + corev1.EnvVarArgs{ + Name: pulumi.String("DEBUG"), + Value: boolToString(debug).Untyped().(pulumi.StringOutput), + }, + } + + if otel := args.Otel; otel != nil { + if otel.ServiceName != nil { + envVars = append(envVars, corev1.EnvVarArgs{ + Name: pulumi.String("OTEL_SERVICE_NAME"), + Value: otel.ServiceName.ToOutput(ctx.Context()).Untyped().(pulumi.StringOutput), + }) + } + if otel.ResourceAttributes != nil { + envVars = append(envVars, corev1.EnvVarArgs{ + Name: pulumi.String("OTEL_RESOURCE_ATTRIBUTES"), + Value: pulumi.All(otel.ResourceAttributes).ApplyT(func(v []map[string]string) string { + ret := "" + keys := collectionutils.Keys(v[0]) + slices.Sort(keys) + for _, key := range keys { + ret += key + "=" + v[0][key] + "," + } + if len(ret) > 0 { + ret = ret[:len(ret)-1] + } + return ret + }).(pulumi.StringOutput), + }) + } + if traces := args.Otel.Traces; traces != nil { + if traces.OtelTracesBatch != nil { + envVars = append(envVars, corev1.EnvVarArgs{ + Name: pulumi.String("OTEL_TRACES_BATCH"), + Value: boolToString(traces.OtelTracesBatch).Untyped().(pulumi.StringOutput), + }) + } + if traces.OtelTracesExporterFlag != nil { + envVars = append(envVars, corev1.EnvVarArgs{ + Name: pulumi.String("OTEL_TRACES_EXPORTER"), + Value: traces.OtelTracesExporterFlag.ToOutput(ctx.Context()).Untyped().(pulumi.StringOutput), + }) + } + if traces.OtelTracesExporterJaegerEndpoint != nil { + envVars = append(envVars, corev1.EnvVarArgs{ + Name: pulumi.String("OTEL_TRACES_EXPORTER_JAEGER_ENDPOINT"), + Value: traces.OtelTracesExporterJaegerEndpoint.ToOutput(ctx.Context()).Untyped().(pulumi.StringOutput), + }) + } + if traces.OtelTracesExporterJaegerUser != nil { + envVars = append(envVars, corev1.EnvVarArgs{ + Name: pulumi.String("OTEL_TRACES_EXPORTER_JAEGER_USER"), + Value: traces.OtelTracesExporterJaegerUser.ToOutput(ctx.Context()).Untyped().(pulumi.StringOutput), + }) + } + if traces.OtelTracesExporterJaegerPassword != nil { + envVars = append(envVars, corev1.EnvVarArgs{ + Name: pulumi.String("OTEL_TRACES_EXPORTER_JAEGER_PASSWORD"), + Value: traces.OtelTracesExporterJaegerPassword.ToOutput(ctx.Context()).Untyped().(pulumi.StringOutput), + }) + } + if traces.OtelTracesExporterOTLPMode != nil { + envVars = append(envVars, corev1.EnvVarArgs{ + Name: pulumi.String("OTEL_TRACES_EXPORTER_OTLP_MODE"), + Value: traces.OtelTracesExporterOTLPMode.ToOutput(ctx.Context()).Untyped().(pulumi.StringOutput), + }) + } + if traces.OtelTracesExporterOTLPEndpoint != nil { + envVars = append(envVars, corev1.EnvVarArgs{ + Name: pulumi.String("OTEL_TRACES_EXPORTER_OTLP_ENDPOINT"), + Value: traces.OtelTracesExporterOTLPEndpoint.ToOutput(ctx.Context()).Untyped().(pulumi.StringOutput), + }) + } + if traces.OtelTracesExporterOTLPInsecure != nil { + envVars = append(envVars, corev1.EnvVarArgs{ + Name: pulumi.String("OTEL_TRACES_EXPORTER_OTLP_INSECURE"), + Value: boolToString(traces.OtelTracesExporterOTLPInsecure).Untyped().(pulumi.StringOutput), + }) + } + } + + if metrics := args.Otel.Metrics; metrics != nil { + if metrics.OtelMetricsExporterPushInterval != nil { + envVars = append(envVars, corev1.EnvVarArgs{ + Name: pulumi.String("OTEL_METRICS_EXPORTER_PUSH_INTERVAL"), + Value: pulumix.Apply(metrics.OtelMetricsExporterPushInterval, func(pushInterval *time.Duration) string { + if pushInterval == nil { + return "" + } + return pushInterval.String() + }).Untyped().(pulumi.StringOutput), + }) + } + if metrics.OtelMetricsRuntime != nil { + envVars = append(envVars, corev1.EnvVarArgs{ + Name: pulumi.String("OTEL_METRICS_RUNTIME"), + Value: boolToString(metrics.OtelMetricsRuntime).Untyped().(pulumi.StringOutput), + }) + } + if metrics.OtelMetricsRuntimeMinimumReadMemStatsInterval != nil { + envVars = append(envVars, corev1.EnvVarArgs{ + Name: pulumi.String("OTEL_METRICS_RUNTIME_MINIMUM_READ_MEM_STATS_INTERVAL"), + Value: pulumix.Apply(metrics.OtelMetricsRuntimeMinimumReadMemStatsInterval, func(interval *time.Duration) string { + if interval == nil { + return "" + } + return interval.String() + }).Untyped().(pulumi.StringOutput), + }) + } + if metrics.OtelMetricsExporter != nil { + envVars = append(envVars, corev1.EnvVarArgs{ + Name: pulumi.String("OTEL_METRICS_EXPORTER"), + Value: metrics.OtelMetricsExporter.ToOutput(ctx.Context()).Untyped().(pulumi.StringOutput), + }) + } + if metrics.OtelMetricsKeepInMemory != nil { + envVars = append(envVars, corev1.EnvVarArgs{ + Name: pulumi.String("OTEL_METRICS_KEEP_IN_MEMORY"), + Value: boolToString(metrics.OtelMetricsKeepInMemory).Untyped().(pulumi.StringOutput), + }) + } + if metrics.OtelMetricsExporterOTLPMode != nil { + envVars = append(envVars, corev1.EnvVarArgs{ + Name: pulumi.String("OTEL_METRICS_EXPORTER_OTLP_MODE"), + Value: metrics.OtelMetricsExporterOTLPMode.ToOutput(ctx.Context()).Untyped().(pulumi.StringOutput), + }) + } + if metrics.OtelMetricsExporterOTLPEndpoint != nil { + envVars = append(envVars, corev1.EnvVarArgs{ + Name: pulumi.String("OTEL_METRICS_EXPORTER_OTLP_ENDPOINT"), + Value: metrics.OtelMetricsExporterOTLPEndpoint.ToOutput(ctx.Context()).Untyped().(pulumi.StringOutput), + }) + } + if metrics.OtelMetricsExporterOTLPInsecure != nil { + envVars = append(envVars, corev1.EnvVarArgs{ + Name: pulumi.String("OTEL_METRICS_EXPORTER_OTLP_INSECURE"), + Value: boolToString(metrics.OtelMetricsExporterOTLPInsecure).Untyped().(pulumi.StringOutput), + }) + } + } + } + + if args.BulkMaxSize != nil { + envVars = append(envVars, corev1.EnvVarArgs{ + Name: pulumi.String("BULK_MAX_SIZE"), + Value: pulumix.Apply(args.BulkMaxSize, func(size int) string { + if size == 0 { + return "" + } + return fmt.Sprint(size) + }).Untyped().(pulumi.StringOutput), + }) + } + + if args.BallastSizeInBytes != nil { + envVars = append(envVars, corev1.EnvVarArgs{ + Name: pulumi.String("BALLAST_SIZE"), + Value: pulumix.Apply(args.BallastSizeInBytes, func(size int) string { + if size == 0 { + return "" + } + return fmt.Sprint(size) + }).Untyped().(pulumi.StringOutput), + }) + } + + if args.BulkParallel != nil { + envVars = append(envVars, corev1.EnvVarArgs{ + Name: pulumi.String("BULK_PARALLEL"), + Value: pulumix.Apply(args.BulkParallel, func(size int) string { + if size == 0 { + return "" + } + return fmt.Sprint(size) + }).Untyped().(pulumi.StringOutput), + }) + } + + if args.NumscriptCacheMaxCount != nil { + envVars = append(envVars, corev1.EnvVarArgs{ + Name: pulumi.String("NUMSCRIPT_CACHE_MAX_COUNT"), + Value: pulumix.Apply(args.NumscriptCacheMaxCount, func(size int) string { + if size == 0 { + return "" + } + return fmt.Sprint(size) + }).Untyped().(pulumi.StringOutput), + }) + } + + if args.ExperimentalNumscriptInterpreter != nil { + envVars = append(envVars, corev1.EnvVarArgs{ + Name: pulumi.String("EXPERIMENTAL_NUMSCRIPT_INTERPRETER"), + Value: boolToString(args.ExperimentalNumscriptInterpreter).Untyped().(pulumi.StringOutput), + }) + } + + if args.AutoUpgrade != nil { + envVars = append(envVars, corev1.EnvVarArgs{ + Name: pulumi.String("AUTO_UPGRADE"), + Value: pulumix.Apply2Err(autoUpgrade, waitUpgrade, func(autoUpgrade, waitUpgrade bool) (string, error) { + if waitUpgrade && !autoUpgrade { + return "", fmt.Errorf("waitUpgrade requires autoUpgrade to be true") + } + if !autoUpgrade { + return "false", nil + } + return "true", nil + }).Untyped().(pulumi.StringOutput), + }) + } + + if args.ExperimentalFeatures != nil { + envVars = append(envVars, corev1.EnvVarArgs{ + Name: pulumi.String("EXPERIMENTAL_FEATURES"), + Value: boolToString(args.ExperimentalFeatures).Untyped().(pulumi.StringOutput), + }) + } + + if args.GracePeriod != nil { + // https://freecontent.manning.com/handling-client-requests-properly-with-kubernetes/ + envVars = append(envVars, corev1.EnvVarArgs{ + Name: pulumi.String("GRACE_PERIOD"), + Value: gracePeriod.Untyped().(pulumi.StringOutput), + }) + } + + if args.Postgres.AWSEnableIAM != nil { + envVars = append(envVars, corev1.EnvVarArgs{ + Name: pulumi.String("POSTGRES_AWS_ENABLE_IAM"), + Value: boolToString(args.Postgres.AWSEnableIAM).Untyped().(pulumi.StringOutput), + }) + } + + if args.Postgres.ConnMaxIdleTime != nil { + envVars = append(envVars, corev1.EnvVarArgs{ + Name: pulumi.String("POSTGRES_CONN_MAX_IDLE_TIME"), + Value: pulumix.Apply(args.Postgres.ConnMaxIdleTime, func(connMaxIdleTime *time.Duration) string { + if connMaxIdleTime == nil { + return "" + } + return connMaxIdleTime.String() + }).Untyped().(pulumi.StringOutput), + }) + } + + if args.Postgres.MaxOpenConns != nil { + envVars = append(envVars, corev1.EnvVarArgs{ + Name: pulumi.String("POSTGRES_MAX_OPEN_CONNS"), + Value: pulumix.Apply(args.Postgres.MaxOpenConns, func(maxOpenConns *int) string { + if maxOpenConns == nil { + return "" + } + return fmt.Sprint(*maxOpenConns) + }).Untyped().(pulumi.StringOutput), + }) + } + + if args.Postgres.MaxIdleConns != nil { + envVars = append(envVars, corev1.EnvVarArgs{ + Name: pulumi.String("POSTGRES_MAX_IDLE_CONNS"), + Value: pulumix.Apply(args.Postgres.MaxIdleConns, func(maxIdleConns *int) string { + if maxIdleConns == nil { + return "" + } + return fmt.Sprint(*maxIdleConns) + }).Untyped().(pulumi.StringOutput), + }) + } + + terminationGracePeriodSeconds := pulumi.IntPtrFromPtr(nil) + if args.TerminationGracePeriodSeconds != nil { + terminationGracePeriodSeconds = args.TerminationGracePeriodSeconds.ToOutput(ctx.Context()).Untyped().(pulumi.IntPtrOutput) + } + + deployment, err := appsv1.NewDeployment(ctx, "ledger", &appsv1.DeploymentArgs{ + Metadata: &metav1.ObjectMetaArgs{ + Namespace: namespace.Untyped().(pulumi.StringOutput), + Labels: pulumi.StringMap{ + "com.formance.stack/app": pulumi.String("ledger"), + }, + }, + Spec: appsv1.DeploymentSpecArgs{ + Replicas: pulumi.Int(1), + Selector: &metav1.LabelSelectorArgs{ + MatchLabels: pulumi.StringMap{ + "com.formance.stack/app": pulumi.String("ledger"), + }, + }, + Template: &corev1.PodTemplateSpecArgs{ + Metadata: &metav1.ObjectMetaArgs{ + Labels: pulumi.StringMap{ + "com.formance.stack/app": pulumi.String("ledger"), + }, + }, + Spec: corev1.PodSpecArgs{ + TerminationGracePeriodSeconds: terminationGracePeriodSeconds, + Containers: corev1.ContainerArray{ + corev1.ContainerArgs{ + Name: pulumi.String("ledger"), + Image: ledgerImage, + ImagePullPolicy: imagePullPolicy.ToOutput(ctx.Context()).Untyped().(pulumi.StringOutput), + Args: pulumi.StringArray{ + pulumi.String("serve"), + }, + Ports: corev1.ContainerPortArray{ + corev1.ContainerPortArgs{ + ContainerPort: pulumi.Int(8080), + Name: pulumi.String("http"), + Protocol: pulumi.String("TCP"), + }, + }, + LivenessProbe: corev1.ProbeArgs{ + HttpGet: corev1.HTTPGetActionArgs{ + Path: pulumi.String("/_healthcheck"), + Port: pulumi.String("http"), + }, + FailureThreshold: pulumi.Int(1), + PeriodSeconds: pulumi.Int(10), + }, + ReadinessProbe: corev1.ProbeArgs{ + HttpGet: corev1.HTTPGetActionArgs{ + Path: pulumi.String("/_healthcheck"), + Port: pulumi.String("http"), + }, + FailureThreshold: pulumi.Int(1), + PeriodSeconds: pulumi.Int(10), + }, + StartupProbe: corev1.ProbeArgs{ + HttpGet: corev1.HTTPGetActionArgs{ + Path: pulumi.String("/_healthcheck"), + Port: pulumi.String("http"), + }, + FailureThreshold: pulumi.Int(60), + PeriodSeconds: pulumi.Int(5), + }, + Env: envVars, + }, + }, + }, + }, + }, + }, pulumi.Parent(cmp)) + if err != nil { + return nil, err + } + + cmp.Migrations = pulumix.ApplyErr(waitUpgrade, func(waitUpgrade bool) (*batchv1.Job, error) { + if !waitUpgrade { + return nil, nil + } + return batchv1.NewJob(ctx, "wait-migration-completion", &batchv1.JobArgs{ + Metadata: &metav1.ObjectMetaArgs{ + Namespace: namespace.Untyped().(pulumi.StringOutput), + }, + Spec: batchv1.JobSpecArgs{ + Template: corev1.PodTemplateSpecArgs{ + Spec: corev1.PodSpecArgs{ + RestartPolicy: pulumi.String("OnFailure"), + Containers: corev1.ContainerArray{ + corev1.ContainerArgs{ + Name: pulumi.String("check"), + Args: pulumi.StringArray{ + pulumi.String("migrate"), + }, + Image: ledgerImage, + ImagePullPolicy: imagePullPolicy.ToOutput(ctx.Context()).Untyped().(pulumi.StringOutput), + Env: corev1.EnvVarArray{ + corev1.EnvVarArgs{ + Name: pulumi.String("POSTGRES_URI"), + Value: postgresURI.Untyped().(pulumi.StringOutput), + }, + corev1.EnvVarArgs{ + Name: pulumi.String("DEBUG"), + Value: boolToString(debug).Untyped().(pulumi.StringOutput), + }, + }, + }, + }, + }, + }, + }, + }, pulumi.Parent(cmp)) + }) + + service, err := corev1.NewService(ctx, "ledger", &corev1.ServiceArgs{ + Metadata: &metav1.ObjectMetaArgs{ + Namespace: namespace.Untyped().(pulumi.StringOutput), + }, + Spec: &corev1.ServiceSpecArgs{ + Selector: deployment.Spec.Selector().MatchLabels(), + Type: pulumi.String("ClusterIP"), + Ports: corev1.ServicePortArray{ + corev1.ServicePortArgs{ + Port: pulumi.Int(8080), + TargetPort: pulumi.Int(8080), + Protocol: pulumi.String("TCP"), + Name: pulumi.String("http"), + }, + }, + }, + }, pulumi.Parent(cmp)) + if err != nil { + return nil, err + } + + cmp.ServiceName = pulumix.Apply(service.Metadata.Name().ToStringPtrOutput(), func(name *string) string { + if name == nil { + return "" + } + return *name + }) + cmp.ServiceNamespace = pulumix.Apply(service.Metadata.Namespace().ToStringPtrOutput(), func(namespace *string) string { + if namespace == nil { + return "" + } + return *namespace + }) + cmp.ServicePort = pulumix.Val(8080) + cmp.ServiceInternalURL = pulumix.Apply(pulumi.Sprintf( + "http://%s.%s.svc.cluster.local:%d", + cmp.ServiceName, + cmp.ServiceNamespace, + cmp.ServicePort, + ), func(url string) string { + return url + }) + + if err := ctx.RegisterResourceOutputs(cmp, pulumi.Map{ + "service-name": cmp.ServiceName, + "service-namespace": cmp.ServiceNamespace, + "service-port": cmp.ServicePort, + "service-internal-url": cmp.ServiceInternalURL, + }); err != nil { + return nil, fmt.Errorf("registering resource outputs: %w", err) + } + + return cmp, nil +} + +func boolToString(output pulumix.Input[bool]) pulumix.Output[string] { + return pulumix.Apply(output, func(v bool) string { + if v { + return "true" + } + return "false" + }) +} diff --git a/pkg/generate/set.go b/pkg/generate/set.go index 17246a8b8..610e8542e 100644 --- a/pkg/generate/set.go +++ b/pkg/generate/set.go @@ -6,9 +6,12 @@ import ( "fmt" "github.com/formancehq/go-libs/v2/collectionutils" "github.com/formancehq/go-libs/v2/logging" + "github.com/formancehq/go-libs/v2/pointer" "github.com/formancehq/ledger/pkg/client" "github.com/formancehq/ledger/pkg/client/models/components" + "github.com/formancehq/ledger/pkg/client/models/operations" "golang.org/x/sync/errgroup" + "math/big" ) type GeneratorSet struct { @@ -53,30 +56,54 @@ func (s *GeneratorSet) Run(ctx context.Context) error { } return fmt.Errorf("iteration %d/%d failed: %w", vu, iteration, err) } - maxLogID := collectionutils.Reduce(ret, func(acc int64, r components.V2BulkElementResult) int64 { - var logID int64 - switch r.Type { - case components.V2BulkElementResultTypeCreateTransaction: - logID = r.V2BulkElementResultCreateTransaction.LogID - case components.V2BulkElementResultTypeAddMetadata: - logID = r.V2BulkElementResultAddMetadata.LogID - case components.V2BulkElementResultTypeDeleteMetadata: - logID = r.V2BulkElementResultDeleteMetadata.LogID - case components.V2BulkElementResultTypeRevertTransaction: - logID = r.V2BulkElementResultRevertTransaction.LogID - default: - panic(fmt.Sprintf("unexpected result type: %s", r.Type)) - } - if logID > acc { - return logID - } - return acc - }, 0) + if s.untilLogID != 0 { + maxLogID := collectionutils.Reduce(ret, func(acc int64, r components.V2BulkElementResult) int64 { + var logID int64 + switch r.Type { + case components.V2BulkElementResultTypeCreateTransaction: + logID = r.V2BulkElementResultCreateTransaction.LogID + case components.V2BulkElementResultTypeAddMetadata: + logID = r.V2BulkElementResultAddMetadata.LogID + case components.V2BulkElementResultTypeDeleteMetadata: + logID = r.V2BulkElementResultDeleteMetadata.LogID + case components.V2BulkElementResultTypeRevertTransaction: + logID = r.V2BulkElementResultRevertTransaction.LogID + case components.V2BulkElementResultTypeError: + panic(fmt.Sprintf("unexpected error: %s [%s]", r.V2BulkElementResultError.ErrorDescription, r.V2BulkElementResultError.ErrorCode)) + default: + panic(fmt.Sprintf("unexpected result type: %s", r.Type)) + } + + if logID > acc { + return logID + } + return acc + }, 0) - if s.untilLogID != 0 && uint64(maxLogID) >= s.untilLogID { - return nil + if maxLogID == 0 { // version < 2.2.0 + // notes(gfyrag): avoid list logs for each parallel runner by checking only on the first vu + if vu == 0 { + logs, err := s.client.Ledger.V2.ListLogs(ctx, operations.V2ListLogsRequest{ + Ledger: s.targetedLedger, + PageSize: pointer.For(int64(1)), + }) + if err != nil { + return fmt.Errorf("failed to list logs: %w", err) + } + if logs.V2LogsCursorResponse.Cursor.Data[0].ID.Cmp(big.NewInt(int64(s.untilLogID))) > 0 { + logging.FromContext(ctx).Infof("Log %s reached, stopping generator", logs.V2LogsCursorResponse.Cursor.Data[0].ID.String()) + return nil + } + } + } else { + if uint64(maxLogID) >= s.untilLogID { + logging.FromContext(ctx).Infof("Log %d reached, stopping generator", maxLogID) + return nil + } + } } + iteration++ } }) diff --git a/test/rolling-upgrades/Earthfile b/test/rolling-upgrades/Earthfile deleted file mode 100644 index d7c87f0de..000000000 --- a/test/rolling-upgrades/Earthfile +++ /dev/null @@ -1,129 +0,0 @@ -VERSION 0.8 -PROJECT FormanceHQ/ledger - -IMPORT github.com/formancehq/earthly:tags/v0.19.0 AS core - -FROM core+base-image - -CACHE --sharing=shared --id go-mod-cache /go/pkg/mod -CACHE --sharing=shared --id go-cache /root/.cache/go-build - -image-test: - ARG REPOSITORY=ghcr.io - ARG TAG=latest - FROM --pass-args ../../tools/generator+build-image - - DO --pass-args core+SAVE_IMAGE --COMPONENT=ledger --REPOSITORY=${REPOSITORY} --tag=$TAG - -image-main: - ARG REPOSITORY=ghcr.io - ARG TAG - BUILD --pass-args github.com/formancehq/ledger:main+build-image --tag=$TAG - -image-current: - ARG REPOSITORY=ghcr.io - ARG TAG - BUILD --pass-args ../..+build-image --tag=$TAG - -sources: - FROM core+builder-image - COPY ../..+sources/src /src - WORKDIR /src/test/rolling-upgrades - COPY go.* *.go . - - SAVE ARTIFACT /src - -tidy: - FROM +sources - CACHE --id go-mod-cache /go/pkg/mod - CACHE --id go-cache /root/.cache/go-build - RUN go mod tidy - - SAVE ARTIFACT go.mod AS LOCAL go.mod - SAVE ARTIFACT go.sum AS LOCAL go.sum - -cluster-create: - FROM core+builder-image - RUN apk update && \ - apk add --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community kubectl && \ - apk add --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community kustomize && \ - apk add helm git jq - RUN --secret KUBE_APISERVER kubectl config set clusters.default.server ${KUBE_APISERVER} - RUN kubectl config set clusters.default.insecure-skip-tls-verify true - RUN --secret KUBE_TOKEN kubectl config set-credentials default --token=${KUBE_TOKEN} - RUN kubectl config set-context default --cluster=default --user=default - RUN kubectl config use-context default - RUN apk update && apk add curl docker - ARG TARGETARCH - RUN curl -L -o vcluster "https://github.com/loft-sh/vcluster/releases/download/v0.20.4/vcluster-linux-${TARGETARCH}" - RUN install -c -m 0755 vcluster /usr/local/bin && rm -f vcluster - ARG CLUSTER_NAME=test - RUN vcluster create $CLUSTER_NAME --connect=false --upgrade - -run: - ARG CLUSTER_NAME=test-rolling-upgrades - WAIT - BUILD --pass-args +cluster-create - BUILD +image-test --TAG=$CLUSTER_NAME-rolling-upgrade-test - BUILD +image-main --TAG=$CLUSTER_NAME-main - BUILD +image-current --TAG=$CLUSTER_NAME-current - END - - FROM --pass-args +cluster-create - RUN curl -fsSL https://get.pulumi.com | sh -s -- --version - ENV PATH=$PATH:/root/.pulumi/bin - - CACHE --id go-mod-cache /go/pkg/mod - CACHE --id go-cache /root/.cache/go-build - - COPY +sources/src /src - COPY ../../deployments/pulumi+sources/src /src/deployments/pulumi - COPY ../../deployments/helm+sources/src /src/deployments/helm - - WORKDIR /src/test/rolling-upgrades - COPY go.* *.go . - - ARG NO_CLEANUP=false - ARG NO_CLEANUP_ON_FAILURE=false - - WITH DOCKER - RUN --secret PULUMI_ACCESS_TOKEN --secret GITHUB_TOKEN sh -c ' - set -e; - - echo "Connecting to VCluster..." - vcluster connect ${CLUSTER_NAME} --namespace vcluster-${CLUSTER_NAME}; - - echo "Connected on context '$(kubectl config current-context)'"; - - echo "Waiting for VCluster to be ready..." - until kubectl get nodes; do sleep 1s; done; - - echo "Running test..." - go test \ - --test-image ghcr.io/formancehq/ledger:$CLUSTER_NAME-rolling-upgrade-test \ - --latest-version $CLUSTER_NAME-main \ - --actual-version $CLUSTER_NAME-current \ - --project ledger \ - --stack-prefix-name $CLUSTER_NAME- \ - --no-cleanup=$NO_CLEANUP \ - --no-cleanup-on-failure=$NO_CLEANUP_ON_FAILURE; - ' - END - - IF [ $NO_CLEANUP = "false" ] - RUN vcluster delete $CLUSTER_NAME --delete-namespace - END - -lint: - FROM +tidy - CACHE --id go-mod-cache /go/pkg/mod - CACHE --id go-cache /root/.cache/go-build - CACHE --id golangci-cache /root/.cache/golangci-lint - - RUN golangci-lint run --fix --build-tags it --timeout 5m - - SAVE ARTIFACT main_test.go AS LOCAL main_test.go - -pre-commit: - BUILD +tidy - BUILD +lint \ No newline at end of file diff --git a/test/rolling-upgrades/README.md b/test/rolling-upgrades/README.md deleted file mode 100644 index 00beb02a3..000000000 --- a/test/rolling-upgrades/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# Rolling upgrade tests - -This directory contains tests for rolling upgrades on K8S. - -## Running the tests - -To run the tests, you need to have a K8S cluster running. You can use the `k3d` tool to create a local K8S cluster. - -You need also a Pulumi access token to run the tests. -You can create one by following the instructions [here](https://www.pulumi.com/docs/pulumi-cloud/access-management/access-tokens/). - -### Install k3d - -```bash -curl -s https://raw.githubusercontent.com/rancher/k3d/main/install.sh | bash -``` - -### Create a K8S cluster - -```bash -k3d cluster create -``` - -### Run the tests - -```bash -kubectl create serviceaccount testing -kubectl create clusterrolebinding testing --clusterrole=cluster-admin --serviceaccount=default:testing - -export KUBE_TOKEN=$(kubectl create token testing --duration=999999h) -export KUBE_APISERVER=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}') -export PULUMI_ACCESS_TOKEN= - -earthly --push --no-output +run \ - --KUBE_APISERVER=$KUBE_APISERVER \ - --KUBE_TOKEN=$KUBE_TOKEN \ - --PULUMI_ACCESS_TOKEN=$PULUMI_ACCESS_TOKEN -``` - -### Delete the K8S cluster - -```bash -k3d cluster delete -``` - -## Test description - -The test : -* creates a K8S deployment with a single replica of the server -* then create a test pod in charge of sending requests to the web server and checking if the response is ok. -* then updates the deployment with a new image and waits for the new pod to be ready. -* then checks if the test pod is still alive. If alive, it indicates no errors during the rolling upgrade. - -Under the hood, the test will create a [VCluster](https://www.vcluster.com/docs/get-started) on your k3d cluster. -This VCluster will be used to run the tests and simulate a real rolling upgrade on a K8S cluster. diff --git a/test/rolling-upgrades/go.mod b/test/rolling-upgrades/go.mod deleted file mode 100644 index 548662974..000000000 --- a/test/rolling-upgrades/go.mod +++ /dev/null @@ -1,121 +0,0 @@ -module github.com/formancehq/ledger/test/rolling-upgrades - -go 1.23 - -toolchain go1.23.3 - -replace github.com/formancehq/ledger/pkg/client => ../../pkg/client - -replace github.com/formancehq/ledger => ../.. - -require ( - github.com/formancehq/go-libs/v2 v2.0.1-0.20241121194732-b79c48b683f2 - github.com/formancehq/ledger v0.0.0-00010101000000-000000000000 - github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.12.0 - github.com/pulumi/pulumi/sdk/v3 v3.117.0 - github.com/stretchr/testify v1.10.0 -) - -require ( - dario.cat/mergo v1.0.1 // indirect - github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/ProtonMail/go-crypto v1.0.0 // indirect - github.com/ThreeDotsLabs/watermill v1.4.1 // indirect - github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect - github.com/agext/levenshtein v1.2.3 // indirect - github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect - github.com/atotto/clipboard v0.1.4 // indirect - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/blang/semver v3.5.1+incompatible // indirect - github.com/charmbracelet/bubbles v0.16.1 // indirect - github.com/charmbracelet/bubbletea v0.24.2 // indirect - github.com/charmbracelet/lipgloss v0.7.1 // indirect - github.com/cheggaaa/pb v1.0.29 // indirect - github.com/cloudflare/circl v1.3.7 // indirect - github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect - github.com/cyphar/filepath-securejoin v0.2.4 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/djherbis/times v1.5.0 // indirect - github.com/emirpasic/gods v1.18.1 // indirect - github.com/fatih/color v1.18.0 // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.5.0 // indirect - github.com/go-git/go-git/v5 v5.12.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/glog v1.2.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-hclog v1.6.3 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/hcl/v2 v2.17.0 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect - github.com/kevinburke/ssh_config v1.2.0 // indirect - github.com/lithammer/shortuuid/v3 v3.0.7 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/mitchellh/go-ps v1.0.0 // indirect - github.com/mitchellh/go-wordwrap v1.0.1 // indirect - github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect - github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/reflow v0.3.0 // indirect - github.com/muesli/termenv v0.15.2 // indirect - github.com/nxadm/tail v1.4.11 // indirect - github.com/oklog/ulid v1.3.1 // indirect - github.com/opentracing/basictracer-go v1.1.0 // indirect - github.com/opentracing/opentracing-go v1.2.0 // indirect - github.com/pgavlin/fx v0.1.6 // indirect - github.com/pjbgf/sha1cd v0.3.0 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/pkg/term v1.1.0 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 // indirect - github.com/pulumi/esc v0.6.2 // indirect - github.com/rivo/uniseg v0.4.4 // indirect - github.com/rogpeppe/go-internal v1.11.0 // indirect - github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect - github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 // indirect - github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/skeema/knownhosts v1.2.2 // indirect - github.com/spf13/cast v1.4.1 // indirect - github.com/spf13/cobra v1.8.1 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/texttheater/golang-levenshtein v1.0.1 // indirect - github.com/tweekmonster/luser v0.0.0-20161003172636-3fa38070dbd7 // indirect - github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect - github.com/uber/jaeger-lib v2.4.1+incompatible // indirect - github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 // indirect - github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 // indirect - github.com/xanzy/ssh-agent v0.3.3 // indirect - github.com/zclconf/go-cty v1.13.2 // indirect - go.opentelemetry.io/otel v1.32.0 // indirect - go.opentelemetry.io/otel/log v0.8.0 // indirect - go.opentelemetry.io/otel/metric v1.32.0 // indirect - go.opentelemetry.io/otel/trace v1.32.0 // indirect - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.29.0 // indirect - golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect - golang.org/x/net v0.31.0 // indirect - golang.org/x/sync v0.9.0 // indirect - golang.org/x/sys v0.27.0 // indirect - golang.org/x/term v0.26.0 // indirect - golang.org/x/text v0.20.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 // indirect - google.golang.org/grpc v1.68.0 // indirect - google.golang.org/protobuf v1.35.2 // indirect - gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect - gopkg.in/warnings.v0 v0.1.2 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - lukechampine.com/frand v1.4.2 // indirect -) diff --git a/test/rolling-upgrades/go.sum b/test/rolling-upgrades/go.sum deleted file mode 100644 index 9d7b0bd69..000000000 --- a/test/rolling-upgrades/go.sum +++ /dev/null @@ -1,373 +0,0 @@ -dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= -dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= -github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= -github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= -github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= -github.com/ThreeDotsLabs/watermill v1.4.1 h1:gjP6yZH+otMPjV0KsV07pl9TeMm9UQV/gqiuiuG5Drs= -github.com/ThreeDotsLabs/watermill v1.4.1/go.mod h1:lBnrLbxOjeMRgcJbv+UiZr8Ylz8RkJ4m6i/VN/Nk+to= -github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= -github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= -github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= -github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= -github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= -github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= -github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= -github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= -github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= -github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= -github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= -github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= -github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY= -github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= -github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= -github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= -github.com/cheggaaa/pb v1.0.29 h1:FckUN5ngEk2LpvuG0fw1GEFx6LtyY2pWI/Z2QgCnEYo= -github.com/cheggaaa/pb v1.0.29/go.mod h1:W40334L7FMC5JKWldsTWbdGjLo0RxUKK73K+TuPxX30= -github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= -github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= -github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= -github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= -github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= -github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/djherbis/times v1.5.0 h1:79myA211VwPhFTqUk8xehWrsEO+zcIZj0zT8mXPVARU= -github.com/djherbis/times v1.5.0/go.mod h1:5q7FDLvbNg1L/KaBmPcWlVR9NmoKo3+ucqUA3ijQhA0= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= -github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= -github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241121194732-b79c48b683f2 h1:JsacSDe6MYGCm04ZY/VJyYTUAWg8jhOBZm6AGyErF/I= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241121194732-b79c48b683f2/go.mod h1:ZGHVcAC54ZbPgeoGKy/d31CHy94RMhHQlX+Vui15ST8= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= -github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= -github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= -github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= -github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= -github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY= -github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU= -github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= -github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/hcl/v2 v2.17.0 h1:z1XvSUyXd1HP10U4lrLg5e0JMVz6CPaJvAgxM0KNZVY= -github.com/hashicorp/hcl/v2 v2.17.0/go.mod h1:gJyW2PTShkJqQBKpAmPO3yxMxIuoXkOF2TpqXzrQyx4= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= -github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8= -github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= -github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= -github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= -github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= -github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= -github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= -github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= -github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= -github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= -github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= -github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= -github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= -github.com/opentracing/basictracer-go v1.1.0 h1:Oa1fTSBvAl8pa3U+IJYqrKm0NALwH9OsgwOqDv4xJW0= -github.com/opentracing/basictracer-go v1.1.0/go.mod h1:V2HZueSJEp879yv285Aap1BS69fQMD+MNP1mRs6mBQc= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= -github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= -github.com/pgavlin/fx v0.1.6 h1:r9jEg69DhNoCd3Xh0+5mIbdbS3PqWrVWujkY76MFRTU= -github.com/pgavlin/fx v0.1.6/go.mod h1:KWZJ6fqBBSh8GxHYqwYCf3rYE7Gp2p0N8tJp8xv9u9M= -github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= -github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/term v1.1.0 h1:xIAAdCMh3QIAy+5FrE8Ad8XoDhEU4ufwbaSozViP9kk= -github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 h1:vkHw5I/plNdTr435cARxCW6q9gc0S/Yxz7Mkd38pOb0= -github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231/go.mod h1:murToZ2N9hNJzewjHBgfFdXhZKjY3z5cYC1VXk+lbFE= -github.com/pulumi/esc v0.6.2 h1:+z+l8cuwIauLSwXQS0uoI3rqB+YG4SzsZYtHfNoXBvw= -github.com/pulumi/esc v0.6.2/go.mod h1:jNnYNjzsOgVTjCp0LL24NsCk8ZJxq4IoLQdCT0X7l8k= -github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.12.0 h1:vG/22IHpYupt+ZD+KOnRo5PqIrhShYj2MGYeRz3RFGI= -github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.12.0/go.mod h1:WnJK/yelFkTPdsx7jZuUZixRunf+QQlgCwoRi1mVF3A= -github.com/pulumi/pulumi/sdk/v3 v3.117.0 h1:ImIsukZ2ZIYQG94uWdSZl9dJjJTosQSTsOQTauTNX7U= -github.com/pulumi/pulumi/sdk/v3 v3.117.0/go.mod h1:kNea72+FQk82OjZ3yEP4dl6nbAl2ngE8PDBc0iFAaHg= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= -github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI= -github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= -github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 h1:TToq11gyfNlrMFZiYujSekIsPd9AmsA2Bj/iv+s4JHE= -github.com/santhosh-tekuri/jsonschema/v5 v5.0.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= -github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= -github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= -github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= -github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= -github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/texttheater/golang-levenshtein v1.0.1 h1:+cRNoVrfiwufQPhoMzB6N0Yf/Mqajr6t1lOv8GyGE2U= -github.com/texttheater/golang-levenshtein v1.0.1/go.mod h1:PYAKrbF5sAiq9wd+H82hs7gNaen0CplQ9uvm6+enD/8= -github.com/tweekmonster/luser v0.0.0-20161003172636-3fa38070dbd7 h1:X9dsIWPuuEJlPX//UmRKophhOKCGXc46RVIGuttks68= -github.com/tweekmonster/luser v0.0.0-20161003172636-3fa38070dbd7/go.mod h1:UxoP3EypF8JfGEjAII8jx1q8rQyDnX8qdTCs/UQBVIE= -github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= -github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= -github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= -github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= -github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 h1:H8wwQwTe5sL6x30z71lUgNiwBdeCHQjrphCfLwqIHGo= -github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2/go.mod h1:/kR4beFhlz2g+V5ik8jW+3PMiMQAPt29y6K64NNY53c= -github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 h1:3/aHKUq7qaFMWxyQV0W2ryNgg8x8rVeKVA20KJUkfS0= -github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2/go.mod h1:Zit4b8AQXaXvA68+nzmbyDzqiyFRISyw1JiD5JqUBjw= -github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= -github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zclconf/go-cty v1.13.2 h1:4GvrUxe/QUDYuJKAav4EYqdM47/kZa672LwmXFmEKT0= -github.com/zclconf/go-cty v1.13.2/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= -go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= -go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= -go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk= -go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8= -go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= -go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= -go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= -go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= -go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= -go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= -golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= -golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= -golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= -golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= -golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= -google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= -google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= -google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= -google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= -gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -lukechampine.com/frand v1.4.2 h1:RzFIpOvkMXuPMBb9maa4ND4wjBn71E1Jpf8BzJHMaVw= -lukechampine.com/frand v1.4.2/go.mod h1:4S/TM2ZgrKejMcKMbeLjISpJMO+/eZ1zu3vYX9dtj3s= -pgregory.net/rapid v0.6.1 h1:4eyrDxyht86tT4Ztm+kvlyNBLIk071gR+ZQdhphc9dQ= -pgregory.net/rapid v0.6.1/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= diff --git a/test/rolling-upgrades/main_test.go b/test/rolling-upgrades/main_test.go deleted file mode 100644 index 3abfae74b..000000000 --- a/test/rolling-upgrades/main_test.go +++ /dev/null @@ -1,286 +0,0 @@ -package main - -import ( - "context" - "flag" - "fmt" - "github.com/formancehq/go-libs/v2/logging" - "github.com/formancehq/ledger/pkg/features" - corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1" - "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v3" - metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/meta/v1" - "github.com/pulumi/pulumi/sdk/v3/go/auto" - "github.com/pulumi/pulumi/sdk/v3/go/pulumi" - "github.com/pulumi/pulumi/sdk/v3/go/pulumi/config" - "github.com/stretchr/testify/require" - "testing" - "time" -) - -var ( - latestVersion = flag.String("latest-version", "latest", "The version to deploy first") - actualVersion = flag.String("actual-version", "latest", "The version to upgrade") - noCleanup = flag.Bool("no-cleanup", false, "Disable cleanup of created resources") - noCleanupOnFailure = flag.Bool("no-cleanup-on-failure", false, "Disable cleanup of created resources on failure") - projectName = flag.String("project", "", "Pulumi project") - stackPrefixName = flag.String("stack-prefix-name", "", "Pulumi stack prefix for names") - testImage = flag.String("test-image", "", "Test image") -) - -func TestK8SRollingUpgrades(t *testing.T) { - - flag.Parse() - - ctx := logging.TestingContext() - - testFailure := false - cleanup := func(stack auto.Stack) func() { - return func() { - if testFailure && *noCleanupOnFailure { - return - } - cleanup(ctx, stack) - } - } - - logging.FromContext(ctx).Info("Installing Postgres") - pgStack, err := auto.UpsertStackInlineSource(ctx, *stackPrefixName+"postgres", *projectName, deployPostgres) - require.NoError(t, err, "creating ledger stack") - t.Cleanup(cleanup(pgStack)) - - _, err = upAndPrintOutputs(ctx, pgStack) - require.NoError(t, err, "upping pg stack") - - ledgerStack, err := auto.UpsertStackLocalSource(ctx, *stackPrefixName+"ledger", "../../deployments/pulumi") - require.NoError(t, err, "creating ledger stack") - t.Cleanup(cleanup(ledgerStack)) - - pgStackOutputs, err := pgStack.Outputs(ctx) - require.NoError(t, err, "unable to extract pg stack outputs") - - err = ledgerStack.SetAllConfig( - ctx, - auto.ConfigMap{ - "version": auto.ConfigValue{Value: *latestVersion}, - "postgres.uri": auto.ConfigValue{ - Value: "postgres://postgres:postgres@" + pgStackOutputs["service-name"].Value.(string) + ".svc.cluster.local:5432/ledger?sslmode=disable", - }, - "debug": auto.ConfigValue{Value: "true"}, - "image.pullPolicy": auto.ConfigValue{Value: "Always"}, - "replicaCount": auto.ConfigValue{Value: "1"}, - "experimentalFeatures": auto.ConfigValue{Value: "true"}, - }, - ) - require.NoError(t, err, "setting config on ledger stack") - - _, err = upAndPrintOutputs(ctx, ledgerStack) - require.NoError(t, err, "upping ledger stack first time") - - testStack, err := auto.UpsertStackInlineSource(ctx, *stackPrefixName+"test", *projectName, deployTest) - require.NoError(t, err, "creating test stack") - t.Cleanup(cleanup(testStack)) - - ledgerStackOutputs, err := ledgerStack.Outputs(ctx) - require.NoError(t, err, "unable to extract ledger stack outputs") - - ledgerURL := fmt.Sprintf( - "http://%s.%s.svc.cluster.local:%.0f", - ledgerStackOutputs["service-name"].Value, - ledgerStackOutputs["service-namespace"].Value, - ledgerStackOutputs["service-port"].Value, - ) - - err = testStack.SetAllConfig(ctx, auto.ConfigMap{ - "ledger-url": auto.ConfigValue{Value: ledgerURL}, - "image": auto.ConfigValue{Value: *testImage}, - }) - require.NoError(t, err, "setting config on test stack") - - _, err = testStack.Destroy(ctx) - require.NoError(t, err, "destroying test stack") - - _, err = upAndPrintOutputs(ctx, testStack) - require.NoError(t, err, "upping test stack") - - // Let a moment ensure the test image is actually sending requests - // We could maybe find a dynamic way to do that - <-time.After(10 * time.Second) - - err = ledgerStack.SetConfig(ctx, "version", auto.ConfigValue{ - Value: *actualVersion, - }) - require.NoError(t, err, "setting version on ledger stack") - - _, err = upAndPrintOutputs(ctx, ledgerStack) - require.NoError(t, err, "upping ledger stack second time") - - testStackOutputs, err := testStack.Outputs(ctx) - require.NoError(t, err, "unable to extract test stack outputs") - - checkStack, err := auto.UpsertStackInlineSource( - ctx, - *stackPrefixName+"check", - *projectName, - func(ctx *pulumi.Context) error { - pod, err := corev1.GetPod( - ctx, - testStackOutputs["name"].Value.(string), - pulumi.ID(testStackOutputs["id"].Value.(string)), - nil, - ) - if err != nil { - return err - } - - ctx.Export("phase", pod.Status.Phase().ApplyT(func(phase *string) string { - return *phase - })) - - return nil - }, - ) - require.NoError(t, err, "creating test stack") - t.Cleanup(cleanup(checkStack)) - - ret, err := upAndPrintOutputs(ctx, checkStack) - require.NoError(t, err, "upping check stack") - - testFailure = ret.Outputs["phase"].Value.(string) == "Failed" - require.False(t, testFailure) -} - -func cleanup(ctx context.Context, stack auto.Stack) { - if *noCleanup { - return - } - - fmt.Printf("Destroying stack '%s'...\r\n", stack.Name()) - if _, err := stack.Destroy(ctx); err != nil { - logging.FromContext(ctx).Errorf("destroying stack: %v", err) - } - - fmt.Printf("Removing stack '%s'...\r\n", stack.Name()) - if err := stack.Workspace().RemoveStack(ctx, stack.Name()); err != nil { - logging.FromContext(ctx).Errorf("removing stack: %v", err) - } -} - -func upAndPrintOutputs(ctx context.Context, stack auto.Stack) (auto.UpResult, error) { - out, err := stack.Up(ctx) - if out.StdErr != "" { - fmt.Println(out.StdErr) - } - if err != nil { - return auto.UpResult{}, fmt.Errorf("upping stack '%s': %w", stack.Name(), err) - } - - if out.StdOut != "" { - fmt.Println(out.StdOut) - } - - return out, nil -} - -func deployTest(ctx *pulumi.Context) error { - conf := config.New(ctx, "") - namespace, err := conf.Try("namespace") - if err != nil { - namespace = "default" - } - image := conf.Require("image") - ledgerURL := conf.Require("ledger-url") - - generatorArgs := pulumi.StringArray{ - pulumi.String(ledgerURL), - pulumi.String("/examples/example1.js"), - pulumi.String("-p"), - pulumi.String("30"), - } - - for _, key := range features.MinimalFeatureSet.SortedKeys() { - generatorArgs = append(generatorArgs, - pulumi.String("--ledger-feature"), - pulumi.String(key+"="+features.MinimalFeatureSet[key]), - ) - } - - rel, err := corev1.NewPod( - ctx, - "test", - &corev1.PodArgs{ - Metadata: metav1.ObjectMetaArgs{ - Namespace: pulumi.String(namespace), - }, - Spec: corev1.PodSpecArgs{ - RestartPolicy: pulumi.String("Never"), - Containers: corev1.ContainerArray{ - corev1.ContainerArgs{ - Name: pulumi.String("test"), - Args: generatorArgs, - Image: pulumi.String(image), - ImagePullPolicy: pulumi.String("Always"), - }, - }, - }, - }, - pulumi.Timeouts(&pulumi.CustomTimeouts{ - Create: "10s", - Update: "10s", - Delete: "10s", - }), - pulumi.DeleteBeforeReplace(true), - ) - if err != nil { - return err - } - - ctx.Export("name", rel.Metadata.Name()) - ctx.Export("id", rel.ID()) - - return nil -} - -func deployPostgres(ctx *pulumi.Context) error { - conf := config.New(ctx, "") - namespace, err := conf.Try("namespace") - if err != nil { - namespace = "default" - } - - rel, err := helm.NewRelease(ctx, "postgres", &helm.ReleaseArgs{ - Chart: pulumi.String("oci://registry-1.docker.io/bitnamicharts/postgresql"), - Version: pulumi.String("16.1.1"), - Namespace: pulumi.String(namespace), - Values: pulumi.Map(map[string]pulumi.Input{ - "auth": pulumi.Map{ - "postgresPassword": pulumi.String("postgres"), - "database": pulumi.String("ledger"), - }, - "primary": pulumi.Map{ - "resources": pulumi.Map{ - "requests": pulumi.Map{ - "memory": pulumi.String("256Mi"), - "cpu": pulumi.String("256m"), - }, - }, - }, - }), - CreateNamespace: pulumi.BoolPtr(true), - }) - if err != nil { - return fmt.Errorf("installing release") - } - - svc := pulumi.All(rel.Status.Namespace(), rel.Status.Name()). - ApplyT(func(r any) string { - arr := r.([]interface{}) - namespace := arr[0].(*string) - name := arr[1].(*string) - - return fmt.Sprintf("%s-postgresql.%s", *name, *namespace) - }) - - ctx.Export("service-name", svc) - - return nil -} From af53ed56dd1a668c39093a2b8df0622f2129eed7 Mon Sep 17 00:00:00 2001 From: Ragot Geoffrey Date: Thu, 5 Dec 2024 18:22:36 +0100 Subject: [PATCH 51/71] fix(upgrades): upgrades when creating a new ledger on an already migration bucket (#602) --- docs/api/README.md | 1 + internal/README.md | 196 +++++++++--------- internal/api/common/errors.go | 1 + internal/api/v2/controllers_ledgers_create.go | 2 + .../api/v2/controllers_ledgers_create_test.go | 7 + internal/controller/system/errors.go | 1 + internal/storage/bucket/bucket.go | 1 + internal/storage/bucket/default_bucket.go | 12 ++ .../storage/driver/buckets_generated_test.go | 15 ++ internal/storage/driver/driver.go | 15 ++ internal/storage/driver/driver_test.go | 4 + openapi.yaml | 1 + openapi/v2.yaml | 1 + tools/generator/cmd/root.go | 26 ++- 14 files changed, 176 insertions(+), 107 deletions(-) diff --git a/docs/api/README.md b/docs/api/README.md index 0ac42e9d2..52cc516f7 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -3244,6 +3244,7 @@ Authorization ( Scopes: ledger:write ) |*anonymous*|INTERPRETER_PARSE| |*anonymous*|INTERPRETER_RUNTIME| |*anonymous*|LEDGER_ALREADY_EXISTS| +|*anonymous*|BUCKET_OUTDATED|

V2LedgerInfoResponse

diff --git a/internal/README.md b/internal/README.md index bef2ec79d..debde2f3e 100644 --- a/internal/README.md +++ b/internal/README.md @@ -146,7 +146,7 @@ var Zero = big.NewInt(0) ``` -## func [ComputeIdempotencyHash]() +## func ComputeIdempotencyHash ```go func ComputeIdempotencyHash(inputs any) string @@ -155,7 +155,7 @@ func ComputeIdempotencyHash(inputs any) string -## type [Account]() +## type Account @@ -174,7 +174,7 @@ type Account struct { ``` -### func \(Account\) [GetAddress]() +### func \(Account\) GetAddress ```go func (a Account) GetAddress() string @@ -183,7 +183,7 @@ func (a Account) GetAddress() string -## type [AccountMetadata]() +## type AccountMetadata @@ -192,7 +192,7 @@ type AccountMetadata map[string]metadata.Metadata ``` -## type [AccountsVolumes]() +## type AccountsVolumes @@ -208,7 +208,7 @@ type AccountsVolumes struct { ``` -## type [BalancesByAssets]() +## type BalancesByAssets @@ -217,7 +217,7 @@ type BalancesByAssets map[string]*big.Int ``` -## type [BalancesByAssetsByAccounts]() +## type BalancesByAssetsByAccounts @@ -226,7 +226,7 @@ type BalancesByAssetsByAccounts map[string]BalancesByAssets ``` -## type [Configuration]() +## type Configuration @@ -239,7 +239,7 @@ type Configuration struct { ``` -### func [NewDefaultConfiguration]() +### func NewDefaultConfiguration ```go func NewDefaultConfiguration() Configuration @@ -248,7 +248,7 @@ func NewDefaultConfiguration() Configuration -### func \(\*Configuration\) [SetDefaults]() +### func \(\*Configuration\) SetDefaults ```go func (c *Configuration) SetDefaults() @@ -257,7 +257,7 @@ func (c *Configuration) SetDefaults() -### func \(\*Configuration\) [Validate]() +### func \(\*Configuration\) Validate ```go func (c *Configuration) Validate() error @@ -266,7 +266,7 @@ func (c *Configuration) Validate() error -## type [CreatedTransaction]() +## type CreatedTransaction @@ -278,7 +278,7 @@ type CreatedTransaction struct { ``` -### func \(CreatedTransaction\) [GetMemento]() +### func \(CreatedTransaction\) GetMemento ```go func (p CreatedTransaction) GetMemento() any @@ -287,7 +287,7 @@ func (p CreatedTransaction) GetMemento() any -### func \(CreatedTransaction\) [Type]() +### func \(CreatedTransaction\) Type ```go func (p CreatedTransaction) Type() LogType @@ -296,7 +296,7 @@ func (p CreatedTransaction) Type() LogType -## type [DeletedMetadata]() +## type DeletedMetadata @@ -309,7 +309,7 @@ type DeletedMetadata struct { ``` -### func \(DeletedMetadata\) [Type]() +### func \(DeletedMetadata\) Type ```go func (s DeletedMetadata) Type() LogType @@ -318,7 +318,7 @@ func (s DeletedMetadata) Type() LogType -### func \(\*DeletedMetadata\) [UnmarshalJSON]() +### func \(\*DeletedMetadata\) UnmarshalJSON ```go func (s *DeletedMetadata) UnmarshalJSON(data []byte) error @@ -327,7 +327,7 @@ func (s *DeletedMetadata) UnmarshalJSON(data []byte) error -## type [ErrInvalidBucketName]() +## type ErrInvalidBucketName @@ -338,7 +338,7 @@ type ErrInvalidBucketName struct { ``` -### func \(ErrInvalidBucketName\) [Error]() +### func \(ErrInvalidBucketName\) Error ```go func (e ErrInvalidBucketName) Error() string @@ -347,7 +347,7 @@ func (e ErrInvalidBucketName) Error() string -### func \(ErrInvalidBucketName\) [Is]() +### func \(ErrInvalidBucketName\) Is ```go func (e ErrInvalidBucketName) Is(err error) bool @@ -356,7 +356,7 @@ func (e ErrInvalidBucketName) Is(err error) bool -## type [ErrInvalidLedgerName]() +## type ErrInvalidLedgerName @@ -367,7 +367,7 @@ type ErrInvalidLedgerName struct { ``` -### func \(ErrInvalidLedgerName\) [Error]() +### func \(ErrInvalidLedgerName\) Error ```go func (e ErrInvalidLedgerName) Error() string @@ -376,7 +376,7 @@ func (e ErrInvalidLedgerName) Error() string -### func \(ErrInvalidLedgerName\) [Is]() +### func \(ErrInvalidLedgerName\) Is ```go func (e ErrInvalidLedgerName) Is(err error) bool @@ -385,7 +385,7 @@ func (e ErrInvalidLedgerName) Is(err error) bool -## type [Ledger]() +## type Ledger @@ -401,7 +401,7 @@ type Ledger struct { ``` -### func [MustNewWithDefault]() +### func MustNewWithDefault ```go func MustNewWithDefault(name string) Ledger @@ -410,7 +410,7 @@ func MustNewWithDefault(name string) Ledger -### func [New]() +### func New ```go func New(name string, configuration Configuration) (*Ledger, error) @@ -419,7 +419,7 @@ func New(name string, configuration Configuration) (*Ledger, error) -### func [NewWithDefaults]() +### func NewWithDefaults ```go func NewWithDefaults(name string) (*Ledger, error) @@ -428,7 +428,7 @@ func NewWithDefaults(name string) (*Ledger, error) -### func \(Ledger\) [HasFeature]() +### func \(Ledger\) HasFeature ```go func (l Ledger) HasFeature(feature, value string) bool @@ -437,7 +437,7 @@ func (l Ledger) HasFeature(feature, value string) bool -### func \(Ledger\) [WithMetadata]() +### func \(Ledger\) WithMetadata ```go func (l Ledger) WithMetadata(m metadata.Metadata) Ledger @@ -446,7 +446,7 @@ func (l Ledger) WithMetadata(m metadata.Metadata) Ledger -## type [Log]() +## type Log Log represents atomic actions made on the ledger. @@ -467,7 +467,7 @@ type Log struct { ``` -### func [NewLog]() +### func NewLog ```go func NewLog(payload LogPayload) Log @@ -476,7 +476,7 @@ func NewLog(payload LogPayload) Log -### func \(Log\) [ChainLog]() +### func \(Log\) ChainLog ```go func (l Log) ChainLog(previous *Log) Log @@ -485,7 +485,7 @@ func (l Log) ChainLog(previous *Log) Log -### func \(\*Log\) [ComputeHash]() +### func \(\*Log\) ComputeHash ```go func (l *Log) ComputeHash(previous *Log) @@ -494,7 +494,7 @@ func (l *Log) ComputeHash(previous *Log) -### func \(\*Log\) [UnmarshalJSON]() +### func \(\*Log\) UnmarshalJSON ```go func (l *Log) UnmarshalJSON(data []byte) error @@ -503,7 +503,7 @@ func (l *Log) UnmarshalJSON(data []byte) error -### func \(Log\) [WithIdempotencyKey]() +### func \(Log\) WithIdempotencyKey ```go func (l Log) WithIdempotencyKey(key string) Log @@ -512,7 +512,7 @@ func (l Log) WithIdempotencyKey(key string) Log -## type [LogPayload]() +## type LogPayload @@ -523,7 +523,7 @@ type LogPayload interface { ``` -### func [HydrateLog]() +### func HydrateLog ```go func HydrateLog(_type LogType, data []byte) (LogPayload, error) @@ -532,7 +532,7 @@ func HydrateLog(_type LogType, data []byte) (LogPayload, error) -## type [LogType]() +## type LogType @@ -552,7 +552,7 @@ const ( ``` -### func [LogTypeFromString]() +### func LogTypeFromString ```go func LogTypeFromString(logType string) LogType @@ -561,7 +561,7 @@ func LogTypeFromString(logType string) LogType -### func \(LogType\) [MarshalJSON]() +### func \(LogType\) MarshalJSON ```go func (lt LogType) MarshalJSON() ([]byte, error) @@ -570,7 +570,7 @@ func (lt LogType) MarshalJSON() ([]byte, error) -### func \(\*LogType\) [Scan]() +### func \(\*LogType\) Scan ```go func (lt *LogType) Scan(src interface{}) error @@ -579,7 +579,7 @@ func (lt *LogType) Scan(src interface{}) error -### func \(LogType\) [String]() +### func \(LogType\) String ```go func (lt LogType) String() string @@ -588,7 +588,7 @@ func (lt LogType) String() string -### func \(\*LogType\) [UnmarshalJSON]() +### func \(\*LogType\) UnmarshalJSON ```go func (lt *LogType) UnmarshalJSON(data []byte) error @@ -597,7 +597,7 @@ func (lt *LogType) UnmarshalJSON(data []byte) error -### func \(LogType\) [Value]() +### func \(LogType\) Value ```go func (lt LogType) Value() (driver.Value, error) @@ -606,7 +606,7 @@ func (lt LogType) Value() (driver.Value, error) -## type [Memento]() +## type Memento @@ -617,7 +617,7 @@ type Memento interface { ``` -## type [Move]() +## type Move @@ -638,7 +638,7 @@ type Move struct { ``` -## type [Moves]() +## type Moves @@ -647,7 +647,7 @@ type Moves []*Move ``` -### func \(Moves\) [ComputePostCommitEffectiveVolumes]() +### func \(Moves\) ComputePostCommitEffectiveVolumes ```go func (m Moves) ComputePostCommitEffectiveVolumes() PostCommitVolumes @@ -656,7 +656,7 @@ func (m Moves) ComputePostCommitEffectiveVolumes() PostCommitVolumes -## type [PostCommitVolumes]() +## type PostCommitVolumes @@ -665,7 +665,7 @@ type PostCommitVolumes map[string]VolumesByAssets ``` -### func \(PostCommitVolumes\) [AddInput]() +### func \(PostCommitVolumes\) AddInput ```go func (a PostCommitVolumes) AddInput(account, asset string, input *big.Int) @@ -674,7 +674,7 @@ func (a PostCommitVolumes) AddInput(account, asset string, input *big.Int) -### func \(PostCommitVolumes\) [AddOutput]() +### func \(PostCommitVolumes\) AddOutput ```go func (a PostCommitVolumes) AddOutput(account, asset string, output *big.Int) @@ -683,7 +683,7 @@ func (a PostCommitVolumes) AddOutput(account, asset string, output *big.Int) -### func \(PostCommitVolumes\) [Copy]() +### func \(PostCommitVolumes\) Copy ```go func (a PostCommitVolumes) Copy() PostCommitVolumes @@ -692,7 +692,7 @@ func (a PostCommitVolumes) Copy() PostCommitVolumes -### func \(PostCommitVolumes\) [Merge]() +### func \(PostCommitVolumes\) Merge ```go func (a PostCommitVolumes) Merge(volumes PostCommitVolumes) PostCommitVolumes @@ -701,7 +701,7 @@ func (a PostCommitVolumes) Merge(volumes PostCommitVolumes) PostCommitVolumes -## type [Posting]() +## type Posting @@ -715,7 +715,7 @@ type Posting struct { ``` -### func [NewPosting]() +### func NewPosting ```go func NewPosting(source string, destination string, asset string, amount *big.Int) Posting @@ -724,7 +724,7 @@ func NewPosting(source string, destination string, asset string, amount *big.Int -## type [Postings]() +## type Postings @@ -733,7 +733,7 @@ type Postings []Posting ``` -### func \(Postings\) [Reverse]() +### func \(Postings\) Reverse ```go func (p Postings) Reverse() Postings @@ -742,7 +742,7 @@ func (p Postings) Reverse() Postings -### func \(Postings\) [Validate]() +### func \(Postings\) Validate ```go func (p Postings) Validate() (int, error) @@ -751,7 +751,7 @@ func (p Postings) Validate() (int, error) -## type [RevertedTransaction]() +## type RevertedTransaction @@ -763,7 +763,7 @@ type RevertedTransaction struct { ``` -### func \(RevertedTransaction\) [GetMemento]() +### func \(RevertedTransaction\) GetMemento ```go func (r RevertedTransaction) GetMemento() any @@ -772,7 +772,7 @@ func (r RevertedTransaction) GetMemento() any -### func \(RevertedTransaction\) [Type]() +### func \(RevertedTransaction\) Type ```go func (r RevertedTransaction) Type() LogType @@ -781,7 +781,7 @@ func (r RevertedTransaction) Type() LogType -## type [SavedMetadata]() +## type SavedMetadata @@ -794,7 +794,7 @@ type SavedMetadata struct { ``` -### func \(SavedMetadata\) [Type]() +### func \(SavedMetadata\) Type ```go func (s SavedMetadata) Type() LogType @@ -803,7 +803,7 @@ func (s SavedMetadata) Type() LogType -### func \(\*SavedMetadata\) [UnmarshalJSON]() +### func \(\*SavedMetadata\) UnmarshalJSON ```go func (s *SavedMetadata) UnmarshalJSON(data []byte) error @@ -812,7 +812,7 @@ func (s *SavedMetadata) UnmarshalJSON(data []byte) error -## type [Transaction]() +## type Transaction @@ -833,7 +833,7 @@ type Transaction struct { ``` -### func [NewTransaction]() +### func NewTransaction ```go func NewTransaction() Transaction @@ -842,7 +842,7 @@ func NewTransaction() Transaction -### func \(Transaction\) [InvolvedAccounts]() +### func \(Transaction\) InvolvedAccounts ```go func (tx Transaction) InvolvedAccounts() []string @@ -851,7 +851,7 @@ func (tx Transaction) InvolvedAccounts() []string -### func \(Transaction\) [InvolvedDestinations]() +### func \(Transaction\) InvolvedDestinations ```go func (tx Transaction) InvolvedDestinations() map[string][]string @@ -860,7 +860,7 @@ func (tx Transaction) InvolvedDestinations() map[string][]string -### func \(Transaction\) [IsReverted]() +### func \(Transaction\) IsReverted ```go func (tx Transaction) IsReverted() bool @@ -869,7 +869,7 @@ func (tx Transaction) IsReverted() bool -### func \(Transaction\) [JSONSchemaExtend]() +### func \(Transaction\) JSONSchemaExtend ```go func (Transaction) JSONSchemaExtend(schema *jsonschema.Schema) @@ -878,7 +878,7 @@ func (Transaction) JSONSchemaExtend(schema *jsonschema.Schema) -### func \(Transaction\) [MarshalJSON]() +### func \(Transaction\) MarshalJSON ```go func (tx Transaction) MarshalJSON() ([]byte, error) @@ -887,7 +887,7 @@ func (tx Transaction) MarshalJSON() ([]byte, error) -### func \(Transaction\) [Reverse]() +### func \(Transaction\) Reverse ```go func (tx Transaction) Reverse() Transaction @@ -896,7 +896,7 @@ func (tx Transaction) Reverse() Transaction -### func \(Transaction\) [VolumeUpdates]() +### func \(Transaction\) VolumeUpdates ```go func (tx Transaction) VolumeUpdates() []AccountsVolumes @@ -905,7 +905,7 @@ func (tx Transaction) VolumeUpdates() []AccountsVolumes -### func \(Transaction\) [WithInsertedAt]() +### func \(Transaction\) WithInsertedAt ```go func (tx Transaction) WithInsertedAt(date time.Time) Transaction @@ -914,7 +914,7 @@ func (tx Transaction) WithInsertedAt(date time.Time) Transaction -### func \(Transaction\) [WithMetadata]() +### func \(Transaction\) WithMetadata ```go func (tx Transaction) WithMetadata(m metadata.Metadata) Transaction @@ -923,7 +923,7 @@ func (tx Transaction) WithMetadata(m metadata.Metadata) Transaction -### func \(Transaction\) [WithPostCommitEffectiveVolumes]() +### func \(Transaction\) WithPostCommitEffectiveVolumes ```go func (tx Transaction) WithPostCommitEffectiveVolumes(volumes PostCommitVolumes) Transaction @@ -932,7 +932,7 @@ func (tx Transaction) WithPostCommitEffectiveVolumes(volumes PostCommitVolumes) -### func \(Transaction\) [WithPostings]() +### func \(Transaction\) WithPostings ```go func (tx Transaction) WithPostings(postings ...Posting) Transaction @@ -941,7 +941,7 @@ func (tx Transaction) WithPostings(postings ...Posting) Transaction -### func \(Transaction\) [WithReference]() +### func \(Transaction\) WithReference ```go func (tx Transaction) WithReference(ref string) Transaction @@ -950,7 +950,7 @@ func (tx Transaction) WithReference(ref string) Transaction -### func \(Transaction\) [WithRevertedAt]() +### func \(Transaction\) WithRevertedAt ```go func (tx Transaction) WithRevertedAt(timestamp time.Time) Transaction @@ -959,7 +959,7 @@ func (tx Transaction) WithRevertedAt(timestamp time.Time) Transaction -### func \(Transaction\) [WithTimestamp]() +### func \(Transaction\) WithTimestamp ```go func (tx Transaction) WithTimestamp(ts time.Time) Transaction @@ -968,7 +968,7 @@ func (tx Transaction) WithTimestamp(ts time.Time) Transaction -## type [TransactionData]() +## type TransactionData @@ -983,7 +983,7 @@ type TransactionData struct { ``` -### func [NewTransactionData]() +### func NewTransactionData ```go func NewTransactionData() TransactionData @@ -992,7 +992,7 @@ func NewTransactionData() TransactionData -### func \(TransactionData\) [WithPostings]() +### func \(TransactionData\) WithPostings ```go func (data TransactionData) WithPostings(postings ...Posting) TransactionData @@ -1001,7 +1001,7 @@ func (data TransactionData) WithPostings(postings ...Posting) TransactionData -## type [Transactions]() +## type Transactions @@ -1012,7 +1012,7 @@ type Transactions struct { ``` -## type [Volumes]() +## type Volumes @@ -1024,7 +1024,7 @@ type Volumes struct { ``` -### func [NewEmptyVolumes]() +### func NewEmptyVolumes ```go func NewEmptyVolumes() Volumes @@ -1033,7 +1033,7 @@ func NewEmptyVolumes() Volumes -### func [NewVolumesInt64]() +### func NewVolumesInt64 ```go func NewVolumesInt64(input, output int64) Volumes @@ -1042,7 +1042,7 @@ func NewVolumesInt64(input, output int64) Volumes -### func \(Volumes\) [Balance]() +### func \(Volumes\) Balance ```go func (v Volumes) Balance() *big.Int @@ -1051,7 +1051,7 @@ func (v Volumes) Balance() *big.Int -### func \(Volumes\) [Copy]() +### func \(Volumes\) Copy ```go func (v Volumes) Copy() Volumes @@ -1060,7 +1060,7 @@ func (v Volumes) Copy() Volumes -### func \(Volumes\) [JSONSchemaExtend]() +### func \(Volumes\) JSONSchemaExtend ```go func (Volumes) JSONSchemaExtend(schema *jsonschema.Schema) @@ -1069,7 +1069,7 @@ func (Volumes) JSONSchemaExtend(schema *jsonschema.Schema) -### func \(Volumes\) [MarshalJSON]() +### func \(Volumes\) MarshalJSON ```go func (v Volumes) MarshalJSON() ([]byte, error) @@ -1078,7 +1078,7 @@ func (v Volumes) MarshalJSON() ([]byte, error) -### func \(\*Volumes\) [Scan]() +### func \(\*Volumes\) Scan ```go func (v *Volumes) Scan(src interface{}) error @@ -1087,7 +1087,7 @@ func (v *Volumes) Scan(src interface{}) error -### func \(Volumes\) [Value]() +### func \(Volumes\) Value ```go func (v Volumes) Value() (driver.Value, error) @@ -1096,7 +1096,7 @@ func (v Volumes) Value() (driver.Value, error) -## type [VolumesByAssets]() +## type VolumesByAssets @@ -1105,7 +1105,7 @@ type VolumesByAssets map[string]Volumes ``` -### func \(VolumesByAssets\) [Balances]() +### func \(VolumesByAssets\) Balances ```go func (v VolumesByAssets) Balances() BalancesByAssets @@ -1114,7 +1114,7 @@ func (v VolumesByAssets) Balances() BalancesByAssets -## type [VolumesWithBalance]() +## type VolumesWithBalance @@ -1127,7 +1127,7 @@ type VolumesWithBalance struct { ``` -## type [VolumesWithBalanceByAssetByAccount]() +## type VolumesWithBalanceByAssetByAccount @@ -1140,7 +1140,7 @@ type VolumesWithBalanceByAssetByAccount struct { ``` -## type [VolumesWithBalanceByAssets]() +## type VolumesWithBalanceByAssets diff --git a/internal/api/common/errors.go b/internal/api/common/errors.go index ff8b86e53..0e440d6c6 100644 --- a/internal/api/common/errors.go +++ b/internal/api/common/errors.go @@ -17,6 +17,7 @@ const ( ErrMetadataOverride = "METADATA_OVERRIDE" ErrBulkSizeExceeded = "BULK_SIZE_EXCEEDED" ErrLedgerAlreadyExists = "LEDGER_ALREADY_EXISTS" + ErrBucketOutdated = "BUCKET_OUTDATED" ErrInterpreterParse = "INTERPRETER_PARSE" ErrInterpreterRuntime = "INTERPRETER_RUNTIME" diff --git a/internal/api/v2/controllers_ledgers_create.go b/internal/api/v2/controllers_ledgers_create.go index 3bba4617f..2c59eb0b1 100644 --- a/internal/api/v2/controllers_ledgers_create.go +++ b/internal/api/v2/controllers_ledgers_create.go @@ -37,6 +37,8 @@ func createLedger(systemController system.Controller) http.HandlerFunc { errors.Is(err, ledger.ErrInvalidLedgerName{}) || errors.Is(err, ledger.ErrInvalidBucketName{}): api.BadRequest(w, common.ErrValidation, err) + case errors.Is(err, system.ErrBucketOutdated): + api.BadRequest(w, common.ErrBucketOutdated, err) case errors.Is(err, system.ErrLedgerAlreadyExists): api.BadRequest(w, common.ErrLedgerAlreadyExists, err) default: diff --git a/internal/api/v2/controllers_ledgers_create_test.go b/internal/api/v2/controllers_ledgers_create_test.go index 79fa67f1e..60bcca31c 100644 --- a/internal/api/v2/controllers_ledgers_create_test.go +++ b/internal/api/v2/controllers_ledgers_create_test.go @@ -79,6 +79,13 @@ func TestLedgersCreate(t *testing.T) { expectStatusCode: http.StatusBadRequest, expectErrorCode: common.ErrValidation, }, + { + name: "bucket actually outdated", + expectedBackendCall: true, + returnErr: system.ErrBucketOutdated, + expectStatusCode: http.StatusBadRequest, + expectErrorCode: common.ErrBucketOutdated, + }, { name: "unexpected error", expectedBackendCall: true, diff --git a/internal/controller/system/errors.go b/internal/controller/system/errors.go index 18d5eca64..fb503d902 100644 --- a/internal/controller/system/errors.go +++ b/internal/controller/system/errors.go @@ -7,6 +7,7 @@ import ( var ( ErrLedgerAlreadyExists = errors.New("ledger already exists") + ErrBucketOutdated = errors.New("bucket is outdated, you need to upgrade it before adding a new ledger") ErrExperimentalFeaturesDisabled = errors.New("experimental features are disabled") ) diff --git a/internal/storage/bucket/bucket.go b/internal/storage/bucket/bucket.go index cd63b3029..ff353a179 100644 --- a/internal/storage/bucket/bucket.go +++ b/internal/storage/bucket/bucket.go @@ -15,6 +15,7 @@ type Bucket interface { HasMinimalVersion(ctx context.Context) (bool, error) IsUpToDate(ctx context.Context) (bool, error) GetMigrationsInfo(ctx context.Context) ([]migrations.Info, error) + IsInitialized(context.Context) (bool, error) } type Factory interface { diff --git a/internal/storage/bucket/default_bucket.go b/internal/storage/bucket/default_bucket.go index 467b8ee36..0b3b89409 100644 --- a/internal/storage/bucket/default_bucket.go +++ b/internal/storage/bucket/default_bucket.go @@ -4,6 +4,7 @@ import ( "bytes" "context" _ "embed" + "errors" "fmt" "github.com/formancehq/go-libs/v2/migrations" ledger "github.com/formancehq/ledger/internal" @@ -22,6 +23,17 @@ type DefaultBucket struct { tracer trace.Tracer } +func (b *DefaultBucket) IsInitialized(ctx context.Context) (bool, error) { + _, err := GetMigrator(b.db, b.name).GetLastVersion(ctx) + if err == nil { + return true, nil + } + if errors.Is(err, migrations.ErrMissingVersionTable) { + return false, nil + } + return false, err +} + func (b *DefaultBucket) IsUpToDate(ctx context.Context) (bool, error) { return GetMigrator(b.db, b.name).IsUpToDate(ctx) } diff --git a/internal/storage/driver/buckets_generated_test.go b/internal/storage/driver/buckets_generated_test.go index e26f69bce..89d168b38 100644 --- a/internal/storage/driver/buckets_generated_test.go +++ b/internal/storage/driver/buckets_generated_test.go @@ -82,6 +82,21 @@ func (mr *MockBucketMockRecorder) HasMinimalVersion(ctx any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasMinimalVersion", reflect.TypeOf((*MockBucket)(nil).HasMinimalVersion), ctx) } +// IsInitialized mocks base method. +func (m *MockBucket) IsInitialized(arg0 context.Context) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsInitialized", arg0) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsInitialized indicates an expected call of IsInitialized. +func (mr *MockBucketMockRecorder) IsInitialized(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsInitialized", reflect.TypeOf((*MockBucket)(nil).IsInitialized), arg0) +} + // IsUpToDate mocks base method. func (m *MockBucket) IsUpToDate(ctx context.Context) (bool, error) { m.ctrl.T.Helper() diff --git a/internal/storage/driver/driver.go b/internal/storage/driver/driver.go index 175af6959..7c8b2b337 100644 --- a/internal/storage/driver/driver.go +++ b/internal/storage/driver/driver.go @@ -44,6 +44,21 @@ func (d *Driver) CreateLedger(ctx context.Context, l *ledger.Ledger) (*ledgersto } b := d.bucketFactory.Create(l.Bucket) + isInitialized, err := b.IsInitialized(ctx) + if err != nil { + return nil, fmt.Errorf("checking if bucket is initialized: %w", err) + } + if isInitialized { + upToDate, err := b.IsUpToDate(ctx) + if err != nil { + return nil, fmt.Errorf("checking if bucket is up to date: %w", err) + } + + if !upToDate { + return nil, systemcontroller.ErrBucketOutdated + } + } + if err := b.Migrate( ctx, make(chan struct{}), diff --git a/internal/storage/driver/driver_test.go b/internal/storage/driver/driver_test.go index a0e0a5811..615cb475e 100644 --- a/internal/storage/driver/driver_test.go +++ b/internal/storage/driver/driver_test.go @@ -209,6 +209,10 @@ func TestLedgersCreate(t *testing.T) { systemStore.EXPECT(). CreateLedger(gomock.Any(), l) + bucket.EXPECT(). + IsInitialized(gomock.Any()). + Return(false, nil) + bucket.EXPECT(). Migrate(gomock.Any(), gomock.Any(), gomock.Any()). Return(nil) diff --git a/openapi.yaml b/openapi.yaml index 28808975b..7e769bd79 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -3492,6 +3492,7 @@ components: - INTERPRETER_PARSE - INTERPRETER_RUNTIME - LEDGER_ALREADY_EXISTS + - BUCKET_OUTDATED example: VALIDATION V2LedgerInfoResponse: type: object diff --git a/openapi/v2.yaml b/openapi/v2.yaml index 378675367..0c8c82d8d 100644 --- a/openapi/v2.yaml +++ b/openapi/v2.yaml @@ -1770,6 +1770,7 @@ components: - INTERPRETER_PARSE - INTERPRETER_RUNTIME - LEDGER_ALREADY_EXISTS + - BUCKET_OUTDATED example: VALIDATION V2LedgerInfoResponse: type: object diff --git a/tools/generator/cmd/root.go b/tools/generator/cmd/root.go index 84cca4463..b031f17f4 100644 --- a/tools/generator/cmd/root.go +++ b/tools/generator/cmd/root.go @@ -147,19 +147,27 @@ func run(cmd *cobra.Command, args []string) error { ) logging.FromContext(cmd.Context()).Infof("Creating ledger '%s' if not exists", targetedLedger) - _, err = client.Ledger.V2.CreateLedger(cmd.Context(), operations.V2CreateLedgerRequest{ + _, err = client.Ledger.V2.GetLedger(cmd.Context(), operations.V2GetLedgerRequest{ Ledger: targetedLedger, - V2CreateLedgerRequest: &components.V2CreateLedgerRequest{ - Bucket: &ledgerBucket, - Metadata: ledgerMetadata, - Features: ledgerFeatures, - }, }) if err != nil { sdkError := &sdkerrors.V2ErrorResponse{} - if !errors.As(err, &sdkError) || (sdkError.ErrorCode != components.V2ErrorsEnumLedgerAlreadyExists && - sdkError.ErrorCode != components.V2ErrorsEnumValidation) { - return fmt.Errorf("failed to create ledger: %w", err) + if !errors.As(err, &sdkError) || sdkError.ErrorCode != components.V2ErrorsEnumNotFound { + return fmt.Errorf("failed to get ledger: %w", err) + } + _, err = client.Ledger.V2.CreateLedger(cmd.Context(), operations.V2CreateLedgerRequest{ + Ledger: targetedLedger, + V2CreateLedgerRequest: &components.V2CreateLedgerRequest{ + Bucket: &ledgerBucket, + Metadata: ledgerMetadata, + Features: ledgerFeatures, + }, + }) + if err != nil { + if !errors.As(err, &sdkError) || (sdkError.ErrorCode != components.V2ErrorsEnumLedgerAlreadyExists && + sdkError.ErrorCode != components.V2ErrorsEnumValidation) { + return fmt.Errorf("failed to create ledger: %w", err) + } } } From 04a2d4672b9cebd3a9d38bdbd6c16a1ff0f0f566 Mon Sep 17 00:00:00 2001 From: Ragot Geoffrey Date: Fri, 6 Dec 2024 13:37:55 +0100 Subject: [PATCH 52/71] fix: blocking migrations (#604) --- .../summary/relationships.real.compact.dot | 1 + .../summary/relationships.real.compact.png | Bin 71192 -> 73108 bytes .../summary/relationships.real.large.dot | 2 +- .../summary/relationships.real.large.png | Bin 94364 -> 94382 bytes .../diagrams/tables/accounts.1degree.dot | 1 + .../diagrams/tables/accounts.1degree.png | Bin 59639 -> 54640 bytes .../diagrams/tables/accounts.2degrees.dot | 1 + .../diagrams/tables/accounts.2degrees.png | Bin 63527 -> 58959 bytes .../diagrams/tables/moves.1degree.dot | 2 +- .../diagrams/tables/moves.1degree.png | Bin 67496 -> 67512 bytes .../diagrams/tables/moves.2degrees.dot | 2 +- .../diagrams/tables/transactions.1degree.dot | 1 + .../diagrams/tables/transactions.1degree.png | Bin 73342 -> 74700 bytes .../diagrams/tables/transactions.2degrees.dot | 1 + .../diagrams/tables/transactions.2degrees.png | Bin 76011 -> 77413 bytes .../15-create-ledger-indexes/up.sql | 34 +----------------- .../23-delete-orphan-indices/notes.yaml | 1 - .../23-delete-orphan-indices/up.sql | 2 -- .../up_tests_before.sql | 0 .../23-noop-keep-for-compatibility/notes.yaml | 1 + .../up.sql} | 0 internal/storage/ledger/main_test.go | 3 ++ pkg/client/.speakeasy/gen.lock | 6 ++-- .../docs/models/components/v2errorsenum.md | 3 +- .../operations/v2countaccountsrequest.md | 11 +++--- .../operations/v2counttransactionsrequest.md | 11 +++--- .../operations/v2listaccountsrequest.md | 1 - .../operations/v2listtransactionsrequest.md | 1 - pkg/client/docs/sdks/v2/README.md | 4 --- pkg/client/models/components/v2errorsenum.go | 3 ++ .../models/operations/v2countaccounts.go | 13 ++----- .../models/operations/v2counttransactions.go | 13 ++----- .../models/operations/v2listaccounts.go | 15 ++------ .../models/operations/v2listtransactions.go | 9 ----- 34 files changed, 38 insertions(+), 104 deletions(-) delete mode 100644 internal/storage/bucket/migrations/23-delete-orphan-indices/notes.yaml delete mode 100644 internal/storage/bucket/migrations/23-delete-orphan-indices/up.sql delete mode 100644 internal/storage/bucket/migrations/23-delete-orphan-indices/up_tests_before.sql create mode 100644 internal/storage/bucket/migrations/23-noop-keep-for-compatibility/notes.yaml rename internal/storage/bucket/migrations/{23-delete-orphan-indices/up_tests_after.sql => 23-noop-keep-for-compatibility/up.sql} (100%) diff --git a/docs/database/_default/diagrams/summary/relationships.real.compact.dot b/docs/database/_default/diagrams/summary/relationships.real.compact.dot index 68003186a..269fdddcb 100644 --- a/docs/database/_default/diagrams/summary/relationships.real.compact.dot +++ b/docs/database/_default/diagrams/summary/relationships.real.compact.dot @@ -12,6 +12,7 @@ digraph "compactRelationshipsDiagram" {
accounts_address_array
asset
effective_date
+
transactions_id
... < 2 > diff --git a/docs/database/_default/diagrams/summary/relationships.real.compact.png b/docs/database/_default/diagrams/summary/relationships.real.compact.png index cedb77457b91f17c49eae4c4dfd13ff99d977d0e..b6db6af89dc361f045e4455c31b0ca0f2f1017b9 100644 GIT binary patch delta 56685 zcmagGcR1H=|3CgN4M}#A8A`**F0w*KDXByvWRy*2!mB|jGYwLSw8$z%M#(N^WGlPO zii~8`_i=XJ_vdrpzwh@re%Enaf24PBuW_Ew=VLw3*e6S_7A%Qc!%$7Xj7FnzMTCuH z3AXN}b{Q5?v_FicilZYmAtCxp%Y=lIZ0#Sf>)yBJ%7{^P!|pxr4zRQ9y%XP{Av=|N zb)7`_?jVa$p)EVl?QjkeV_1^(bk(gcfq=LK;SRPbAMk{8z(04UioJ} z-EyX~n2q{ll>*eO)>bZBrd1_V;KhqV($Z{)4<8PpE?qhw9K8I%fddv+Ru|+Pn!Br_ zg+s!^G&7DBv8-9MTtGlz%%{*cLDcT_(9lpwXlO~s%iovGsfE?}k4xT>OI<@lDq!Ac zlc=aWpO9c+l-96hb@YQ5YbM`tCU&=_R)&xHypfb+TyDPeMR-=2REVa~qm&f&bu6!5 zzh2DA$w~E2pFKC>mK3$vc{_~gt;#!J_sh7H!%)@m9WbQ%{KOLCn$B%MEQ z=i=gW<&(gb+16QJ5vJ9BI3~0&Lbg$B=RwKC!&#JwhzLCs(?vXkTeolbjEr0!@2k`> zFkn4>`gGO%_ZL%B9m3t3o13fNz1wAJDL6Ydva#OQ#ic8IMYKp;j>DRC^OsDt{rmUR ztQha#zwa~NyOe=}!S#Em^2Y*iX8eS9?CU`{50A223ht7Qt&)@9y99Rc-fgD@{{QdoVzJ3i)pnk6vJ^Vz?!eZTVE35I|@|85T0|(fAe||Hw zGM&1Uv~6o*{rxr0{uA%E<){Q+uk71tX2z$brNt$Ck_FcnTz^m7>ebmr9&4|%s4Jdj zIy5lYbU)kvl3%G7pT@z1>;VA*I(zr-I(=Hd7`RqT_`{b;M&tf=aSmZb+2~Ej#cqjo(Ig<{H;PV17*>OXuatqA9F z!>;o9_HnJt%LV!CVn@#Z_;D&-LpYE{=pNGz-p5;hE~Xti{Mns%gp$fF>zuq z=LShHQ8$Tt@IWL&K$VG^nc2tJS4cu4VkC;(*V!w=>L#&IpGq_FZaGHq{N?X0{LwfQLElq_&2Dhy z$O?+0(lzh}Ke%_}ry=R-63yvmm+#(PH^1<+%QRSnUa+6SO$BN^J5hTvCWf6;%$Qce z)zZVFPQPdar5)(Hd*{wYJ9qBP^BEV4;8%X*xjm3YU9CD=Pf1xhzEE{NIt8A?J9ZYk zHDrOi^;K9d%ubmG<5?~_<@cOh@N4x`o44JgqhYPB#)Wg=3QU7PB+0vUh6M&LNiSU& znC-qdKmImeN&DG=g;~qiOiSaJ0saO05o~O1QB+hE?Zk-_<>}H*IZe6B83ngLl%~1xN5IoFDq|aN7`9_Q0c1 zdB@hX5@s*dP!SjAerlVV-pWf$N?LrGQ~Z2lqTrIni%iYT$N~)=-{#U87$46=)x3L` z_~5}Z{>bHge0*X2$|fo9WGyKv@h)1lNK#T#?Pb7%NkBvLuDlm7UUzo#tIm(nwIt%j z25VItu#dK8(cw}^!}Ri!Cr=h@aooMD`N@$B!rBq*@a&EpIWjgeQQBfoiMxfNI+m1| zFJ%@`(L|3C(Z546dV71{+U};F;!YzKzoDUlg`NF^Mle%O=Nh$}pPn3f=GtGy>bpr? zFwiyR$~_T!diqz_#gEoK)E78uXJ^@-ZRhIcrDbf)MYBM&G=65giZn}dF9NFk&urhZ zMQ+`^d8fVomIwQ8GvF?5Y;5#mB^Mc`n?}#_HU&_>f1TLAefwR9mh?24(+xuS)Fo6h ztYkMn2EjlNW{g1=+`^W?#U zi|J+sy))x|w6kyC>_)>|hF5T7n72tQjBAlfLKnj`5%G6bRrKlU>DyhqY5zP#YFD*^ zaFNgWVp`PIt4pGF){mp{+{kMmkLSs}cJ11RP5~7^u94=n3vqF4Q3)1B>jXM=DaVOrD;eXjfN9T+96Y{mb3L`F7+QzSqS)jQ>v`Yklq_AT1*kedkWZh2_k>&1pwz zXMar$SbctTEzj?l^rcIeSorwFvwG=hYa~pmv-j_BNJ&ZI_GD`K4P^AXscH2?{e&-y z9O^pD(c!z+sj3R#5tLh9zGN-5TF(iprZ0;e&XFIl$i z_@@`1nc3Na*yN}`Wj7iQrH7oKwOyFX?=*G#G-`dx_m_VnfSZIjZd^`t z{Zg}5?8wt4)cp8zEbZuP*Meqdd0d`V=|#UA=z2 zx9DeQ>Fpif1OknYI-)mQb-y|XD0MM9n)TGFQ<*kzImhYF06SSNS+b-X9XT^IQ_and zk|N!F+qP|2uU)gO2wMxhIFZW9#~0q)dwg(Yr1ZlF_5{BlD~cDUj{@OX^p=GJY`*gD z@(!Vck$vyE@^A}-}4i-obg9BHYxPh-BCW%4a8bOqXHTMc=%+CBXO@>ap|3JnCFy>}OoZ|@LStbDQmRGA6mi%fpeV)%Jq>sp{cp#jw%%~ICzrw3fDuPW#?Q=fwv3x65fHapRTyFGT;06?*~6T zWPbDZZO_oq3Sd4kKJ{I@?oSnSsc#qIV30cTo;UX9P0%pPVtTX1LpJQui}$o6UjzMPH z&t>wifu*!gos#CV>jI%Em77pvedLg0hvNVA}b z$b&fBy*eFuM>1htQj0<4wLCmzCnqPPu3a+}?s>e|(9kgLkC{=+WPxkXdCf#?iFzA3 z_4ZAm2iUvI!Oi*=ugcff)v>Tm4YucqcAojJ4645OmIa<$ zPP^#}yS;uY=7m2PXw}u#g!&!MI3}K6?Txp=-3L@4GX5KF5I6W(E_Ar@F}sI{M|qYsfLTIxMk_l3A+^;^D{xW{N}zsM-wXpA_p+L$gDC$d$};pgN0STpfb} zU->X0fB~f%|FU`T?ZCu%+ld50Nl6v}KvHhLbrxL)Zga~OjF(7d#eJc$c2R$nQ6!g% zi;Jh1+O2N#zEbq9@$o@9m6a=2?9|Z-nW2i#pI@x#>+dgWxP1tk?+1S z>E6AprgJ;e*Lgv6i~S};tHy43WE9f#SHe1jI1;_HxI2nM#(lu3v1J8 zXr2ML2OfW$P*hb_o%!{vX{Hd00Xi)DdX$~S-kmp^=fo>u8tb{^si&1E3q4MSapPUTyXMcX){6#m*AUP(Z>_5C7p5 zPIelTBb|kTo!lT;FZ}1^X?0Fx$p(0Pc}zR)r>CcTj|YLrnB4g&qD{Aq@fFsegZPY= zj{k50QzvIzM~4*_&Ze!@)?n~NLWXn-w85(E`bNKgH8^%mBC97FKYN=9vM8U9zO&!J zAkkktkfmWad0^oO4;(lTbmRQ#MY-oD{8VuNDa{bx%&e@QCkMoEd$L&DLd&0|rImZ1 zWP=KFx6{P_vzq0)+cHNLxkuT;kz)+nbgy4$8FqO& zdM()u=hSH0+S;Kf>Iv?^tB{nH4OClqHZ@h0Uo}9?jnQuQ|KdqyPCW9CYBwaz?qa$9 zUYNsX-1pH7^aTt<4U!+U8fRzcsyA=!GsCm}mH3%&D^i~7wzeXFxj=#TjV5dIFLAxc zKRnn=^q?>A;-zffaExmW|6kH%aK8J1y21FLm$r7Zf2kATV`ywF*GUHlI-1vL>$$qR z{hC@@uiw03FfcF}8yi#Gy_*5(p8%E*5A_K=L#3it{GRD!=HlT2^n3VTUP!g3rpA7K z16IyWfi&J~BAG%k8$`_lc?9GqAbDRzu1i-0F36(p?oPnm5^x?Vhi0)*E1eGy+W|Ki z(Q!S#wy&nKu&|sJKbm`2KS8Cgp@9GnLX$HyB_%7M4BbT;qwttlu3QPOK?mV*Dwp+_ zrLC=&C|PI>gD%=@lOH}Tt*vFodPYsu{PTNZ5e=&1KWb!R*#=1(5EuI&vAqfQ!dhXp zgxWPPYu~=r02c+QNCbWkD6g#S1G1HrlG^F&DygKTBze4yUQ<(3Z9Ur}wIU};A z|1XtuQQfLr*RBUXB&D-m`lX0q)%Hed=(6=|Xf|k$& zz4aRM9hq&sSUX^ay*c*tXJxVfT-mNkCVnO3zH`Y!(e!|8xJhuJYmH9HPBJnwbXHGF zo!-0`H3bN$`^7ffo{?g&y71G17FPNQk9h5;G?Du#9D;zMm#H;dE6}MePn-z;+VQ-! zvQqowiKY}oD1{db3U(YT^f~vuD}bk@qy)VyUC)2`DVW~LlP4{}^v0&9g4EWjC8+pk zx)1EbLa_o$ud1q2v$5GACMK5Y_v=&`ufo&Le-<^378e)Cw<#-4)5cJcwr=Dxvyakh z8jZ5rT4KD}M@dfNf^cBAk>0+b;9waArUX|!$IH;b-S{rnpfSXuNUmJ9>a*fik?5(p zms4sNKIPTM_vPb(t@E_bwKnAbvKiBnr= z0Zb|;FMs&s;bX_%gA`UVGBN^!QrzzD?p81V{NBON5Og3Rfu+Isrl9I}O|*bl zYofhOObWByx$Y>4Fk~Wrox@rDmRBey%oo1%-H7I>x4LUkHuYf^rTA2h! z$zOsFN4|)au!tL5%_(pISH69z2Hkdxd~!~e#nLO?nfx2#kg?3b-XzgAqF zzc+&P!LxV7^sRG2TpvBI9+1fgU$~o?Sl@Fvf~KY+$&~cwZ?zku^~+NV4J|FpX?izh zL;G|>3PVFex)lROh8}r8iCfF}S#e1gkvl);x&0Lz1^-^CFbBi<yd{onNf z@QJj3pkSe`TbYeAjs-n`zAcOj4a4#qc;fi+*QgTfRd}GI94+#dd;5lZb6kl(^0q2ftaWQDr+WCE zLd$9gvxFA!!lWc25f=JZpq#R=GPX4qz$`AOK4wh(_;LK$v19LiYnpAHo!>+7e&xv( z-oRf>sq1t!G5#tMG*P&+)7MvVetw=1)IEFl5Yv&iAGR7U75sX)>0&zCjvYJ3Ytd6F zdBB0t9nn~QQMYfeGBPsSv}MaB^KssD-C8u-I-0KL&O_tlK9hs2!0#-)yo}Jd2@_>y zWre*)+wXGp;1^S)CsY2%y2c5(`usTv#3EF62m0y*{R9caCw!q;+-8S`1EVc+eqTT>>?^;#X1o8aEG`MQ zYo>4CXb5Bjv%@jx9zNVSzpwy{Aw^qDr*bESOS*F9N>qIOWdN8<-*a{M>|uWnW~{Mi z&&ti4H?#FYJluRR;RSZvPSn|~tgNZ&4~>lk#uFFw*s+MQG1tfK_8;nQ*=%?>-g$a} z4{fa%BoCk@I5Lu%gJUpqDE{Q<5Reh3g*4#}Xn3?&aHaNxLX=Tunqo(D^^$7;<#U=A z`>iFKLPP{-#&A``LwS(dJvStRUcOYF{@M{-Q=^AQ-v@M-=Q(m+(;RX*7!mZoSEvoc zxfL}>I2-s~AQx=+7-ZR(ptAQyW9LgC{3FTHD)+ zlETf+JvKgG^5gScsZ;$`z8yCePaM%XQ@B>fwj0}?%B3NYAPqFph@R#R&K5>c&$^k)$`zj`$9rOvEaq2j~;a^a>TifFP!o5$uvwo ztm~H>u;5G9JRoRK$27@cm1hOU5sgU#~JbJMBE#WyX_dQ4S`?7g{U&6+hzelsk1d$CHV zrQr~}^_H!8{C-iRnQ$1(Yw_I?ijv^O(2AR9XTLu0GEu&BqV^8e11WfB@Lr@OBsnf_ z?p$BSA4%>5?`dc8sw0G>_2ELbKDAjnJ2^xLI85>p08;cTSJKgle(liwq=Xpn_wHRl zZovw=QSUg=Gz|W7tiY1K@w*T}i9Y$}n&^Rqd;dXBay&)!>Frpk4cf`?h^*V`J0eav zOPBvoJ_vPo4<0<|p7;d=MO{O#H2JYM{`^1w&N1aL5UON)Uzn}Beb2;9?lryO$E4-8 zSLB5i6yio$`Y)n(1*#{OnP+P%X0$Mlz{d}t9xkBqq-cB1j9CJevf*6^-MquJwVAOqA3UJ#_bQ=b`l`S|LK&Um{iOQYWa5%EF1k{gaT%X3|H{rYtn z1t|uP4pBGVVVz5u0P5}YgyW6>OoIh)45zMLrehQd=(-Auu<+HZS6|^-rlp00C|w@W zm-ApkwJoW=!3XU!e?L=X6NMOGd2#A%jDK5X_qh{f*nz(8qx`^^!_n@?uje4wFf zfG2E^cwm*4ld}sFA}XmR)R;@uC5BtdXE;%=Ux8@6g=iloa;2mH(M?DQErKix?$A%u zHBKU_!-?fUki!P;G{;{*KKo3Mt~E6^-EV5j2pIu&fT-$dOs2uw^mao3f?-uZiXS!bs>{kLsCd-o=IsDiArM);B&Z2-HZ9{!?=UWCmAW8sx zC9(e@K>yc|riO#S6%BM(0zzs1{S;Zf9IIFNU=#c`0lzNWpe?O<`p0^JLGs#AJKXq~ zhPr*HhL_UvyuNZ1w5T&D_?+@ibxX|4-;{kBsRoPtM3- zhFfJ?;I#_grD?G6wT|a5MAtoZ5072wSg~JQzg69vXhwp*Nl@m@8B$(>qqd)!7VGWp zz3K50L>hEe?%>trd#B;TMBTjk#*+&qpK>Pu@RYlIA{x_p8!}5b{XRkfm;c3;+;W0$^O!X3VOpV#CUV=LjT$UC<~+E2O?b1oc`@%wPO(?88A%0zmS8v?VFf?2ZYW$AP!GC~`25Xj)Pmu$y>pU_LB!IB_ z;DdF<(UY`mSWisv6Yp>B#5x2nVZv*g|M7M!jXKM>!)u+aEJt*7bmr5i>W2<-KqDaz zak}X~lSu3WQtYo@4YVx1h!rFlSfy;DU@xfpXI_Svm$xp*!RUXaoc~mE#3!bP!R-*Ws-@JK4fG!q~kV*h-&|Js1^D6$cY5*Wm z!7e;|CWADbiM2{{0BSH%qLf$NfjqThgSNOuSqLk(W$#avf`6shV78w_`3ccX;@m-L z4D8X@4+d0$sA*|u7m7DPN87z;4-wY?G3%-SaqG1{ZUiodiw@);l$4Y-vsyraNRm0N zw{#YejU(FdKb+?rD8}8#kB{`pKnI2nc^uYl_FCCYsA%yf->@C=kvd zNmjYJx#f9(=kM+7+fc#(Uxq#fM}K{%&+mVy+%$!v%dof2-pLCH@t~uwU%$R;@BPt= zvLirRNPKaITx3yS4>EO0R!*)5<{KU)R)R3{8Z=~jkcZj)!L9S7U;Op!B2B;k7ul(s zo4;(%0vjcHj;QG9b4V*e0i>gP3nn4?-Azrs4EHSV%RZB4IQ`KvF~VzVkGy^VUhDMf z{Kkb{h^!qrz=8*|a*f0~1%)FI{v2yBK-?_9Vk=av2Z{ZUu7zp}!6Q$)dsofx#7#)U z*dW1RAgD@mK8%RC5D(VQ&JOVUHSp`$2YpptPZf9SKf&NXSnzla8iEDh7FHg2W#7av z;};&Au|2X}x>WvAefi*Ym8BhhZoQzWBQw?Y_l)|RC1Wbeqrlb6g z_%2YVu#i!$#~{_eF=Z_*YGg}w;)l(nPN}Oe)@EgGP{AW*X!!5}=G{tcB*61mm6gk( zf>i=eEusndPqISHCMHX3TbrCuVMg&RgM@?xNr39@-OI|$E9Rz~T0B80KOnhrp^p@b zi6r&g4=Ba#mh7Ul$l3(Vk88udIfuj|%mfrUEoWy+f@ab7)P|msvFwHC2=00X!eD`c zfoCiG9S6j06($ANgbrE(d`7+?4gn3Tuq@zbc?#*k=Eny& zVry3b^>L`U+#+TvY zH$;zb8c$70X@yH0i(d$H;y9Y^ar}ccZ~oN7X;-*j+I7E-o&N{7PKN4?t}` zhb#h#hhlSas;b%*!tK0^K8sh^tH?pikT-)Iz<*DR3Rg$07O50VYwPtBZfH%kyGcpv zg+{u-(ZdJ0kh+Xj^xR_m?q(1i_>VqtXx+|wK}Gz*y;-3zA(uvn z4ZsQmdgp~@O*vuI+IDLNP-3bObx% z)oxL0A9=%S6?Ww4(Pv<2Ks=&bwj4(|(UQhRn=V-{5Yl12@EezF&dI|gdQ9q4c(@Hj z66taos|rT6JEhQCw60h@clJ=tsfc&)-r3NMy}WXLZpFvPYbGEv@ZkRaaxN<|QPImt zV0!(YKmGVPb!K7)K*bRySNdJw)HPxJm+SrgSf~mZz8-3~Qp=Y+8aWp$;$mYn;jV<% z;-$WZz59;KN>5vxh}QZ^3lp`f53dOe#e6YVNMdNyd<`Nv4$C?(FOR%)thP*`&i(sY zkb=l#ij=izNY-tANvQ%PP<5kcUIG4)FeWrl!rKSio^9-;{`ff;TLY3Erc3_{LCu^%ofogg|m0rwH;M$Y+;(rKXpM*}&+ z>8Rhl$fRh4JKt0ae$lw?+Z4B*%TsuA{|dmBIlH)Qo%pGya}eIU5CIqf^Si(pf8$kI zpEw~bFMpj)SV0OqUCDRyQQP-pihzY!p5vdqyQswMYz{=(5OO(>sz7}6Z_iQa@#xm= z*uewBXc1`j`hyT(G;1{ke~xcrwdW zUm=p)cw3j>1z2e{w|A^ToKPLh7CqYMnkBSpM6sDs{dpEfx1r+E zGOl=o3dB|JIl4#hrlLiIz20pp=)-1apA-d_eyCHzME< zl<5DIy(JvlmZ{$)`f|1m$?3okyqUKDxQ-4zF3(Z+s~NZ0E4R35$RhLps>?t>(x2*p6F$scUs>Xw-@SmoND*F)uzO9Q z?CQVQdhmhXPWYS8nv`DP4gtgrMlQWW3ne87nac4KKzanN^$)_<9PE0jitRwT0()zf z@Ap4(GdQNjI7 z_vNj{C$B5W+L&s{H2^J0{UhdRY~nbMHe#HA>VBgedxNP@Sao%2Y^UpBouJ+wr6bPN zoejI%BM>wYps}#Bmbk4KKY^q%I8X_Q$4$Q{z}>HrW&uBZl(PW#>gw%XiCss+jsJwS zx{=31HsXeHN$6CIrEaN9>21$>@XLiC9&IF_2Im;h6t%Fr55HKT1yvVa>&ni+K z8GBEj+yvQ$4ha;3B~dg8ruOsmbDbVNLNr;Za76bY<}|>k6%_XBR7=EN08y|;Avs-cBzJUyXm~ODDkTs; z%5M}BZjul%y8>~&bI>*+a%EvZ)iE=fQ>@I1eHU)9Ar%&qPq_~eDVHsXFZCTfa0uOMyLJWTu~<9U59S{OK{oC8 zf^^!DN#c%(6XuBQ$k?N&S2o)AEX%d$^|HAwn>Xk99@nJ*S6!1r1wz2m0tpw0U}-t} zNRo)`yA|@Y^GrrXOvehrWMP2{ccPCsY zJ{<(zGSSBip-1gMe7NFu(8|B_4eh+`QG0ck%V1<{aA+v8kSgSb$NhIoz=?@m6`-3G z2m=WhO8L)v@GAL4k)ev@y@_w_@wYGMf2>@_$aNN3P6VZy0gp)i{8F1x>vDiK$!e02*Sd2T27HaCmk=C>skzMo&0IR zd<;7V4Ef5evLIhIm{1g|r<7Gii0T|*Jt48c+=1VCCrNrvyQb#5fUp zN`$i$t9(WtP`J6*4ZN!lnh=c!LjkgU?u^0;K|Z5`4tI|q{kqsiYSi%0K#%p1qo7(W zhgh^;B?00sxcGSla1mQsw|zSomQ?RFm`%@Z?@r>Zp!P6f6M~ENqJv}gigNSmBcOwR z-{m_;evGwGV$mexigE@LEjN1$PbbRj1~04nuoraUFa*b&bMGiqXL@ljB$!5?2uVAW zpch!c7}+c^lR_dRE5VF1y}p{jWh0|i#syyD`|c=hZu<4T>t$qE7$a~~DTGSg1mu^; z-Ze1dX*9DJ9;_KBKNX!>6DGCRR^ku75?R$sh)y2#U%x%&7EJcS5_wG_v)nyAY;r;X zs|!SH5isk_0eE{7@UEm7H&6i@g_wLeD8Fw)(KG^wf*Uq0)xV>(6AI0B3A0O!=$5Hr znvIM%_Lg5Jb|1<((SLwof{Tj$FNCtv(0MRkMpFY&1At#t=o{ z_rL5|GIR#((4?qAQBXYe?<0e_feg2};eiA1Ff?F-IOCGfd+`im4rpLv1Ado}5Th#7}~m2K^vRs4|~i-q@_5E6zt3_`+? z9wGPeWlZq>%}FRZ+2wcpu0gvf$MtEGY;#5o?#ves!b=?f(&D(Mlyj6MRBCk_RJQ?%v)R z&-`b`s2)8-!)u^J%hcu%96Dr8i-q1+aC+iF9%WVh2TD{HvY}7O|0pkU17i>V!|s)l zm9;ML`kIpC;_rmk8X53rG};6){s39K0MC z5;Mro$M+6!mMZ&xq9(QkfN*0#!nJGVcx5}Fn$SJF)>yZlot>SNon3gm4Uzg*;HOHb<((kTCg~Nb%ovAe+OF| zTL)V^u{&;#u(aO?k$@j#*z~*<;{)65=K=pEt*cldnOSt?Iz4*wBoG1s1mpG7ET_+$ zA%no=S6>q~LbxOpHb_sy{XM#%C@d zA>Ru}708K9o!Eaeb+WWPhZHxaqt4e3kT=XOqW9=0n}J5kw!oE37E^JRH{;{!XeewK z@o3kJh#ZF;$>#rmu|;VK^I}t%c(~HVGrcQGxRC%Qh`CsmS%`ax;jek0p?Hv)Q`8E= zBQfJSGxRXQ`cCkF>A||$|Em>KejDRuJ9#THjtVS8#)i=0arK(O8HgB$KuTCd1H6jF z8ziY1RN7K9fe-vur6i!C;*im&>v7`lpq;WR}><^GtL2?5<#!|xr;dwhKR-ft?OkiM4t6* zSxDK*plCfvPA-F-XAE)pf>W3jihmgp6PdFe0qf8pTe(S0WZjrNru+3NMmvD-w?`yT z!LjLqXwEieZ?Xr*D=Kp^s2=uLH;M?|x{jabhYw$b4M4acta$O&TkMkI7uLjY(=Xf@ z$|6DBTfyhBZv`9PA__KT`818l-I$o4`A+@0jQ7}_w|@PqUFAj{!iI;aLpOI6dObv5 z9x7Q*F=o%(ga#ZPUY4;o9J9|g@W8fzrwme#

8hMqUwa_<|V~Tp;s>Wveh4xEMpGgr|gY%k8H8 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

J9Nqxl1gboGi3PUa#8y_#o$PlWD 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
post_commit_volumes
post_commit_effective_volumes
is_source
-
transactions_id
+
transactions_id
< 2 > URL="tables/moves.html" diff --git a/docs/database/_default/diagrams/summary/relationships.real.large.png b/docs/database/_default/diagrams/summary/relationships.real.large.png index 5d72e87da458d81e3eb91c090c2fd77f19f66d58..bdda4e138f247e831a26902970ec0b987250600c 100644 GIT binary patch delta 33111 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%0A9M00vj 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 WbL%}`KTyVh!-kHuJvd~_FaHBV;y)Jv delta 33104 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 H*+2Xr1LEr^ diff --git a/docs/database/_default/diagrams/tables/accounts.1degree.dot b/docs/database/_default/diagrams/tables/accounts.1degree.dot index 3130804ef..c7dc9093a 100644 --- a/docs/database/_default/diagrams/tables/accounts.1degree.dot +++ b/docs/database/_default/diagrams/tables/accounts.1degree.dot @@ -49,6 +49,7 @@ digraph "oneDegreeRelationshipsDiagram" {
accounts_address_array
asset
effective_date
+
transactions_id
... < 2 > diff --git a/docs/database/_default/diagrams/tables/accounts.1degree.png b/docs/database/_default/diagrams/tables/accounts.1degree.png index 15a6958d41df6698d5bfb86a795824c7f224c71b..c212dfcfc76c937a8a4572c1cbec69b6c9cf7540 100644 GIT binary patch literal 54640 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 D)t(+6vR*r~&M}0>;l7l=rfKWM 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
accounts_address_array
asset
effective_date
+
transactions_id
... < 2 > diff --git a/docs/database/_default/diagrams/tables/accounts.2degrees.png b/docs/database/_default/diagrams/tables/accounts.2degrees.png index 886c587f491a6cbf974f145489fadf5bdc09f95a..79ab8c67558d9308ca7dbf8946c3d22e26c387cf 100644 GIT binary patch literal 58959 zcma&O2OySx`#ye$j0TEOb`+H;mAz#|MyV*PjHryVBcmuqG7BL)BFQd7OGsIfO(lC} zukb%EJ@5N{-tqnZ{`Whc_jTX*b$_nUIM3rej^n)UXltr$T(@lHq$7vrPq;)6cU&-FXzHZ;#h^gOJFHJ4 zrw%Sl=imL}Yg)m_?Ut9U*A^7!nsDU#J3c8;yP2L^c8Xt6$#PHLi^Ggre#u+buHS1z zy~RrDbnfztAbiRD+P>rjTxve5x6$DzqI$haig#(bvBOpMF-2lh9?Gs;Z;30?Ju%y^ZWbyRBXo^ zb&W@!>_0|~Sz@wlWqEeIyIj&~n9Jw)@5!36;#s3i8=IBor9e(uqXHX4x_ftUlk0ka z{4g;y3%h^6=ll0_mX=~>6+sX=IfP?`2;1hxxW$%bA`_RkgL3 z-{0P}X3d(KN79K!G2-^U$>Byn#$A}0n7%yLzz`!6JjiNmo}{N=x_B`yJ)J?JI$E5k z@65%EHK)&=7I^UZ@y^i7YbA|sbYHwp*N~)Q7Zw(Djjr}|7P~1cE1P7AadDmf@&3-% zty?j~va&L6Zf*?RC&;-*C zg2G$86L9zLtK#A=x3OZ}qR62`pJOC-H8iYeM?1z!S7f+(MMa&*f0W@Do;`b}si|3D z(~*{%s{KuiMCo(r(${NC(@kCOvoSAU&j0%Uj)r@FeqQ~7Kvqu90TGc$VpmMm)ckTM zb1Z5e?A1Sy`RN{K+I!~2sZ(D^BQVr^_wMc7xl`xy(Roa|+b(hJwyE_i3r(e??Jq;` z-#0&hUYUCT{{8dafwI}z+0oI_)Z9&(#-3CC#~d6Ea{t5+My|eETU$?`K5bDQJw83% zSYQ9Krl#~tHzhak&YidDvoC*tTOP>yCH?oWcXYwQ!Mk?tVx#TruS?X5m2t8?(N%RSXWhE41;;y&CJr}g<`i^SDrt2XJlkF z)uJvbDXDO~s7Nxr%3L_|-K~v0J|}c_EzHf2F(yBLEOP0qfWqyYH_KjJ>89ouwe4*9 z@+C1vxa|G=hK2@SUS4{I2ZDwV@88FrDo}3~x8n`3I&|PbV$Juy-rjrcl1>e&#{vQZ zIy*b7tE=nk>aga1ZEh^7E>fM6W3NoloqO~B@&h4bqcdmD96zog^iaZaFx$Lp!^Vwk z$>jC)^h``l+7y-tQer8or~=uNC3 zy!*n13x^JuSRoE%qwKr}*BtO z>Ct{uN@>j_M`!}e_Xr7DWa+7g3lihaw{CM9X}up2(VQ+x{V}Dm(DlRJtuit)G&D4~ zZrxH+Qp(PIb^pPG2d7plE?v4r!;S5+-aR8t_xnK^(uNJ#>w9#c@%jkp=YMKxx%#sr z_|Tz4Gcz;w<^6Tx>OzKvNf{YS3qzR<3X~hSb9IeYOA6*QZU!p6>YglvPP? z?m<@8D1-oW9LlLKDlCj&>me4oh@(5*h40XZ1P32Gb7mV0i>0NdD)s!*Y?rxPXNkN1 zf&?F53J!2%ESWdu#`>lvElthQ=~_(JKCj`{yi0x6G5PuV2p6^W^=EP|>$GH-lAb&{ zB4n6rxyirm;)M&5qP862RmrT13JOl6?c#`)*o;X@ygNhZTQ6m9Qn0eNF2B1qG&Iy6 zbB!6;vgg#bnHGz6ZTnfo@Se``<9`1B>FMe7*JQYVE{>IErl%KqEESfPmRi-|N<_p> zNZ2kaI*TFC{`&PiFYo@yYcpY4oZ7g!xc>frl|YW3o}L)7D+%E{`-%DFXOX^Xv|C2T zXvX?5@uFBdqoFZUJli?c|6nJg;Z)P8`TodHPP$t~teS7GXEx#&P34u9Ej9LBYOJiR ztgYS0$A_IG&FT4j+;7XCTN_zt5QV;2gjiD1aU4}uMeJ8sSND&K(#Kr3)qTy-FYmFF zT^!Cq)U&*BA%IQX4*REg6DyGq;|}1+VKuEpqCTG=8W@O?aX(X7QA5167G|nrB&Ym! zQZzRybY7eMoS#_dpX8$uugxE3eF4|TvAB3~@QvZ?3)hfrt0^cbe!09l)iuyBJyx

a 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 $=NsC_N#};6|*^y94YP!7i#>|Bae*m}galJ2! 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 QgA-XzvoX71;JMiZYf; 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 IPg(JQ04&o*IsgCw diff --git a/docs/database/_default/diagrams/tables/moves.1degree.dot b/docs/database/_default/diagrams/tables/moves.1degree.dot index 4eecec0aa..3f1e4b504 100644 --- a/docs/database/_default/diagrams/tables/moves.1degree.dot +++ b/docs/database/_default/diagrams/tables/moves.1degree.dot @@ -35,7 +35,7 @@ digraph "oneDegreeRelationshipsDiagram" {
post_commit_volumes
"_default"."volumes"[2147483647]
post_commit_effective_volumes
"_default"."volumes"[2147483647]
is_source
bool[1] -
transactions_id
int8[19] +
transactions_id
int8[19] < 2 0 > > URL="moves.html" diff --git a/docs/database/_default/diagrams/tables/moves.1degree.png b/docs/database/_default/diagrams/tables/moves.1degree.png index c4d89080b0b0595f8f675235519853ab7bc13d91..f1a8d1c93b86f0a8c6e4257bd982fb7e6114e0a8 100644 GIT binary patch delta 7971 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 diff --git a/docs/database/_default/diagrams/tables/moves.2degrees.dot b/docs/database/_default/diagrams/tables/moves.2degrees.dot index 522901764..79bbc3fe8 100644 --- a/docs/database/_default/diagrams/tables/moves.2degrees.dot +++ b/docs/database/_default/diagrams/tables/moves.2degrees.dot @@ -48,7 +48,7 @@ digraph "twoDegreesRelationshipsDiagram" {
post_commit_volumes
"_default"."volumes"[2147483647]
post_commit_effective_volumes
"_default"."volumes"[2147483647]
is_source
bool[1] -
transactions_id
int8[19] +
transactions_id
int8[19] < 2 0 > > URL="moves.html" diff --git a/docs/database/_default/diagrams/tables/transactions.1degree.dot b/docs/database/_default/diagrams/tables/transactions.1degree.dot index 5fbecf18b..475911a66 100644 --- a/docs/database/_default/diagrams/tables/transactions.1degree.dot +++ b/docs/database/_default/diagrams/tables/transactions.1degree.dot @@ -14,6 +14,7 @@ digraph "oneDegreeRelationshipsDiagram" {
accounts_address_array
asset
effective_date
+
transactions_id
... < 2 > diff --git a/docs/database/_default/diagrams/tables/transactions.1degree.png b/docs/database/_default/diagrams/tables/transactions.1degree.png index 5dbb51b85d0f45f8955c3af863b498cc05def26d..b7e0537d38ce662ce0d4fc9cb263f7f4074992c8 100644 GIT binary patch literal 74700 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

(dT?Vb1Df?Xx?_M{af_d8yHYMp-Es{I`H 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 Ue>H!}UlN%Zni^cxU%2J}0fXMR2LJ#7 diff --git a/docs/database/_default/diagrams/tables/transactions.2degrees.dot b/docs/database/_default/diagrams/tables/transactions.2degrees.dot index 1303d9cd3..5c7dfa2b7 100644 --- a/docs/database/_default/diagrams/tables/transactions.2degrees.dot +++ b/docs/database/_default/diagrams/tables/transactions.2degrees.dot @@ -26,6 +26,7 @@ digraph "twoDegreesRelationshipsDiagram" {
accounts_address_array
asset
effective_date
+
transactions_id
... < 2 > diff --git a/docs/database/_default/diagrams/tables/transactions.2degrees.png b/docs/database/_default/diagrams/tables/transactions.2degrees.png index 010febb8c663252a647aa66a8b8efb85d2a6a0cc..752460a43275c12e9c2339dda13dfb3e684d8b3f 100644 GIT binary patch delta 18634 zcmcJ1c|6ta+V7H-24h7TQ!1%YAu~~iL}bhyCG$)&{UoA=By?h|l7G7#+eTAEC@zZf~*;llM2Dp7mOC9rXS>vUpne)J> zdvbxaxihtQ1XT_eBuk;XV0GtFa-Sm z^)qE7{wT3-%BJqoeoi*|Xu{C&8+ZAA{(f#=jJ# zX{1QGPsq44-IPxmoA_Pl=`|l`GyD6*d~ACCj+0(DI{QC_u$GpVW}8>@^6*T|%=9~m zsB8(EnsT~yY3Pmj@5P0Mf&PBAfSWgOdU$x0mzU$=jvOJ5;NgwEXPf7Kbk5GsZs5AM zp3cp|fqL_1Zht(ACEj@X_b<+)=BEt|zMPR>cEOU)o$M6vdgC2i*0Ys~N%fIbS9$r# z2XcZ!LKiPy3@G|EIy@XCV$;l}XX2yqprk|hLY*DhZXl=!PTt%h2;fZo_adDPG zfz$W47abjXCr_4t`(`rrbJ)Ksia%_}(ZL^``N!_(}Hg+y}gGn3fbA&qR;$%UnqGmmIl1cOvl z4)E~YkB+8Vw{GJoo0!YT&!0~l85yal*uL}M5FH(@(%Cb;fE5hitMa&U1Y5g{_E~1; z+~nl>Bhgsixw$!JJ_Gu$OLG%FWe1spFwp_-o9hU#Y>ltd5Y01G)iTMj3)*C zWFq+a`RVBBN-JzT^Sl>FQvAYHO1>QO)({diQ#Ll8tgY#BSXy2ls(qA|p8m;1z`)S3 z`;E8q561DM2bQ*;cw18wT00cz@Bgu?>cD{mo$c)>Pn=Mjefsq2@0k(j$=;KtN@pHQ zSXx=7r>2USz7I-GlNohNkzC=BrJ4d3nOKh39m$1H{C{5)u;p!WbABK7IP6X>6#hT;=OaD$NXL=06gB z+N$VD5B1V2&PUqrD;l=!6?GOO?_z|k-*bkNx(xMye?FxXx@Kpcpuz}+2wP3$A^n&x z;-Z;Zwpm5!)YMc=OiZ4z=kGb&z2W;8-;R!s;@@(uTX!ie{Qg;T=8@EL6@Q6@`$T+w zCeF~OrLnhHDZQjtQ7uoNJn8M{@sg4j6dND0Yg))`Pt#|h*r(QZq$0@ay?KjqEx59{|N7E~g{hAsYiEaLgiHS*l zesu4K>2< zqtECj0I;)g}UqzkU*DVs5!JX9r(yv>Q~IlxOeyS7cXvvga`=< z<-UB$%g@iebR~I3v#P49v!lZYCn+Pejv@Z#7DxFTSFt~#gDhS zy1k}V65rvVZK&NNmsrmtNwq`3R^a@#N%WL6+*aHus89cJyk^D@sb0OAFJvIXS~Fi~W`1 zD8PWIP!RRuu|7jO(u^0sG4aO6!oq!F>SA`$%gR&9 zvh1ClrcxgHj1CUw;h4xbZrr3+Ym&Od+T$S_M)EXc2ngg8-fr~rf>LAR)@|DYH}8?+tN8PedH774mRY~d2y?d?>4vMO(gOl#A zuCB-IzvkrU>!_*CFOFrE+l0s`-uNh;`sE4A-b{A=@a*kGl1$iat1(fHQmU>##LJsb ztgbjZI^sB?u@alt$(=j5&=)3cF0j2d%eb|v>HF=*wDk0jwl=SsdIk0dJ^sVqi!Ltm zS0i(#4e4^9?_)aWTXX&AVT*h;wxU#sWA}!(Y?QK$0QXn1Y+3A4$v9Xuk zrML!XEJ!;;-3ED@)>1xBnW3onL3hW%?BmE=`zxi$*_m(-|JJ@~(%7k!U{3|nVpqqqO6GZl29-uXXw7SHKKUFE0r$oOp8#zAlTFChR)Z zCZ=2L;w=>wDQ5acc{c4BF=BZtzF(75Q!T!|z1rLQvw>{@XhR{4V;M`oe)D$v?c0Zk zhv(-f&0Manzs(`RQCn4IUGqS+n;=$;Z{GZLx!e1gWkZtPj57;8{X}nh@bIw3B=efB z;c036gUd(Tgeg2?`}_Nan}0^IVQa^m(lmgZP*O~M((V)A$lq@FMMU;vreP7E(QEg` z8Ot+gYHNZhNF527T=*c{HZRa_+;~=7`wjk2+Y2))*Sq22l$4ajF`KrXwzWD&B|gO% zgQ)-QtgM&a`=07#$Ng(rjMTyj zwW-Q|kKUJ;yV{UQ6IcC*lG1Q&EG&i_hoo0O>+0&Zb#~TbW&+Ob-@o6G!z9?t8&K4> z{E;9NK}s?R9Nv%3gi{{{6(n1m${K9$wy4Voapr z?d;Q;Lzt{)A8w)cl~h!oBww~pFDxYVw6!DkoP!^yrp~V{|EAqQB8eH;*`dE!V|4-u z8>$NzE{L$v29_1Ljg%x4Ter@9 zZ$FAB>8l7k7JG$f|9&AsLF%kLEknbMq@*S;3X)6(ri+uCn^BI%ogm^@1pwWfH*dzr z$NBj9x_y4z>RfL{6)JE@`><`@%3Zph6kq0gI&>F5|IhJppRtyV-3p=Hd-uL`LGL~@D0dzVo`p<=SH;^&uw?2)e>9UaC#3m1VN8?~<$B$Z{Kkv9SKZ!HV z!pdsjnWy3&m6tacfBc%ic;$!k^7i)jzxMo;vNAh6JLmyp-Rr?rvS*tq-4moJzTXnFFK zEEF=m-+%mAP*Jq?o5Datw+2m+!_aJak%RZF9sC?!X8K~-r7$3XDtDg5BMuvBT=hvgUdGars9_`OtKl{J(I&ssn|uUa^V0eeUdxt-O$)mZlOT zxL;g6mcDmnU7Z@vlyIyH^}_P6ZiQRhkEb+ZG(U)o>wbNuvBf&==~Il?Twv)q z)yqy!9?Ofl-{={LA_D`1cV%Vy_T8CJo(Ner@uWKdAb$ApVe`7O4~F2%P~m(#Hp zUQI3uJCPX5BI)5G9!hoosdmOY$*mOV;fX#vpJRlcPOBciGIe3oreL(?8gZ%sdPWY8 zuP9HD%>HF|HefOeFU^pH zg98l7Zvvnt%dklM+_~tuICd;*uT;Lvp;%$tBPjaQrw{V+NqNp446odJi8-+B@Udf; z31{abjE3o<+6Awbp0dE|>gu0eMLK$V9=-23fS-UUoA0*KXseqA^}(w=D~#==>BKQz z!7q=-Gqar|(5PiU&W^Q;r+J@`yOT&=Ra?8y-=V-j>QPok20J5TL#pim&RZln!pNBa z`}c!w+lVb&&{|K~ZW`jT|N8pQojX{M`Pz=L$O8Q_1I1^_?(Mw{ zmP;Gh_YJkeP}(=y;c;kTVFB8}JBFCpSPj-)Zt84oY#2?cKi<8Q<%=5Y?{6A1#v#fw zDz*}SCmcZ2KnGOhB27U-!EgNLOR;Bu5~XEILqo$0vkKk=2Z)ODa!F#vpsp_^A>pdv zh)jpgd8j0f?;-_bbTmF|J(HA_bar;WRP&&~u}>ZZ9pfu^L~q{9Cz0AxQ159G&%5b+ zI@;QdvS&3kuE^r0cT?^2^8|gF&^4s4(>^{cHF4q?S!4!AUXir4v?eWq=+m-_^78An zGM^GppC%;KTRi#jf8YjZsMXcg8GOWcSr`k7iWV#`dh{@E+qTW#{wKLZc63Cxh*^y2 z;qRJST8#}2YlS$aeZGulChH!I1mjS)mK9=65IHZG@5@F-D%>G0EsgUDe6O^=!3Wjt zf7^MeMvKtWdXkj%%(%qpIVz{NyIUHZ3auWq2uIQ`Pfc6ft*g+L^_bnQkPw^&6-C7f zVDDeQQjG5C>gzv}xN>Co?!=s&84N;DqHq!Q7wzBY=iT|D^!4<3dHVkrgA&*M&Kc3u zMnkh~Oa?@Xij4g5{=FIh;%;5>CZ1mlTJ9o(aMm+Caz2d%^Xi2Ymf#CAT^%jwl zksCH_z~{t_@RE8|(P!IL*!=OMqLIl92MuQM=BeKoyq@BL!a#qqEcamBaD6!`kmNvWs0XH{Z+8pOu^}eEyyP z=g-97$GfBBlJ@o0pIMBD)$Qp~fmqpw^EVhtGingD#GD6DE97eH>swn|0^1O?qs^gK z8f-_7905s2cOU;0zGg zBui%WL?$M;@h_r~BFTu?aQ*5XlCP$+va#YAjO1cZQnj`H0jz|McI8Kh(Eeze0Z2N@$sy}~-F`z%Id?E}ezsGBS&Ae>q@-tM zUA}y|#AT#`lUO?J_FY#gQ0`-iwJ#c)*$Z8?5ytEz4--kgThmfg9h{wiqcAfK3QR9u z+Bf2`Lf0}zG4Eg-XG()6qqvDLO8nxdhzAcI+`qp)?UWixW*6v>uO<-kRUiW3Y@pD^ z*)e=4NoGo5Zaaya_=#x^_|JinQXPBjPCSW3MgCD|EiFgB6gWp6($B+_;m6_ZDk-cb zHLUsRL6R}G`|=7qkA9eGTq1S;-CFWTZ=kBhYOP66PuI@Sn*}1i#U^^>&>@-)r|jvs zZF`cE(%NEJ7u!SbhPe?D5m1UAR1FXuqE7D{8#NkwKzQCcZzT?HdtVn9?_sBUyIM;Anq5FhfLIuul~7jSERG&H{gv0Y=AmR~+K=G*n|-kq zF#$uUz!7(euXNH&0vPM-u1)^xCZJz365$UeJ%umTurURnOd%Q%9XtrYo#rI)zs9WDlErxo0^+5jEeOLef?O~ z6me0}VxMI_hoB%sfMBdFI#g>z1B;N=R;^c_zwA)J>#m(QGuy(*+WEn{#q=pR7D{)A%P+A^z{7%Kwa1re26ybxkM~2fh+Y;1NWuTFck67($Yd0X>vP%o-3-#)yZjeItyG9+L_Qls?JU2U1qo^kspB_~h$}tQi755ZJTybTQ~s=C!n@vVcegd)n*QUm!-{hr>?rnD|B!$#16(%Jvs@iax!U zi+lI(YrHsJ&2@E(N=giwUu+Tp)*&!C^Z-5X*enmqvVjZJ_T3qUwLgOQ?%SthY|L+f zhwAXfjUSB_dFO2oUf1NLlMhKMR)?YAz-|I{W>P5!+*mTsL8P*&*k3P zvB)&ujSL-F73cm0V(>oAGb3*<`d-hf_(Nc$W5<3$V}`)_k+^@@40t&yao7jv?n_q} z*jGL<{>7@u+1CYUAijW{f_@(wk%{*d5E3$cVOkF4vWGpWDO$*yiHm#JF4@nE_b+Ml zekpXVs;*XPP$!jkC%B1t&V7evhaMPy_bx0Bz7dD`YzPynX=y^dY!J(!mh52+yH6BK zeTU}<^}8naSXOw)Pp#XDiVW$$#T0Mezs4>TpPp`oYQidPVme4oOPky{ayOJ(><-|j ziG|& znU(bu;05&d`dTy^Ix;AFwF2oL?dMZf6{^(CNtiTj;j!J+wN5+-U&cgW`{s3KKRR;bw(9PF`0!!V4nWBkp-bNnt3D7pY}57} z#oW}SdFIR+GB7HpcG{ZehIfQq3u75}U`%v$#<}d|$B+Gp4lNy>e6tE#tP?g1gI-6c z6$Mj2&`qpLYSox$>tyIbqLCF6a-T2(h=EF^&RMcB$hz(t*L{tePoHj9ULc!=8p)^Y zff!(HKysJMaOYrWFE%M%Bdh4{E^@83uC;Za{{v}a1wwaAbF+|uzCVc~boHIJf^2^jg@_S}9#8WvNBI<{i}4>&f!&>_N%X=gzeQ>q^5df$2wP;Lv*S zhAQ5L4s?O^t?Y!nJX&Ays&k-J_E}7wy}Z0u^AkOQKCiA!n>#o-U}}U(uP9Q(#aIAj zM@v3xLcH}wpSX2vGbjhc07SZh%J6+~+#pr}Jmb9Ieel4m?*k1LRe*L`YG&p`5l8gZ z$5*eGf_AK-qznlOfi0b#ot<{2b~6nPVA+U^2gaQr)uz$0v3;r!baiwtW9cKA9Ic%?3#LRFnDvF4Tib{#pr*!1z=02$VJRa&Eju)ojU0Q`H;Mj}S)(1v- zNQL>ZS;6`z$95~+v4TtnRr$%oGx*3J?(S-vB1(LgV?3l7*w`e9l_elQH1sNL4Ue?z z=vJ=&_cyk{5g|}R5H=ed8*3j)^ACR4ho96+WEyH{YCbq@_Tk zrW(&khrm{!pX~F$jj`9%+S+K5BV^rj`hmy`w43Ifw-psb(81CB!MaZGJ-T~$9|}xA z->%qoOxw}%MOxYj8T*1!q))-BU_PWbQ^840EO@P7AL#4rYrfBUGc`3K0*w-Ttib7= z10w)#%nhRUD%dL2esp$N>F3X%harf4=7nZrnl9!`$Gvh&a_j4{J62G$F=fgE>00aS z&*fM!aQW*UmBg^D9m0n{Yk%Rw#fv~*CnHi)QZxoopj`gjw{QOgUdFqM_L+0zpNIqw z&s*CM`_b=mKhqSHn>&jYvbMH1G%%3AwcT;FDNXxP<60`JYC!fard5%kB`U0FXJTS+ja5o-+zwmHLb&mo&#aJ9vFz)|I*p%i8h0+ z+ctfKYu~<0z3*=jWZ4}bb!EPf4xDx8@sH|BI;yJByG1XLKL`jYFTJ+DHA8=28wOA+ zP4p_CKZBBgF}a+JC1n8t$JOVw!WYfpmOfE4+l@*!fwEXBrMh61%~ z^q4YA5ls>*A12XcZ#mh7C2DI^+*IxhoUea;656eeu(*JLfUt0$&;uYY^*mIH{y)Is?&y+5EE1AG;k`KBj4*V$%P*e-&*YX3lt0zS16~n6o5hx zRVQ?L8EQ)l3wnuT!66~r)TPj&@MMC5f@*}MG;Dr|4j)VCPD012EXfurfy)c{X2Wt? znaE6irdSuA|NQy$^z>0I?wQl44Pce*^cB7`_q|=m*!!Zbt>lG|w*hpw$gWlPqhS$L zRI7gf-oMmD!gKEQd>alcsug`deFexvANbxjBq4u4FK>*bXI@|+Q2{idarUg#;_z-V zzw~80c~iZN2lxvtHd!^UkXZF8<%9oGEA{&p{Y#gTvuKn|!9r#qHZu%h+yGqq--dS| zvi-0rRtoYIPSw#%HQfGQOOqA;x525=7MXo8=IOCq{z+BXC@+O zxX5tE&dv^Nlx18ZEF{D(;t>Dj33$;UGEzX3k$i@Qcvo#!V6iQ)U9p5K;FCBOr)U%w zAw>9FOTpHE9tXh)`dhPwj13Mu;B8-DUwMiDS0H37xE89asxXVL%ujFymfgW1B(1LR z^GND|QL-=)WxP=e54$n%-o1N>S148U?|Syc3G*TA&0>1Hr6L(c*l~h>hfs>7C0EqM(3wt|$MgZ;_1OY(1 zc*<|08x`ekZEa7Uq=GG7Z-EUPs)^h3eQIWA>qBQlf%=u7CB&*KHGrIUV@s;x8hm4j zj{+=chy!@Je%s@s0Vx^2zW4H?tBFZ`e*Up_KW8L>2k*I1lh`S1)n$&re|`M)X%U)V zrYPCQg3rN9PhTgL60Qo=k504IDCEqlLr=AV(1GsC(&4}Pvh zQlzRXmH_G)sEVq%wPP9~G|ai-kr5$%Md}5l)Xv0-;XId{XJcO0_BeoLz`=$H`7||k zI~&`<5#_b>Y5;q6Wdj2PhYViTL45h}VeiqSTQ=N`sKbLoDD2GVk5m0MGxHhRa#dB| z^nwJ(8FlqLswuz5T5HS7@^kD(R&x?ETU~%aHXm5-oDZT<&wC-qm1yKQ=8uQ|7Sy>}hoBjAA5z(jID(^?8B`Vw4tF&1(AszQB?Pq~6}!~p>KNktG1 zsuvt$HCQ1H$4d$D<3U7mz7&L4USO0>Z6*Y%Ad3KX0G8lGgK)+ky6}gSHpvc$P@lO; zU6%oIbXfFea4@WL6JXZ|57^k*y`gLNYzvj~K&XyE79N9+me#$ycV!RR?YULa2eA)B zMAh9U)FI(NYU_-$Z8mNM(gjgfPa>7p55Iv{IkOgf2^$lB4b<~?U`!7WGAZ4|zBR|< ztuF-(MR>(<9TBM!aT%t+mrKSN91{0SO?c2`i;B=@#KpvfQ9SN=rW?`>ddo0;!;28_Xj2W^1SP;>a{^MkSWIEdVMV>|LU zMe#Fx!LLgY=0N5wb5HfS**Km~ofsBUlX=Q+!@fBy|KNa1Ezw2z92 z5cHh0QBvT_L^Kf%i_>R5At?#k$R+THCPMZW=}(!ZVQEiMpUbtnmqa8ClXbDtN zkX9gKrS(*#y8T#P7fO6KcsfX_l4COv5`2iId2|Cy!I-4JHKgGaAZ1B95U-H?L$H*r z?GzS@F*8TaBf8O7p)AHn^36nz$*{oZn83b(3^)UYJb5yJr5>z_3y%Iy5wCL`DZ@iw zO9%oJA8>CIGasDrkN6vkJedE@cFe0JIZD!PQa=wty8C`;8U^Zrk#qn4eHa~6?h=SK zf}^QMA1O!Dw4t7Yq{xVan5nI)>4H%Jzu;?u^FF|Fe*QDC`RnTs3ko)smLAvF*KeR$ zLsF#b=OM+7L(UH4lvYL5Ve0Pp?>4eQZ~*V!YaVTW3ZtkI;Ox^U)APi}G&Incz}1Rk z8&L(Rsi`XbS$D8j_rc+ydr7Wl?K|@j7W@`j1f;NlCJbij##65xLw2BG@ z{iHzj7~0!}sS7%Pe8FyYM;bVOW4 z$Og^rT{!Cmavvy1NN+epcwzs)UZ-JYI;cAV3y{uoVP>QeW;|e=qe8&Oy7mcZj50T& zD=5GiM0uhW00o9|NcK7vU{1oSu^qd1^VKU6dwcuR3S^EDPY4aoU!El8Y=k{QN>cRk zc>{^ z$_AB`l!S%VLI}W~R)78s1e^`z`10iuAdo|cG9p8F`A;k?NqWvbZ?|Cy`C$42+!lQs zj|W(mo|y?czkv%1KcopRf7l~VLqtuSp8yUw@MY@S$L$|t$#Q_ltQBB`SloDj|IFg7 zb!jFBJ~kw<+!RO&5d-*}vWkuF9WG+SPv&=0K_QyVy)8nf`ma!imQ6dlS&>)~K6b3+ z@=qfeIb@+QA_CGBJbqAzcALET4ym5uynZ@=@bDygeAlc$>emePZ9eOy&RfYI-LMtX6b5{)m6b}bo;S~$=!>O;D8|RXZE4Z|oP9LH3PRq<>n$~;&dhPHY2xe62 z*~KDpHSroaU+23-hSvZW``^a+z(^|o zNOSW}(@Q1B>)2znfTN)K_VxDOKRt*vSF9m_q4#By2P@`EDU?lm+2YAN9Dx7iWmtWb z?WUV&6Ah}5{viHyYcGac;v0HWTu8Yo2p^fMsSpYJVyAR;bmZiA@7Xhu7$(KY7Zo1< z6cM$&{riZ0`>tGBKu|hP%G(1R)RsmGs+E?O(4j-N2)k)kB5aWsf={xvv;_0;>^Wt$ zi|omqpR_Xe*Vs<-lA{DcIm_#R4i1`ITX&|ZL)W@qZmLunP*hN$ZcOc415GF&Ll{OP z+6Izvc#`e{=L(#R&!3-p6AD;r(1WqzVO>2vq-8K~fJ201*;!aV4LFUs6vo8pB_=cR z89+Li2bNg{F#JJnS$=aHQZ1(0?a*k^Qh|Ro3+~)u;Oa+fK#0;C&koE>6)1Q!g&nIG zS2Mbu(v~aE+gxer7uUrXy0)Xz&1r?-JcxwY(WIXtxTOLW7nt-Zaz*zAjz4geJj7n! zH<@SGWp&{KH$Q)^=?m33F{F~*H802Tu7Z!ps;Apb4es}Juqq+ZedUtdXWZS)VNLf%}4fHnW!OgR= zNL(!i-nw<#-Q7J!s$W1*kPVq5K*^yvSuaC};yRTcPs(^XwM|vQtKyu_ua4)F^)1NO zfBNTBScw}-R@eE`ssFc7{MR+28(r-Gq%Ny!YWSer?A~35Aag?!q11Gnf;2A!wYyox zpx8sJ!ST5LRW_=^doM zsaP=tiS}&cB9VeNOef0J-ieKcmC*6_tqf{3+4JJRB@vTs8ze|1`s-{|9zl@yaM1=* zfVx>{%)RL79gK`qU^PhSdV#PS?%*Jia<<`NH-KLQz*dBDmOxY?l08vwyLaz~QiKFD zA|Njzhs!(Lt%2hXgQyGrGc7T(52Oe8WMVRI$HcI3aJ)HRwr>6U^#{LDlj;ni507WS zL~3s@z|n>v*7PpJpB|GA(VgS$`ud$<^U6Fj!(4ypsJ9czBEMvHrKsKYxxOD8ftAJz zAwVFjsHqtdUx)2Ed|2J-_;EUyzhI{_aETpmpPX9y^ie(DGdn*&D$g}EUg4dGB#Be? z{r&6a=Y6)~M%Qh5fHx9JhJ~DEAdxKnF)1?E_xG8eR$i zuXlO@bOt>TOdr}fi>T8DkW<%#5bZo>QlaQ#z(WH=Rs>6*W{8@DMyYf5tm%`NzU0cb?4i2YLS^Ux{bG-oZN3&tH_B0D{E^=@vz2V zC?S>O4qFP>F+vL$b{+@b!mGEnqKE_d*bT|YE7OCA^m0=1m9RrKdjO6h)0&!@!S|A* zJ}k_MvJH6K6DqinB8aLl?PsACU|133;Mh1d8WG`BRL z6dxGH=A^_&iBN|T8^xWdQU%&TGdsJQzR8%32ahoDdYlS0 zRW4n+RGJB^4=_vW(du9|>y91K{{Cwj_HDCM(AO8|=1yu(U0W;c;^HDJCr4GZYu7Fq zlxe=w+)3uOh;1W|v$t(vY%F+)N0Jiu4ar5B_{(ti3&V!uq5d_hr%`mBmYo;5A+m!l zPhH>IibU(6g9yF{k~gUQ32_W5o+!WJlzbS6Az*b}TvzDr69|^^ z@88cL6$SC}zN0MNH8O_e1=6R^QzrhEXiv*ubAq9peaPLzV{mW~33X?v%y@Ss5UkVU zu3ul*5cwt*cU~Tk1#_N#I)q9Lsev^7{?--`Fh3HBapxcRx4Yr%f6YwFF>L;O8*N;f z>+BSllq?3-L=NML8?NFINJm3`eShl{x*SI1tCfFR%AG7{pU%$yGhMd_WX5imXZQ|? z97&NBTOAq}Hsn-*dQ|zv@-HMDhO5V+li>0iIXLPKk+s>p#)yvxZG zdyqdYvg;}YOb5nBVhxw%<{?=^sw;WGc$C|5G)wn|DKrU0@%RoM3b}Cu_>f_BDq&yg z68W<}ho}jaj)HQXK=fxw@xmSACU1S?<98zCf!T+EBquKK;X;zjHEb``{wwf&f!Dyy zaj6cu)eT(4q1i0JZXecTr*GoXa+6kVS32 ze-?I`kOYl62>`w|mVUZtg(fi)<#6NDb6*G_mTt$y-d?8A9Fy zlCK~+K=AMXQK0r9N#?+NW6tfQ6aP$b4LVSgEV{s$;d3F!B=d`joxR%h#d`vIS}Ib! zEAX~%*7|Rh1Gr|BCbjrK{rvy&&HwX%v?WKLaJcI`aYuptWjW_C4RHVU8kpO>jP5T4 z?W@7xnns05UOUMI=>I%K{^Nu>;w5F=a2Ts3gb0CBAgNC7x$ZMC=o{yDIKQGG<=8c+ zYvG_m8f5|h`3Ght_OL6iA4_Z9Mo%A~l*H}hO@3pGfIsku%qaG-Qwy3~Siq$k#=Rv( zM@v0MT@mmNrr)*VnjrmeyW(;a>HwsRKVfCVuF6Uv9k~Vu3YVkg=_tk#RsmfY0RrVy zr;sfZ5GI5XraXJ`;uGi}!>R&^;C#CVW`TevudwhO$fw`CzkraAfQ2Xzk0R;~5C}@= zJ+=F!F0uzr%0Z?>OHHi~7hW)0-rzjRdj?$6`uh&Z`cv2&fquz1j4R za`-SgFcfe5zhAm2Ay$n}4Ez@w%GROqo&U4nAuwW~kRdOI{Quqi_i_K(8~R&AV`FT~ z!TtNmX1TP_5>Q)_l&bGIElOTS{G6^X`5(b!&>=8@t#k5oa+>Pv9g(=l8DZQ&7hfbt zY$f^9Jq8IG8fr~_{@kP@*WY`_}6PLvNZ{s>FaXcX|&d}8K zalhZ1VRQ0hx=46>dOqW&o48uX`ELm3KNjCVuurAQT4|E61^O+}MtmSrU?D+4^w~%L z9KVxfek|uZD0yYdpWf8Uig!dtjLGRua~fs0HaQzD|Gh6PT<6UyCV zZ=g&<0)~o(t-5ok{`Hk<@Bwc2CPI<$M;q|&xZH+_hR@QZg0yrArUiHo9D=>V!nkNq zV0O(q{MD;h$YUZZzu|*S^f7xQNL*NKT-1VA0(CnR*SJRFSL7CVuLbNp6N(tQDhA^-|T zx&F>r5)F@Q$bIH-KLmCN1G0J4lQT2q+dCE(sUW1W)lBs!m`vcpyv98 z{s>kZ{s5&?5(Ai^5%_^j?OQ5^nQ#0DHkseU}6c{-u}ncmp?aO z2=dhz`Fps_gVw7Fu!+X?+4ChD%zlkzq)c$T7LSHU8P2YiLZi8OBpPxm8cC9suxHkZ z6T`i|NDcJ%^gzr>e)dcP&I*(e$4nMn;Om?0R8?0$eB{X469E9g$hCJDUCu&Q9(e?i zAP`>s0)|IrJ#c4`YsIGT6&LRW2&=Du(JTkQvFpnh#Bf$&KS({Q9dHUm(}<1L$Bx@} z3ga=;ss2F4ENwHgU-u`?`Cnnqc&hWbG$x-20Up2Cft;#BlmOrx2`v$kw)gLoaN8lB zY0DNO@!2y2b#*Q;vUq(jBI3>RTqnX4o1)gQT?_RN0gJ)@eq=Kci0~I z9ddm6>wQQ}fyVLk@E~B444nXqH6*$4sHk7KNo;1;hc{O{l;e`6ck}k*8mjf{HB$>Rrv2_7A|~Vr$39D=lK}P0{4szpna4t+Sz?= zxaR|R)Xx?owTZw(DQ^%h$a<%WlOc&AZ}0`5EtZ2EL;vdqaTEj_*D=({FB51)Ia9Z} z;S-7jXf7z)ub`Ib`$z>9d!$W{Ry?&NrF>`KKo(&Gc7pIh;i{Cvx*DE}m{!#|`htl;Mv#}S$jvc_LWPpm85(Lw{CO^0u0HwHP1Mvl4W|ddzQv&B2?^rz!sR`0ADw?_E z_#_?f6;fWX+y0^KhfFyr>{*F3^yp z$Sj10($dl4z5uf%`Ilu#Au@uF1oVtI!BovKN*q?Ee-z}|Y~$agFuF{OoO(uKVT^}j zWAf8=B+|UYj~$+`P0MfM{NS2CfIdOs?ke^VzfA)|O#IxZ%E}A?@T$l|_=z~Ou%WT1 z$ehNnygjD8B!~DVH5ZhfSOhM?R$H=+`P<;oqH+E6*QlhG{@brnIW6dl=l}NY8=yCM zt6lrci)^$C0;U#|L~l9yZWA&rjSu>;zraG^#_;4}=WU{)sjjSyK5Vv*>;0d*LB=q9 z8X9ob0}(cyWMW>@Qc3yOsT2P1bPUzha0#}u#{qDtZd8jBIO6r3t65q`1`Rbe(kn>c z-^;v*)F5kgnLL1ZW8}YZmy*~D~wQRYB+A$u=$DvLdZwc%+@`~5@K#?8GZY< z$%PA;9o7FT0iG1pH8lLu*H?Do2)s4i#^Z+(efCkiF|q85m{Yd)|Y47DYZPK!9f1Z zY_t{K-6iZgWFMyI!u;wFR|?Z3|1BV@sNm%2xC1vDL6-E>>voe*QPLKVZ8-{hdL7t* zD0uruWC*c`K@W1*8w+8SCb(s`Hd;fv{+B=}a=5`WHRS54eCHgIw*zRfE*ZvHf?3k-{jkY*dbGoj|#PZwE8Hs_*p(0teS44t>|xwz!j)j!nMhF0}_Exc4!US9t3 zWBOPqJKaF_on3~cP*mADD=RCJ+@2)m;Fb)7sxHTmA3tv1vW4oT`LMa2-EnG(tJ6OU zoO(EAJ^7?JDrYnuUhoBqLR+Yt)0t(yPbR9hTOh=d$0E6-u`|M509dP zf@Te_W5ej7WnV6WcxaxQB-c*v60(vjZnjUKy6}fQx zO${|wefuWM#}~lx!r^PtP}38)pWnPbKR=(Hon5*-KXQ-%Yz&KimCM4kJu7R}>({S0 zY~d{^DhkR8e);kx{^)pCPew+j;Z?oSbGOm^O&Yi4mgVA zyDm979o5h{eE2Z)Ce^`?{EK#W>91a04YzdlnLgP_Cp=}G(PT-Ud9I3-Ev&gx3@1CHbU{6m^NJz+`g9nFYI0BL# z6590LPF0=^j*5yR{r&yFkM>fw$weM`H+1l`4)R|N&L3k!7*B(#l; z;@+8^h?kzL@TYgV=a`_^^@M9R_MA!i#?71i_U?U}me$_hu5#o^{pc0x_@t!n8yi{I z?ve3cEO`3#X;9GnzpkxGPD)zaH+ZNcwdu*pCSg}8x3R_PQ3JB*c0hyWbWc@SYnI8% zNEYRkv&iMSCyyViEQZs#m9>2O^vFbPCNCT4`^)}`C^@e!*pUy5-3>&=_p zTwICY_OY?Wk~OhnQ4tZRPoGxv_>42k(G_UGe!jkY_byI*r|OmQ?`7}8_mB&t`Tq1f zTGHBImniQ9iB*;3_gI|GKG&J78n#n)NWA@10q3^UT3QcJ`TP3%=Js3{ z^|f(N+f0(=;lp`Y`!SD?*yDm%uY$)~M*I36J%0SsYr*v>{kM4OtJ8LPtu^a5-IpTW z#vVvqnxFY8=Q3E&Ds0T4xR0M-Mp}AtX=!iJDNRjHZSAX*{Tij-OGTGQShj83+dsj~ z%)EgcDIQYt+A4LU=+ev7ma*R~XHK7P7PM2#pxRP7bY7`)P)#Vw{1UCI4;`IEkeTG= z;mT({!=IithHh+sosz;c|8reeX6Oa|0@Cr}xQNs5&C*6;p`i{ox9mHx%?R|}!onm{ z=*6E;N=gb&Jga1WxbH)h>Dya#qxqeK7d2%QObiV@C%To_ulIj;HmeA)dWB|wZw|^rN^OAKfmft^>u98L{;n%Z zmF!jL&Yeqq{MdD6*-POaBJxcjt>k0TFP1EXyeC5A?CI0D0s|S3tDG}6-Lhqisf~?| zxw+1%Q#)rp9+#%wi-@?1;$ZRdzKZG~b$wf)cyiqlm$K=}$;p|S)~2SM+}vL0LLD8Q zmX?;^ESdUwHlyu%Dk`Lk?|ORg+3|;2Sy`o}ax$zDd3nDldaCxEcwl8_HiZh*)Kq^s z?VH0;p5)zNu72@#WsZKbUh_P>yeP=J4?ZXQ-kmbvT+BM3EU#`GwWKA))|pm?GEU4Vr#yMG^1GEZx%llhPgvu}FhZh7iY{*lS482y>BdDLNW?&HOt>yjwuT@x+IL|0z_h`ZG^-G*i z)wj8)lF1?70Tm)i(4^$SST5hFd}82WC$skB;}Tx;xw)p?ehe})#XdehNb#0^ov++} zqC;gF7XIosITycGH(?5~*sLObD@@amWdX&)80?K7+siQ<#)z!IQyW{d^=jVBM z?+)2^=xt4n{n(enyvNKei3w%(hgx#wkN}qlc5gp$q0-W|x}uYtYqc{IVKsn~!F&8Z z7XtUk=je#_0t z8EJmngcuR-Z-}^ePq}x8%h)L$olnva*SNS>YY~ZG=NEpKB9sRE`&pQoKNVk{`TSXz zit5HgCI5rb$MHXf$1m?byTjLwDoRzxqBD_|ujxS&yXLPdWbfs%x%|NUTu^T}_O>$I)W1USArhRq$PRREmX|708H5)?~_a>UrzC zo}~9)oNajfwjUkJYktbQh9DBP06MO% zRn?O#VXx1XU1#P$Bl7dWD#@6|#YN*{kHF3nol6neWlm}LajahE>hvZW8fQmG_xY*4 zMP+NM>0}dr_VpdMZ_iC2m-RF>MxT%g-p;PB4oWL0*Q{A%QtItB)VOokCr&k2!2>Hahp7#6v`}Jr*_Fw`>Ww zkekI$H~6xBKh!YtJ0L51rI}Ayn4XSqtm#P|66eEdw%0P#(*+a*#w$+k+La7gAa<4@>VNFqwd?oPRC;>4 z-$sBMr1rviml9s~)3dW3ZEZ$~V0}g!9v&V)fB&CtIjYLa%I(>N{G?U9n+$5jHc`9# z&z{*HJ4Pc@Y+xkKdD7T8-uP1RV=^h}7htBv27-t&I&-E48RvWby5C9%4d7{E0p;_h z=(6o_^HXHusRxc(MMdpZRRF2qK7G>BKS0Rm*v1d=Q4X4p&U^0Hqqw-QT^0T#HPRFy zsja>1bvsK~)yS8^OF3%^qK02iuJheH>)HnrJHPFC^5n_FJUNfH^{S|7dA27kW1F$y zdOCIli#3WalPTe2(JCzwgu3$T)k20rJ{`L_AogEfj-H;LoH8CfUv7@QQ7N5xSnRdX zQeB;@w;DfiT32`Q`}h6Mz14ySFPk$m($g2wEYVO8doQ|UJDZ!EZ{KFvTPuXtx!814 zjubS0t($Zpq<%grDQVNDO*|RbL(`)dKI-f0vZE#;rFnUI*~A=}G8;dA8vXhz-*HbH zYKVbB$DcFT*3!D;>sxV~TWPz8z@58yH*<29Sk%NIJ_U@5PAe%T-`kaLKGKrmDZApo z!~3?wRve_h*^g{&WiM z=ks%O<`x!>H}>t@S5Eez%}!xQPtiPoiKn&>^SU2=+q_<@)rICDL;oaKAXJXQc@Z=1Ox_@8SR(=dV}0jbWH zFJq%R$z1F3(9l{uXQ&)NA@=_L{n5wS*w_>ju6gWYlC!f~GrTOjIpmuyy0yk}>FI&pB7T}% zTc3PDi9-q_kq;r+dwLRNuC|bx=fQIxK0NQ@;$m!UeEj&W?Cb+^k2RUht*y6h-O4B4 z`cHH}+TC4lc5voL+vM`n!mV4Iq@<+QuU}vEzM`Vy!v}SS_`7!-^K4t+zkh%5;6WVs zkuL)`GtIviT~<+1!O2olQQ;`hXX|#KMpEC8jeS*Eh%UDBiF|gJNqMxhwDZfCMMNK} z22O{PufPz+jK`Qnb4vjKak6@g)74rj>pIJlx|VY<3&tfI7P$m6I4{lrW|1s;9$BsB z*Vo(2pjcoiq^wm`RP^xS!@TXy2B}Y;xb;>umQ@7=P@`$fCY(GRnJg|Lu^}xfIT^)r zh_vrw@54DcJ3CKIOe9ZT62Ft zg~1k>E8W;3>&ft%hUE6!wC!L@Zf=CP)2^6tMfL65x4(Jw=5mJ%8W9gqC4LV=zH{gG zx&d_$K?w86eURxwGqK|aBYLnxAiA& zf(nE3o;*6Z~2TMQ_W<*b{xZ*SN1 zyKs}%{tp^6#fHALYkL7e2qEF9R3z`QqemZRU4L_dQL(YHanRYz%S%u&L!ab7++qOz zBNc7Q&>gx0Z+KhrRX4=|oWpn%W7&kVwx%XX8HvI z_oBr0_4U1b_YU!w#d3NMpM?dJBA2~VQVWRh{EMBL`I^SY-k;KRY+Ex!0t5H%+ZPlT zHuL-U@0pn=CU;PPcgT55;|)?%2g`441O|U5uH_{DprAlpD9YgUX&%1b$+<92nfcj? zT{53llWg^Jtr|e#96C#kPM>Dt;Lwm;w8~9mA%O-#7T2v^>v!V@UaZ)Cq8rEsB`|51 zors+k8_(#t;?EnhISKS$ipmuLV8G`!-z=w5<*>!%;)LoX=NXa4)&CEI178r}Dx0X? zeo@ggr%qkSX`>LE)Z0m^-@l*J*48#B&CP*35FQ)fK4)TKL1)xfRrN1B=ehK|HA={6 z^Vp$R-b)?|TLS|Fudi4kjiGpLT31nBJvcWxfQo6ce`}oNWq^l`G&DBldE@_02~nuh zWz@;rnS$b0_wnO#U@0Xf)8UD6A7|k4OO3|r|0J8{%Cyk0dOOBHN*5u*#=p2EEnX%#2;!QNI0}%Mnn9eV4_>#nq&Qtm^bH3waVgyy_YnUrR`@g?O2( z^%e5_C>*(GB87v>MPTy)U(M`i7NT7S9os_U1X=|^LPkbLe!g1{I|E^sP$@yPbZ`Ta zco^;b{)avQYdvGmWX4sJG~hRc9xEp&Cku<~^bb7{RqLwNM8(Z${54 zeJ-c1J3q>u^8PG3Mnv0XihBfbM>B zc@dA((9p0}T}!-J{nLYxw{iGV(ACvdHZYlGQYvfH%tv|i7gTjxggnjr4><{pG1d{ zm-oRrXJu=x86up^^ZDe#*aFmZ&rjJi1v(C;ChfjI{GBJn9vt~Z+|=Lk2qP=2DjB8ClxJE+N9UiM zp8nAyrvj~$Y3y5Ztk92d#S35+RJO6YdU~Tk)Q*|Ktis^@^&Od+nf`Pfzow@xvUSlf zkdoWBZS!LQ7>Ag_DdCJH3OC+06ehR4Ros!wx2(RQ!O7X#V`aHGr4{@%&vVY%$jE4& zZ`a@;IXm`Ms!V8SnxT;qTDh2gdrEThA@}ifMUD&8qrjfWd7Iw8ucLqE zKwMTI{_2KTLRDd4FD)w4H!)cOEuy6&LXrtWAHmX|sNlPGlM2);4ULf83TqlgHFtOS zNzU-G(C~Zrbk3Z4>C~fw22doupWF{!Q5cv3kVIB?xh=<%mU$f&)wb>1Ra8|6`ug5f zRzeYJvZhhiij0iBcI_Ip_yWgkZ!R=+2nh)t=e-1Z9?C&ob@g+=GDpY6XVTKrP@>m% z?cKZA=#?t~xBxU-0fBTZw9!^C-y!bK9W6ygMFoX2NGQ6bK%}@1o`lBi;NSo_Oa2~a zR@Oq_784UIb^B?IXv{XP0-JaM>Gz^v&1ToH#BgNutBY!CYN+pP<$4<$8bI$Lc84tQ zcVYhnR<6959le7vP)BcSd*zk|1-Aa7(oohX(X`#pOy|+Y{TPIz)U_fq$xH`yzl?Ri zS%!y(IB<012X^jM)X}+DH=rHT#|PcvdKz*7=+VPKT=fc$@WBH^PtU~x!{En{Ep&D7 zC%#8tyS+XjHEGN0c||M8_$bwkp6n9Ny*FvuhC`m~m1nZaf4%E5Be-whh5cKdoSa+= z5y9wyyF*gE$Z!$WvR9mh6(be}Wy&b~!W+cu#V-X7&&~>4tMV8azjp5$9i@OZa2{X= z21R#iW9*fK!x-M_hJonmSxBp8&lnT|QI(FVs}He?C4sA+e4g>}p_ZYcVZH_$33eR6 z{V_VaYv;~c&yti@pnq*$T|SciVPT;}F?Ujs#l?$RiHY(Tap+EkhN`NnZ%6>3*Io-R zzfsG{{U}=1Y;fvl3Hm*D_;F|Dg!^kGVQsvO!4aaKua}RgU!tVjs5{V>T*RxgZ63DwPup3q?RDKh@(doPUn6rhsIEJgLZCe zD12O%H+*lcaeRC{FmyPthCX8H-n~z#+q;Wcf|6{MQ`6JGSTUnGX*>sURiQ>_>)N=1 zme!uHyl&HUE^aB z{@{V#`SVUt-qDH>D3~}LliIoMUlS_3D_5>KJO4sMqSEp9_Fi6I#zWrM)|QYf%V_;0 zf)PPjclY$50xXeBOG7h^&}6Z(C)L&Y_w6fk>?((*x$nejb~>D6 z=-65A|4l6QzkpJU51zB__UJ6UG!PH*7G>DIp=pzTn&~EzaN$AO2Xjlle!Uz_3T#qv z)G=n8^)=ZXQ~}GJHgFo09E-9Pwm3P`xc)~L@Yn%bvMHP+W}J#2IO4`UKl|9p8(szGCYNpq=E zl7N7~kq1Q;cQJio|NYb5<8~7C8-sj@qf#_n6*p;3bK1zp(4^6KA3wUdxuM#9tBsRF zxrm%Z~ZfR&cJ=Gx1$=4O|MBxSt7Jy))SmhU6>`rQVlI?!PnuNUn&(jtgzX|1|y zZKslkWB*aCg3=601s(O`B_POpi!H^qAKW8hoXXkE# zG%J6QNf~H1gTx>J^G(!nG>dz#M~H)6!2gYO4~xq_n-aI>QRO~on<7%rW*K_{Q1PJ9 zI8)F<I5(+ zh!OUkCB@9Ohpih^0D9E*NvZ_lUAs&HzMu=pI)0Zwa>NhOmb+`U58WyW8qdgBXx9af z3YGe7zqcQ@VG4g|b}*X%Y^L4AzicL^rc&y}p`W%$n7(p?=#7=1)zNvBlH%^+aZHMx z7?i)AT%C&tQz|3CH&mmKAMM@*F{4?zAnHH$*O0L_H8qe7HI5$#pnMP&wWnxtAc1`R zL4v*Zh4}|W@Q~8)p(g)#X3NV9Kc}jot6zI(M$;dW^Ww#YY~7W`$vW&0sHeyDkGTqZ zxqAM1&?xJcbUnY@%(rV( zq@IJC93`OFc){%sgW~kh&V+=7(hF~bSY*_tpae@_>i>ALbjcN?0L0Rl2&wTajRcrrUULPU_*(6<3bCU_Hj@4_{ zV2~)_eYKbGU`H>NU=sbpnk@qCc5;DCU0*}B%&)>+2I?a$9wKtY2daa=Ufrq$%wojxnm9Ljy8i4YJF2v>>*1!Za?Eg zGJ=93Nql(u`H)+HfQX8KUpUQ%KD4Xdep?|>P|yoB`8^gq_^U@du5Mx{#cwn0bU%4I z7-K)6RtOtF#uO2_yE0HEp=s^<^(Rqr5YC1|Vt70QG%yTEy&-sl$K8Tj?(Q}D{rh*u zT`I@~IfUju0f9f`rZt?BE-(^kgREd{fYsx%(USvpZ(bj-{`iqy#$yV)yXz>~{u?{9 zQ(+acS21+wE5KkQLqnh%TTnrqEPN0f(lA;YqAgqz+TWpB2uZeB%Qkvsc-W@Q6&R-Y z4s=|xt!0{Y4Ry*ush$wTP;}^_vm#DPN=j^7(is~{jOXX)C;Mv-iHMl(--=Pp614jh zC+Lh0lQzw%5|{d|Y;7-zKS)eWynp|G(Oeal_VQw9FI4RlagrReo>0+;$~V6J{i7|M;S_{;cL$a& zTXrSA1|C57N5yfe3;Xfo2iejke;TtnGb#-;M zwVYxO59UjuK|!P2ZM}&@1K@-*8j_aouZ@d+Kt5Q+unnDsAdp_SI8~6Uphe=KnaJIZ zX}50ODw*qNQ>frN#XHFyV4oCp+4YskX@Sg_OJ={pWtd?eHLa=aHt#S{auz9VOG_;wdB z)|nklPEN*N2uQv`k9>Mhy1`ZzmYE#gYV7^)pohtYV4oP;AZsyoVt`J2Ld=HnIpf2w z>?L4Wp8#d@+1=yi)C0^>(6XW9yMkOTlcFe*Gktfm7 z(IDyZhz9iCAt7E0CW?)XefiS$%$Z2QqS<6SYVJdpwJw(~HPzR<&y2-AeM(7t0YSm0 zrY2#`F|@T0fwJQvq8fcgXdEpJ^Vi5Jnq z&j|^gSX3XZrcihxBamEiyU))73E_aDoQ=UUSYF<^tC+V*$5)~`T!r77ia`KUDcN_6e)n6 z&JwdgKXc`PXYaUD6E$ln->`c1>Z#wq(MUQZpBTJ$&n_wPhJ|C~!3j~yDEIg9 zr(~6oVJr~a%_5-dvsO6>LYahpyNGCP&AI&mr)%TLRLbNHo%-O`O&lB?7&n1~i=9@Z zZY%M66&Sc7{9SH#Hr{NvA21DNtP2T#DvkA2k+@I)HS8=^M(2~?;Gxh%Z2H3AM*teR zU%3LgkFr&|+cjCBooOYjK&!!C*ka#db%PlX9|{rZ`#?td)3QxtWQaZoXI!&^8uc8~ z_rwEnfQUC;j_8*I-Q9O%Vmt=xAHIKo3}J-%owdo%zXr-LG5NR(NlC$;*9M+0Dk_So zR?{@|LvMpnh;6~Z2O2v5&DB*@S<6$_1R$X7oXbc{O+|&0v2n>aMMbjuu|_O9JTf#T zGgH>KMZnh9w(NZ(F}lVXk4KR2+rR%gI)RwOmw=!k&@y;g!d*4TSD+M^cXYf$Jw-1` zY<>KF!W}t}GXulV&d(1E2uNhznw7s_SUBuMpQNPZzGZ%H?rZT*a3uhZ*2`kDpr@}- zW}VDGJu*(BJl~|>k(rjZosDe)&Vnv$Drp%R;5}ZAm|WLUeP98PzVV_P zQ)^cN!+3II)N<+-j}Hsv$Hd|**kVWvC?^?NSu2qEpb=P41#=zHy_&~5iei{)p`$J2o6bJLDPX0p$pNuEOjfa^~yTuS-iy9PI4Ff;@idup^-1f@_Up00iLx zAv##ryOCAs2dXPXD>Rcwo^b@3u--ImRr^bjOe_(#J1R0#NI!QF(^?Rw+pe1R1t2&= zh6S4A2M!$y|K<*dK}IAnTDpPSBsl5B#8aKrr%$iI2H79XByR)2?+0qBNFDwz!j+4| z=)wicIN=e;jRGh$VeAqw(Amq&6?g1N0O)Jncz|3@YP1m z0((yHyKp=#BP|Vs(vaX_HVNmH+@95h6)nh{Sp#A<42KLn`s0Tkx-7U9CO{mA+SbVX zU&m9V+s42-VB2)2o%DYx%DuxTx@?6f!U=6@bpT(0R9{UTTdqwWf;f#mH( z4cfJcT>C#h71hwynjOI0m=I8NgXbYI{cdB%9uu=YC<%1#!~6GOT$f;uMAA%7lJLh- zv0?nhq|B(8o0liSPUl|%u~^MucFJ!+B#1$l^{mh@IEWzUh^*KVLGQzQ*%@5 z!Leh043N$wNC^ob1)x9P@W2hms|fv7&P>}V1|15Dl<_!SDVn?X=nm!8g#5-b&DGm< zpjiP-dZ4i;awvuFOcsJXxj5_8&_UD-!K95zJX{Z^0KtEl%M^A{L&F}i*hrV50BASJ zOjkpPIRas(2K5bYU{Xyj0Kze)=jnX9we1k<%pr7E1d~_o9gLl=FI=F&InlGJgdi=Z z41`%V@J(-DAGSCXum9}XLhrjRn0}DUo_LF+J5Ix~Us%ui`E%NSJb%9bZzF+< zE(S%&{A*UPrsnotM@zBNa*?;tR4_o2yp}ri{WnfVzr?dJGgHX7%+bZg6BsX%;`v0= zgjR&PUwQdXNOBCP#5e;_tmj~&I>;q0&A!5NN|ky8J$;lb6IZZ)x25UI@)GoPWD4Xw zN!K5`ICvn$y+M-Ri{kzjj3Nu5NR;NigjlxGEoW55zHOQN2FRPymHrsqu*9%=N_E;_)s zRUMF3K;Zaz14VofrvC8m9k-uj;g_NbH5W;%9XvH!^*g8tl#G9A*qG>qpBpYa1;95V{2L(8;jssKGIoF zJNgLT+DDJ>J@`YlsH6D#_@F4XUgd+vznk1m>E?0LSDcXycW;{gOQ&vsa&kveFR15~ z3Tg$4ZKR;VVES$h+`qZUxUNr3rgR__9quD7%h%m5U4mL-6|9nX;SG&wtl%!zvHpIQ z6(u59RWg*I@=5XW4VJ;JxIAaMMG>`;GRhZc55+Bk3l}a}S@9V;{QagzfyZx~GNHU~ z>v9esx+jk$-}>;F<Bk@@majr@EX)};(9=_KAsY}Lyt+0) zPI&MT(4jXh5$Wl6aP#cgfg2DHF#Ylg~+HjbvSMlkIo(75#~T-NJjk9u5*H z#4avO-Yc|30;S16qJurix{8W}%9+Wk10x??cF4%E$9BPkk7f+jTgHqaNLg(NZ0?s0 zi#>GOofK5Bu~W5+LS{qo!E{DZ9&0S{pn=}rFR$EA7Qj5&h$=nqiR`k6ztlc{oQL+m zOpn{1TfneNf2r@5;VE%Vcwymql04c z<%PNW*LEZ-SzTQX@?vgjDXjaP3u;TtgILU?d~YYq$AXRZ!b^xFfWqX#2Ehg1$}GK{ zx8J^TZWIG>W5#>g!!3?}J=~u6EFob>u_HhC5o{l1vw90K7F6EwQ$F=4pf^PrC$JMS zKM=*gGkw1@VBIg_%_sNl-p#1^HxC|7`$Om_CKy;zP{kG0ux>FGY;`PXZF*c<%DrcfJKCT=lYu(4@(esa4SGGht{ zzHpHQa9tf+6Ir%bzS2x;D1Pdto}`e#itni9mLOMyR{Na2;SG z?Y4tFXmao=OOfEwY#JG#kEIHhh=cMEFz&1qai!i;qLhgQr+l zNGP-6=^+f_|6w84-y`iajqwKILz>|p!9lyv`|*T`2qq+eN{8jY=N@zU8Y{8!%=MVq z{HM!T4%aY}oV>kD!o!(V-2Jbzwa zR(8I(`i{X%JL@HJa#gz|Wh*tz{=r8e_rQZ`_oX2FjQxLkYp?tIKHAJFZ{QE_4UwRX zF-l!s)|vA_km%Il`Adt75N?MQAv>&D<9mjlAu)rJ5%r6}d*OHQQgZ`&URT7A`T4CK zqN1vQ3pPYpdAqv0GLg!d#reFXT|%T1@+=fH5JBis=EMIH#{c$@*;ND)0z+biE45}A zkSc6rak5_e{|p+8t3!k~3mvE1HM@#9f(85uxk}(4jNU8lZvFGo|BNc~|0H$)9{>Np z{3QARS}*>`FHKDz;KQcE0C@QW1baYQeQuvKQjFNcj>akWJ`9LDJtsAAHn0MlSy_Rk zxc9xkXK^ci^nG3344gALZHV=^Z^^e1VXJFu?zw8Na4#^Ts_=>0=R?{OQLfyfMOoR{IE%A0u227O zoWMvaYJ8|^5BvcD$;en-I`y0#zs7LTef&FaFj2Ro9ANy5DfymyUF@t6Tlx==68onm5j;p}!Vx6AJnVGd0 zvMuW3DNc+0^cmy80K6b5L^Fgmd~|d!gR#TG(rtJcAuyTD1Cp>Ax8;(`fO8?Wz@(0D z>Vg?51U(r4wVC?<;b$iQZfN#baXTO;20Tg8)i7;anL|CN(b%Q9vhX-U~ZEYp7;Ly-BIy&H8 zB=a}QwODY<<%y~t&=>X?a&FxU+cN96Z2*M;Z7V<84$U9gv~HKU_$+EM=H-l&@wmQ= z>j5AQGmtE>RnY9Pwbiw?l&ft2xF|NXOT;B>d(iFW^)wqcJfsJ(9qsMq>DO6@S~HE=5iG7fp&%FyV)0a(x{IqdY&qMp+Bqs3|v2K}kt{BctL5 zqmMN;bDrMCW%Xp=r9#8B6QPaut1!#~FL(ft((gF zuDZK_jF)*eHU>2&|K-awnws}TtQ#rn@{l5Y>$n`)uqOz6qOEN^6^|RmH191x-al_{ zzI(O+`rXBnxyKG_*D-?wx3O-V};8_#`R?qK=OJv8k7w-W|Wh8$j!N+A|vv2WE$+M)N zI$xr*_fdU_H&cjAFJMe3FFH_X6;K`jOwaoW|jPGk2R{aiLA)MZ`X!c%O6 zf@IMUA)A9K{aokDGA^V&^RR8Rdl?zo0!{OcT;l92EEIbJOZbg{dcfD}K-VP!-jnF9 z{zd!t@m&ECQ5x7RI}5swX;96_kJQ||RBr7T5qa%0bP9DAPTavRIc!IJdpjl@7;Hil z!ZqdWkzEzcB3R@(_Y0K~Qf=w{ zFh8lGQ3DeRB*4ZzEC=I*P1Mx&jg2pIa(X7*A@pTGf1X(SePs8t5V)A%ZFtT?*~G>$ zF!*B}hvOTW;#g?7&KELxL;WgN++1<^DoN*WYGDz1|307lL9NyhHZi3BN4rj2b0~cUlZ``2r zErS&kV|wi4=N!wh#IpbURy+CUTKn#;w_i|f-m#F5yNJp*SXCeoVzB$y@*;#i} z(;i&~=%n5a!&(Hf}HVVBnwno;e`$*20Ex5>yYFh?(v{>l2 zLn@WUn;9R1h?SO-TQVtc+V)OsW%Tl@3kMG$HUcjP{(i)vb_t>}2ZlxSzem_?>V+s< zVz$MDSwz{!W*CMVToUFUm5)gXc#ubOn4n`+C-P>tHl1f?Wo2bzf=R>?z7bp^X{38{ z&*e`F4neId}S&TLcvTvKZqj>)uDgnZGzSO*0m{~xj;f&UTS46dv?uPa>J UFCrm~ClW`MPaMfqJa_&70;+7SD*ylh diff --git a/internal/storage/bucket/migrations/15-create-ledger-indexes/up.sql b/internal/storage/bucket/migrations/15-create-ledger-indexes/up.sql index 5fa90936e..14d95d96a 100644 --- a/internal/storage/bucket/migrations/15-create-ledger-indexes/up.sql +++ b/internal/storage/bucket/migrations/15-create-ledger-indexes/up.sql @@ -4,36 +4,4 @@ drop trigger enforce_reference_uniqueness on transactions; drop function enforce_reference_uniqueness(); drop index transactions_reference; -alter index transactions_reference2 rename to transactions_reference; - -DO -$do$ - declare - ledger record; - vsql text; - BEGIN - for ledger in select * from _system.ledgers where bucket = current_schema loop - -- enable post commit effective volumes synchronously - vsql = 'create index "pcev_' || ledger.id || '" on moves (accounts_address, asset, effective_date desc) where ledger = ''' || ledger.name || ''''; - execute vsql; - - vsql = 'create index "transactions_sources_' || ledger.id || '" on transactions using gin (sources jsonb_path_ops) where ledger = ''' || ledger.name || ''''; - execute vsql; - - vsql = 'create index "transactions_destinations_' || ledger.id || '" on transactions using gin (destinations jsonb_path_ops) where ledger = ''' || ledger.name || ''''; - execute vsql; - - vsql = 'create index "accounts_address_array_' || ledger.id || '" on accounts using gin (address_array jsonb_ops) where ledger = ''' || ledger.name || ''''; - execute vsql; - - vsql = 'create index "accounts_address_array_length_' || ledger.id || '" on accounts (jsonb_array_length(address_array)) where ledger = ''' || ledger.name || ''''; - execute vsql; - - vsql = 'create index "transactions_sources_arrays_' || ledger.id || '" on transactions using gin (sources_arrays jsonb_path_ops) where ledger = ''' || ledger.name || ''''; - execute vsql; - - vsql = 'create index "transactions_destinations_arrays_' || ledger.id || '" on transactions using gin (destinations_arrays jsonb_path_ops) where ledger = ''' || ledger.name || ''''; - execute vsql; - end loop; - END -$do$; +alter index transactions_reference2 rename to transactions_reference; \ No newline at end of file diff --git a/internal/storage/bucket/migrations/23-delete-orphan-indices/notes.yaml b/internal/storage/bucket/migrations/23-delete-orphan-indices/notes.yaml deleted file mode 100644 index 236c85141..000000000 --- a/internal/storage/bucket/migrations/23-delete-orphan-indices/notes.yaml +++ /dev/null @@ -1 +0,0 @@ -name: Delete duplicated indexes diff --git a/internal/storage/bucket/migrations/23-delete-orphan-indices/up.sql b/internal/storage/bucket/migrations/23-delete-orphan-indices/up.sql deleted file mode 100644 index d638dd429..000000000 --- a/internal/storage/bucket/migrations/23-delete-orphan-indices/up.sql +++ /dev/null @@ -1,2 +0,0 @@ -drop index "{{ .Schema }}".accounts_address_array; -drop index "{{ .Schema }}".accounts_address_array_length; diff --git a/internal/storage/bucket/migrations/23-delete-orphan-indices/up_tests_before.sql b/internal/storage/bucket/migrations/23-delete-orphan-indices/up_tests_before.sql deleted file mode 100644 index e69de29bb..000000000 diff --git a/internal/storage/bucket/migrations/23-noop-keep-for-compatibility/notes.yaml b/internal/storage/bucket/migrations/23-noop-keep-for-compatibility/notes.yaml new file mode 100644 index 000000000..ab4b7e1a2 --- /dev/null +++ b/internal/storage/bucket/migrations/23-noop-keep-for-compatibility/notes.yaml @@ -0,0 +1 @@ +name: Noop, keep for compatibility diff --git a/internal/storage/bucket/migrations/23-delete-orphan-indices/up_tests_after.sql b/internal/storage/bucket/migrations/23-noop-keep-for-compatibility/up.sql similarity index 100% rename from internal/storage/bucket/migrations/23-delete-orphan-indices/up_tests_after.sql rename to internal/storage/bucket/migrations/23-noop-keep-for-compatibility/up.sql diff --git a/internal/storage/ledger/main_test.go b/internal/storage/ledger/main_test.go index 9ea1cc84a..668451e64 100644 --- a/internal/storage/ledger/main_test.go +++ b/internal/storage/ledger/main_test.go @@ -74,12 +74,15 @@ type T interface { func newLedgerStore(t T) *ledgerstore.Store { t.Helper() + <-defaultBunDB.Done() <-defaultDriver.Done() ledgerName := uuid.NewString()[:8] ctx := logging.TestingContext() l := ledger.MustNewWithDefault(ledgerName) + err := bucket.GetMigrator(defaultBunDB.GetValue(), "_default").Up(ctx) + require.NoError(t, err) store, err := defaultDriver.GetValue().CreateLedger(ctx, &l) require.NoError(t, err) diff --git a/pkg/client/.speakeasy/gen.lock b/pkg/client/.speakeasy/gen.lock index 0bd31125e..69d84c282 100644 --- a/pkg/client/.speakeasy/gen.lock +++ b/pkg/client/.speakeasy/gen.lock @@ -1,12 +1,12 @@ lockVersion: 2.0.0 id: a9ac79e1-e429-4ee3-96c4-ec973f19bec3 management: - docChecksum: 2624238aba49e6a33f19ef1d62f0b568 + docChecksum: 1596e540a51d2b443378a8de0698b2f4 docVersion: v1 speakeasyVersion: 1.351.0 generationVersion: 2.384.1 - releaseVersion: 0.4.34 - configChecksum: 44b98e4f6380b040c4360085974a2b3f + releaseVersion: 0.5.0 + configChecksum: 598ba44ea6e4a0fa32d0a3e02d870ace features: go: additionalDependencies: 0.1.0 diff --git a/pkg/client/docs/models/components/v2errorsenum.md b/pkg/client/docs/models/components/v2errorsenum.md index f1757ffab..e6959db96 100644 --- a/pkg/client/docs/models/components/v2errorsenum.md +++ b/pkg/client/docs/models/components/v2errorsenum.md @@ -21,4 +21,5 @@ | `V2ErrorsEnumBulkSizeExceeded` | BULK_SIZE_EXCEEDED | | `V2ErrorsEnumInterpreterParse` | INTERPRETER_PARSE | | `V2ErrorsEnumInterpreterRuntime` | INTERPRETER_RUNTIME | -| `V2ErrorsEnumLedgerAlreadyExists` | LEDGER_ALREADY_EXISTS | \ No newline at end of file +| `V2ErrorsEnumLedgerAlreadyExists` | LEDGER_ALREADY_EXISTS | +| `V2ErrorsEnumBucketOutdated` | BUCKET_OUTDATED | \ No newline at end of file diff --git a/pkg/client/docs/models/operations/v2countaccountsrequest.md b/pkg/client/docs/models/operations/v2countaccountsrequest.md index dc139fe4e..fdc614bbe 100644 --- a/pkg/client/docs/models/operations/v2countaccountsrequest.md +++ b/pkg/client/docs/models/operations/v2countaccountsrequest.md @@ -3,9 +3,8 @@ ## Fields -| Field | Type | Required | Description | Example | -| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | -| `Ledger` | *string* | :heavy_check_mark: | Name of the ledger. | ledger001 | -| `Pit` | [*time.Time](https://pkg.go.dev/time#Time) | :heavy_minus_sign: | N/A | | -| `Query` | **string* | :heavy_minus_sign: | Query string to filter accounts. The query string must be a valid JSON object. | {
"$match": {
"address": "users:001"
}
} | -| `RequestBody` | map[string]*any* | :heavy_minus_sign: | N/A | | \ No newline at end of file +| Field | Type | Required | Description | Example | +| ------------------------------------------ | ------------------------------------------ | ------------------------------------------ | ------------------------------------------ | ------------------------------------------ | +| `Ledger` | *string* | :heavy_check_mark: | Name of the ledger. | ledger001 | +| `Pit` | [*time.Time](https://pkg.go.dev/time#Time) | :heavy_minus_sign: | N/A | | +| `RequestBody` | map[string]*any* | :heavy_minus_sign: | N/A | | \ No newline at end of file diff --git a/pkg/client/docs/models/operations/v2counttransactionsrequest.md b/pkg/client/docs/models/operations/v2counttransactionsrequest.md index 17625a992..9e4c5cd88 100644 --- a/pkg/client/docs/models/operations/v2counttransactionsrequest.md +++ b/pkg/client/docs/models/operations/v2counttransactionsrequest.md @@ -3,9 +3,8 @@ ## Fields -| Field | Type | Required | Description | Example | -| ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -| `Ledger` | *string* | :heavy_check_mark: | Name of the ledger. | ledger001 | -| `Pit` | [*time.Time](https://pkg.go.dev/time#Time) | :heavy_minus_sign: | N/A | | -| `Query` | **string* | :heavy_minus_sign: | Query string to filter transactions. The query string must be a valid JSON object. | {
"$match": {
"account": "users:001"
}
} | -| `RequestBody` | map[string]*any* | :heavy_minus_sign: | N/A | | \ No newline at end of file +| Field | Type | Required | Description | Example | +| ------------------------------------------ | ------------------------------------------ | ------------------------------------------ | ------------------------------------------ | ------------------------------------------ | +| `Ledger` | *string* | :heavy_check_mark: | Name of the ledger. | ledger001 | +| `Pit` | [*time.Time](https://pkg.go.dev/time#Time) | :heavy_minus_sign: | N/A | | +| `RequestBody` | map[string]*any* | :heavy_minus_sign: | N/A | | \ No newline at end of file diff --git a/pkg/client/docs/models/operations/v2listaccountsrequest.md b/pkg/client/docs/models/operations/v2listaccountsrequest.md index b2c76f3ab..a55a6bf00 100644 --- a/pkg/client/docs/models/operations/v2listaccountsrequest.md +++ b/pkg/client/docs/models/operations/v2listaccountsrequest.md @@ -10,5 +10,4 @@ | `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 | | -| `Query` | **string* | :heavy_minus_sign: | Query string to filter accounts. The query string must be a valid JSON object. | {
"$match": {
"address": "users:001"
}
} | | `RequestBody` | map[string]*any* | :heavy_minus_sign: | N/A | | \ 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 1ad961552..eb6f8c5ea 100644 --- a/pkg/client/docs/models/operations/v2listtransactionsrequest.md +++ b/pkg/client/docs/models/operations/v2listtransactionsrequest.md @@ -6,7 +6,6 @@ | Field | Type | Required | Description | Example | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Ledger` | *string* | :heavy_check_mark: | Name of the ledger. | ledger001 | -| `Query` | **string* | :heavy_minus_sign: | Query string to filter transactions. The query string must be a valid JSON object. | {
"$match": {
"account": "users:001"
}
} | | `PageSize` | **int64* | :heavy_minus_sign: | The maximum number of results to return per page.
| 100 | | `Cursor` | **string* | :heavy_minus_sign: | Parameter used in pagination requests. Maximum page size is set to 15.
Set to the value of next for the next page of results.
Set to the value of previous for the previous page of results.
No other parameters can be set when this parameter is set.
| aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ== | | `Expand` | **string* | :heavy_minus_sign: | N/A | | diff --git a/pkg/client/docs/sdks/v2/README.md b/pkg/client/docs/sdks/v2/README.md index 3788cf758..5791b00c1 100644 --- a/pkg/client/docs/sdks/v2/README.md +++ b/pkg/client/docs/sdks/v2/README.md @@ -488,7 +488,6 @@ func main() { ) request := operations.V2CountAccountsRequest{ Ledger: "ledger001", - Query: client.String("{\"$match\":{\"address\":\"users:001\"}}"), } ctx := context.Background() res, err := s.Ledger.V2.CountAccounts(ctx, request) @@ -546,7 +545,6 @@ func main() { Ledger: "ledger001", PageSize: client.Int64(100), Cursor: client.String("aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ=="), - Query: client.String("{\"$match\":{\"address\":\"users:001\"}}"), } ctx := context.Background() res, err := s.Ledger.V2.ListAccounts(ctx, request) @@ -831,7 +829,6 @@ func main() { ) request := operations.V2CountTransactionsRequest{ Ledger: "ledger001", - Query: client.String("{\"$match\":{\"account\":\"users:001\"}}"), } ctx := context.Background() res, err := s.Ledger.V2.CountTransactions(ctx, request) @@ -887,7 +884,6 @@ func main() { ) request := operations.V2ListTransactionsRequest{ Ledger: "ledger001", - Query: client.String("{\"$match\":{\"account\":\"users:001\"}}"), PageSize: client.Int64(100), Cursor: client.String("aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ=="), } diff --git a/pkg/client/models/components/v2errorsenum.go b/pkg/client/models/components/v2errorsenum.go index 8bbc25e7a..be953b76e 100644 --- a/pkg/client/models/components/v2errorsenum.go +++ b/pkg/client/models/components/v2errorsenum.go @@ -27,6 +27,7 @@ const ( V2ErrorsEnumInterpreterParse V2ErrorsEnum = "INTERPRETER_PARSE" V2ErrorsEnumInterpreterRuntime V2ErrorsEnum = "INTERPRETER_RUNTIME" V2ErrorsEnumLedgerAlreadyExists V2ErrorsEnum = "LEDGER_ALREADY_EXISTS" + V2ErrorsEnumBucketOutdated V2ErrorsEnum = "BUCKET_OUTDATED" ) func (e V2ErrorsEnum) ToPointer() *V2ErrorsEnum { @@ -71,6 +72,8 @@ func (e *V2ErrorsEnum) UnmarshalJSON(data []byte) error { case "INTERPRETER_RUNTIME": fallthrough case "LEDGER_ALREADY_EXISTS": + fallthrough + case "BUCKET_OUTDATED": *e = V2ErrorsEnum(v) return nil default: diff --git a/pkg/client/models/operations/v2countaccounts.go b/pkg/client/models/operations/v2countaccounts.go index a350b8c91..d5aa0588d 100644 --- a/pkg/client/models/operations/v2countaccounts.go +++ b/pkg/client/models/operations/v2countaccounts.go @@ -10,10 +10,8 @@ import ( type V2CountAccountsRequest struct { // Name of the ledger. - Ledger string `pathParam:"style=simple,explode=false,name=ledger"` - Pit *time.Time `queryParam:"style=form,explode=true,name=pit"` - // Query string to filter accounts. The query string must be a valid JSON object. - Query *string `queryParam:"style=form,explode=true,name=query"` + Ledger string `pathParam:"style=simple,explode=false,name=ledger"` + Pit *time.Time `queryParam:"style=form,explode=true,name=pit"` RequestBody map[string]any `request:"mediaType=application/json"` } @@ -42,13 +40,6 @@ func (o *V2CountAccountsRequest) GetPit() *time.Time { return o.Pit } -func (o *V2CountAccountsRequest) GetQuery() *string { - if o == nil { - return nil - } - return o.Query -} - func (o *V2CountAccountsRequest) GetRequestBody() map[string]any { if o == nil { return nil diff --git a/pkg/client/models/operations/v2counttransactions.go b/pkg/client/models/operations/v2counttransactions.go index c37ed0a65..f6dfc324f 100644 --- a/pkg/client/models/operations/v2counttransactions.go +++ b/pkg/client/models/operations/v2counttransactions.go @@ -10,10 +10,8 @@ import ( type V2CountTransactionsRequest struct { // Name of the ledger. - Ledger string `pathParam:"style=simple,explode=false,name=ledger"` - Pit *time.Time `queryParam:"style=form,explode=true,name=pit"` - // Query string to filter transactions. The query string must be a valid JSON object. - Query *string `queryParam:"style=form,explode=true,name=query"` + Ledger string `pathParam:"style=simple,explode=false,name=ledger"` + Pit *time.Time `queryParam:"style=form,explode=true,name=pit"` RequestBody map[string]any `request:"mediaType=application/json"` } @@ -42,13 +40,6 @@ func (o *V2CountTransactionsRequest) GetPit() *time.Time { return o.Pit } -func (o *V2CountTransactionsRequest) GetQuery() *string { - if o == nil { - return nil - } - return o.Query -} - func (o *V2CountTransactionsRequest) GetRequestBody() map[string]any { if o == nil { return nil diff --git a/pkg/client/models/operations/v2listaccounts.go b/pkg/client/models/operations/v2listaccounts.go index 85dd6ec8a..899d40a7b 100644 --- a/pkg/client/models/operations/v2listaccounts.go +++ b/pkg/client/models/operations/v2listaccounts.go @@ -19,11 +19,9 @@ type V2ListAccountsRequest struct { // Set to the value of previous for the previous page of results. // No other parameters can be set when this parameter is set. // - Cursor *string `queryParam:"style=form,explode=true,name=cursor"` - Expand *string `queryParam:"style=form,explode=true,name=expand"` - Pit *time.Time `queryParam:"style=form,explode=true,name=pit"` - // Query string to filter accounts. The query string must be a valid JSON object. - Query *string `queryParam:"style=form,explode=true,name=query"` + Cursor *string `queryParam:"style=form,explode=true,name=cursor"` + Expand *string `queryParam:"style=form,explode=true,name=expand"` + Pit *time.Time `queryParam:"style=form,explode=true,name=pit"` RequestBody map[string]any `request:"mediaType=application/json"` } @@ -73,13 +71,6 @@ func (o *V2ListAccountsRequest) GetPit() *time.Time { return o.Pit } -func (o *V2ListAccountsRequest) GetQuery() *string { - if o == nil { - return nil - } - return o.Query -} - func (o *V2ListAccountsRequest) GetRequestBody() map[string]any { if o == nil { return nil diff --git a/pkg/client/models/operations/v2listtransactions.go b/pkg/client/models/operations/v2listtransactions.go index 4030fdab9..88a233ac3 100644 --- a/pkg/client/models/operations/v2listtransactions.go +++ b/pkg/client/models/operations/v2listtransactions.go @@ -36,8 +36,6 @@ func (e *Order) UnmarshalJSON(data []byte) error { type V2ListTransactionsRequest struct { // Name of the ledger. Ledger string `pathParam:"style=simple,explode=false,name=ledger"` - // Query string to filter transactions. The query string must be a valid JSON object. - Query *string `queryParam:"style=form,explode=true,name=query"` // The maximum number of results to return per page. // PageSize *int64 `queryParam:"style=form,explode=true,name=pageSize"` @@ -72,13 +70,6 @@ func (o *V2ListTransactionsRequest) GetLedger() string { return o.Ledger } -func (o *V2ListTransactionsRequest) GetQuery() *string { - if o == nil { - return nil - } - return o.Query -} - func (o *V2ListTransactionsRequest) GetPageSize() *int64 { if o == nil { return nil From 2e3bb90ff9896b83f371c2adab9127d8613d863c Mon Sep 17 00:00:00 2001 From: Ragot Geoffrey Date: Mon, 9 Dec 2024 09:48:10 +0100 Subject: [PATCH 53/71] fix(bulk): regression on bulk endpoint using content type application/json (#597) --- internal/api/v2/controllers_bulk.go | 5 ++ internal/api/v2/controllers_bulk_test.go | 62 ++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/internal/api/v2/controllers_bulk.go b/internal/api/v2/controllers_bulk.go index 105acdb5c..5afe2b76c 100644 --- a/internal/api/v2/controllers_bulk.go +++ b/internal/api/v2/controllers_bulk.go @@ -4,6 +4,7 @@ import ( "errors" "github.com/formancehq/ledger/internal/api/bulking" "net/http" + "strings" "github.com/formancehq/go-libs/v2/api" "github.com/formancehq/ledger/internal/api/common" @@ -16,6 +17,10 @@ func bulkHandler(bulkerFactory bulking.BulkerFactory, bulkHandlerFactories map[s if contentType == "" { contentType = "application/json" } + if strings.Index(contentType, ";") > 0 { + contentType = strings.Split(contentType, ";")[0] + } + bulkHandlerFactory, ok := bulkHandlerFactories[contentType] if !ok { api.BadRequest(w, common.ErrValidation, errors.New("unsupported content type: "+contentType)) diff --git a/internal/api/v2/controllers_bulk_test.go b/internal/api/v2/controllers_bulk_test.go index 308d21104..db1e2eb72 100644 --- a/internal/api/v2/controllers_bulk_test.go +++ b/internal/api/v2/controllers_bulk_test.go @@ -37,6 +37,7 @@ func TestBulk(t *testing.T) { expectations func(mockLedger *LedgerController) expectError bool expectResults []bulking.APIResult + headers http.Header } testCases := []bulkTestCase{ @@ -417,6 +418,65 @@ func TestBulk(t *testing.T) { ResponseType: bulking.ActionAddMetadata, }}, }, + { + name: "with custom content type", + headers: map[string][]string{ + "Content-Type": {"application/json; charset=utf-8"}, + }, + body: fmt.Sprintf(`[{ + "action": "CREATE_TRANSACTION", + "data": { + "postings": [{ + "source": "world", + "destination": "bank", + "amount": 100, + "asset": "USD/2" + }], + "timestamp": "%s" + } + }]`, now.Format(time.RFC3339Nano)), + expectations: func(mockLedger *LedgerController) { + postings := []ledger.Posting{{ + Source: "world", + Destination: "bank", + Amount: big.NewInt(100), + Asset: "USD/2", + }} + mockLedger.EXPECT(). + CreateTransaction(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.RunScript]{ + Input: ledgercontroller.TxToScriptData(ledger.TransactionData{ + Postings: postings, + Timestamp: now, + }, false), + }). + Return(&ledger.Log{}, &ledger.CreatedTransaction{ + Transaction: ledger.Transaction{ + TransactionData: ledger.TransactionData{ + Postings: postings, + Metadata: metadata.Metadata{}, + Timestamp: now, + }, + }, + }, nil) + }, + expectResults: []bulking.APIResult{{ + Data: map[string]any{ + "postings": []any{ + map[string]any{ + "source": "world", + "destination": "bank", + "amount": float64(100), + "asset": "USD/2", + }, + }, + "timestamp": now.Format(time.RFC3339Nano), + "metadata": map[string]any{}, + "reverted": false, + "id": float64(0), + }, + ResponseType: bulking.ActionCreateTransaction, + }}, + }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { @@ -428,6 +488,8 @@ func TestBulk(t *testing.T) { router := NewRouter(systemController, auth.NewNoAuth(), os.Getenv("DEBUG") == "true") req := httptest.NewRequest(http.MethodPost, "/xxx/_bulk", bytes.NewBufferString(testCase.body)) + req.Header = testCase.headers + rec := httptest.NewRecorder() if testCase.queryParams != nil { req.URL.RawQuery = testCase.queryParams.Encode() From bf3c12cae7b8428cbc868c635b8933c0409960f4 Mon Sep 17 00:00:00 2001 From: Ragot Geoffrey Date: Mon, 9 Dec 2024 10:45:18 +0100 Subject: [PATCH 54/71] feat: Optimize migrations (#603) * wip * feat: add configurable timeout on generator image * feat: change startup probe using healthcheck instead of delaying the server startup * fix: move migration order to create an index concurrently * fix: blocking migration * fix: slow migration * fix: cache storage migration healthcheck result * fix: pg_attribute bloated * chore: change batch sizes for migrations --- cmd/buckets_upgrade.go | 2 +- cmd/root.go | 24 ++++-- deployments/pulumi/main.go | 16 ++-- deployments/pulumi/main_test.go | 75 +++++++++++------- deployments/pulumi/pkg/component.go | 78 +++++++++++-------- docs/api/README.md | 23 +++++- internal/api/common/errors.go | 1 - internal/api/v2/controllers_ledgers_create.go | 2 +- .../api/v2/controllers_ledgers_create_test.go | 2 +- internal/storage/bucket/bucket.go | 2 +- internal/storage/bucket/default_bucket.go | 4 +- .../storage/bucket/default_bucket_test.go | 2 +- internal/storage/bucket/migrations.go | 22 +----- .../migrations/11-make-stateless/up.sql | 4 +- .../notes.yaml | 1 + .../up.sql | 1 + .../notes.yaml | 0 .../up.sql | 8 +- .../up_tests_after.sql | 0 .../up_tests_before.sql | 0 .../notes.yaml | 0 .../up.sql | 2 +- .../up_tests_after.sql | 0 .../up_tests_before.sql | 0 .../18-transactions-fill-pcv/up.sql | 62 --------------- .../notes.yaml | 0 .../19-transactions-fill-pcv/up.sql | 63 +++++++++++++++ .../up_tests_after.sql | 0 .../up_tests_before.sql | 0 .../notes.yaml | 0 .../up.sql | 2 +- .../up_tests_after.sql | 0 .../up_tests_before.sql | 0 .../notes.yaml | 0 .../up.sql | 6 +- .../up_tests_after.sql | 0 .../up_tests_before.sql | 0 .../notes.yaml | 0 .../up.sql | 6 +- .../up_tests_after.sql | 0 .../up_tests_before.sql | 0 .../notes.yaml | 0 .../up.sql | 6 +- .../up_tests_after.sql | 0 .../up_tests_before.sql | 0 .../23-noop-keep-for-compatibility/notes.yaml | 1 - .../23-noop-keep-for-compatibility/up.sql | 0 .../storage/driver/buckets_generated_test.go | 8 +- internal/storage/driver/driver.go | 59 +++++++------- internal/storage/driver/driver_test.go | 29 +++---- .../storage/driver/system_generated_test.go | 15 ++++ internal/storage/ledger/legacy/main_test.go | 2 +- internal/storage/module.go | 39 +++++++--- internal/storage/system/store.go | 5 ++ openapi.yaml | 14 ++-- openapi/v2.yaml | 14 ++-- pkg/client/.speakeasy/gen.yaml | 2 +- .../docs/models/components/v2bulkresponse.md | 9 ++- pkg/client/formance.go | 4 +- .../models/components/v2bulkresponse.go | 26 ++++++- pkg/generate/generator.go | 21 +++++ pkg/testserver/server.go | 25 ++++-- test/e2e/app_lifecycle_test.go | 13 ++++ test/migrations/upgrade_test.go | 2 +- tools/generator/cmd/root.go | 49 ++++++++---- 65 files changed, 460 insertions(+), 291 deletions(-) create mode 100644 internal/storage/bucket/migrations/16-create-transaction-id-index-on-moves/notes.yaml create mode 100644 internal/storage/bucket/migrations/16-create-transaction-id-index-on-moves/up.sql rename internal/storage/bucket/migrations/{16-moves-fill-transaction-id => 17-moves-fill-transaction-id}/notes.yaml (100%) rename internal/storage/bucket/migrations/{16-moves-fill-transaction-id => 17-moves-fill-transaction-id}/up.sql (83%) rename internal/storage/bucket/migrations/{16-moves-fill-transaction-id => 17-moves-fill-transaction-id}/up_tests_after.sql (100%) rename internal/storage/bucket/migrations/{16-moves-fill-transaction-id => 17-moves-fill-transaction-id}/up_tests_before.sql (100%) rename internal/storage/bucket/migrations/{17-transactions-fill-inserted-at => 18-transactions-fill-inserted-at}/notes.yaml (100%) rename internal/storage/bucket/migrations/{17-transactions-fill-inserted-at => 18-transactions-fill-inserted-at}/up.sql (98%) rename internal/storage/bucket/migrations/{17-transactions-fill-inserted-at => 18-transactions-fill-inserted-at}/up_tests_after.sql (100%) rename internal/storage/bucket/migrations/{17-transactions-fill-inserted-at => 18-transactions-fill-inserted-at}/up_tests_before.sql (100%) delete mode 100644 internal/storage/bucket/migrations/18-transactions-fill-pcv/up.sql rename internal/storage/bucket/migrations/{18-transactions-fill-pcv => 19-transactions-fill-pcv}/notes.yaml (100%) create mode 100644 internal/storage/bucket/migrations/19-transactions-fill-pcv/up.sql rename internal/storage/bucket/migrations/{18-transactions-fill-pcv => 19-transactions-fill-pcv}/up_tests_after.sql (100%) rename internal/storage/bucket/migrations/{18-transactions-fill-pcv => 19-transactions-fill-pcv}/up_tests_before.sql (100%) rename internal/storage/bucket/migrations/{19-accounts-volumes-fill-history => 20-accounts-volumes-fill-history}/notes.yaml (100%) rename internal/storage/bucket/migrations/{19-accounts-volumes-fill-history => 20-accounts-volumes-fill-history}/up.sql (97%) rename internal/storage/bucket/migrations/{19-accounts-volumes-fill-history => 20-accounts-volumes-fill-history}/up_tests_after.sql (100%) rename internal/storage/bucket/migrations/{19-accounts-volumes-fill-history => 20-accounts-volumes-fill-history}/up_tests_before.sql (100%) rename internal/storage/bucket/migrations/{20-transactions-metadata-fill-transaction-id => 21-transactions-metadata-fill-transaction-id}/notes.yaml (100%) rename internal/storage/bucket/migrations/{20-transactions-metadata-fill-transaction-id => 21-transactions-metadata-fill-transaction-id}/up.sql (87%) rename internal/storage/bucket/migrations/{20-transactions-metadata-fill-transaction-id => 21-transactions-metadata-fill-transaction-id}/up_tests_after.sql (100%) rename internal/storage/bucket/migrations/{20-transactions-metadata-fill-transaction-id => 21-transactions-metadata-fill-transaction-id}/up_tests_before.sql (100%) rename internal/storage/bucket/migrations/{21-accounts-metadata-fill-address => 22-accounts-metadata-fill-address}/notes.yaml (100%) rename internal/storage/bucket/migrations/{21-accounts-metadata-fill-address => 22-accounts-metadata-fill-address}/up.sql (86%) rename internal/storage/bucket/migrations/{21-accounts-metadata-fill-address => 22-accounts-metadata-fill-address}/up_tests_after.sql (100%) rename internal/storage/bucket/migrations/{21-accounts-metadata-fill-address => 22-accounts-metadata-fill-address}/up_tests_before.sql (100%) rename internal/storage/bucket/migrations/{22-logs-fill-memento => 23-logs-fill-memento}/notes.yaml (100%) rename internal/storage/bucket/migrations/{22-logs-fill-memento => 23-logs-fill-memento}/up.sql (85%) rename internal/storage/bucket/migrations/{22-logs-fill-memento => 23-logs-fill-memento}/up_tests_after.sql (100%) rename internal/storage/bucket/migrations/{22-logs-fill-memento => 23-logs-fill-memento}/up_tests_before.sql (100%) delete mode 100644 internal/storage/bucket/migrations/23-noop-keep-for-compatibility/notes.yaml delete mode 100644 internal/storage/bucket/migrations/23-noop-keep-for-compatibility/up.sql diff --git a/cmd/buckets_upgrade.go b/cmd/buckets_upgrade.go index 61f578b6e..e48164882 100644 --- a/cmd/buckets_upgrade.go +++ b/cmd/buckets_upgrade.go @@ -30,7 +30,7 @@ func NewBucketUpgrade() *cobra.Command { }() if args[0] == "*" { - return driver.UpgradeAllBuckets(cmd.Context(), make(chan struct{})) + return driver.UpgradeAllBuckets(cmd.Context()) } return driver.UpgradeBucket(cmd.Context(), args[0]) diff --git a/cmd/root.go b/cmd/root.go index e50555cd0..ac9eed7f7 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,7 +2,12 @@ package cmd import ( "github.com/formancehq/go-libs/v2/bun/bunmigrate" + "github.com/formancehq/go-libs/v2/logging" "github.com/formancehq/go-libs/v2/service" + "github.com/formancehq/ledger/internal/storage/bucket" + "github.com/formancehq/ledger/internal/storage/driver" + "github.com/formancehq/ledger/internal/storage/ledger" + systemstore "github.com/formancehq/ledger/internal/storage/system" "github.com/uptrace/bun" "github.com/spf13/cobra" @@ -34,17 +39,20 @@ func NewRootCommand() *cobra.Command { root.AddCommand(serve) root.AddCommand(buckets) root.AddCommand(version) - root.AddCommand(bunmigrate.NewDefaultCommand(func(cmd *cobra.Command, _ []string, _ *bun.DB) error { - // todo: use provided db ... - driver, db, err := getDriver(cmd) - if err != nil { + root.AddCommand(bunmigrate.NewDefaultCommand(func(cmd *cobra.Command, _ []string, db *bun.DB) error { + logger := logging.NewDefaultLogger(cmd.OutOrStdout(), service.IsDebug(cmd), false, false) + cmd.SetContext(logging.ContextWithLogger(cmd.Context(), logger)) + + driver := driver.New( + ledger.NewFactory(db), + systemstore.New(db), + bucket.NewDefaultFactory(db), + ) + if err := driver.Initialize(cmd.Context()); err != nil { return err } - defer func() { - _ = db.Close() - }() - return driver.UpgradeAllBuckets(cmd.Context(), make(chan struct{})) + return driver.UpgradeAllBuckets(cmd.Context()) })) root.AddCommand(NewDocsCommand()) diff --git a/deployments/pulumi/main.go b/deployments/pulumi/main.go index 972f227bb..f7121f448 100644 --- a/deployments/pulumi/main.go +++ b/deployments/pulumi/main.go @@ -6,6 +6,7 @@ import ( "github.com/formancehq/ledger/deployments/pulumi/pkg" "github.com/pulumi/pulumi/sdk/v3/go/pulumi" "github.com/pulumi/pulumi/sdk/v3/go/pulumi/config" + "github.com/pulumi/pulumi/sdk/v3/go/pulumix" ) func main() { @@ -35,23 +36,18 @@ func deploy(ctx *pulumi.Context) error { } } - debug, _ := conf.TryBool("debug") - imagePullPolicy, _ := conf.Try("image.pullPolicy") - - replicaCount, _ := conf.TryInt("replicaCount") - experimentalFeatures, _ := conf.TryBool("experimentalFeatures") - _, err = pulumi_ledger.NewComponent(ctx, "ledger", &pulumi_ledger.ComponentArgs{ Namespace: pulumi.String(namespace), Timeout: pulumi.Int(timeout), Tag: pulumi.String(version), - ImagePullPolicy: pulumi.String(imagePullPolicy), + ImagePullPolicy: pulumi.String(conf.Get("image.pullPolicy")), Postgres: pulumi_ledger.PostgresArgs{ URI: pulumi.String(postgresURI), }, - Debug: pulumi.Bool(debug), - ReplicaCount: pulumi.Int(replicaCount), - ExperimentalFeatures: pulumi.Bool(experimentalFeatures), + Debug: pulumi.Bool(conf.GetBool("debug")), + ReplicaCount: pulumi.Int(conf.GetInt("replicaCount")), + ExperimentalFeatures: pulumi.Bool(conf.GetBool("experimentalFeatures")), + Upgrade: pulumix.Val(pulumi_ledger.UpgradeMode(config.Get(ctx, "upgrade-mode"))), }) return err diff --git a/deployments/pulumi/main_test.go b/deployments/pulumi/main_test.go index c3f21130c..5baea2450 100644 --- a/deployments/pulumi/main_test.go +++ b/deployments/pulumi/main_test.go @@ -19,38 +19,59 @@ import ( func TestProgram(t *testing.T) { - ctx := logging.TestingContext() - stackName := "ledger-tests-pulumi-" + uuid.NewString()[:8] + type testCase struct { + name string + config map[string]string + } + for _, tc := range []testCase{ + { + name: "nominal", + config: map[string]string{ + "timeout": "30", + }, + }, + { + name: "upgrade using a job", + config: map[string]string{ + "timeout": "30", + "upgrade-mode": "job", + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + ctx := logging.TestingContext() + stackName := "ledger-tests-pulumi-" + uuid.NewString()[:8] - stack, err := auto.UpsertStackInlineSource(ctx, stackName, "ledger-tests-pulumi-postgres", deployPostgres(stackName)) - require.NoError(t, err) + stack, err := auto.UpsertStackInlineSource(ctx, stackName, "ledger-tests-pulumi-postgres", deployPostgres(stackName)) + require.NoError(t, err) - t.Log("Deploy pg stack") - up, err := stack.Up(ctx, optup.ProgressStreams(os.Stdout), optup.ErrorProgressStreams(os.Stderr)) - require.NoError(t, err) + t.Log("Deploy pg stack") + up, err := stack.Up(ctx, optup.ProgressStreams(os.Stdout), optup.ErrorProgressStreams(os.Stderr)) + require.NoError(t, err) - t.Cleanup(func() { - t.Log("Destroy stack") - _, err := stack.Destroy(ctx, optdestroy.Remove(), optdestroy.ProgressStreams(os.Stdout), optdestroy.ErrorProgressStreams(os.Stderr)) - require.NoError(t, err) - }) + t.Cleanup(func() { + t.Log("Destroy stack") + _, err := stack.Destroy(ctx, optdestroy.Remove(), optdestroy.ProgressStreams(os.Stdout), optdestroy.ErrorProgressStreams(os.Stderr)) + require.NoError(t, err) + }) - postgresURI := up.Outputs["uri"].Value.(string) + postgresURI := up.Outputs["uri"].Value.(string) - t.Log("Test program") - integration.ProgramTest(t, &integration.ProgramTestOptions{ - Quick: true, - SkipRefresh: true, - Dir: ".", - Config: map[string]string{ - "namespace": stackName, - "postgres.uri": postgresURI, - "timeout": "30", - }, - Stdout: os.Stdout, - Stderr: os.Stderr, - Verbose: testing.Verbose(), - }) + tc.config["postgres.uri"] = postgresURI + tc.config["namespace"] = stackName + + t.Log("Test program") + integration.ProgramTest(t, &integration.ProgramTestOptions{ + Quick: true, + SkipRefresh: true, + Dir: ".", + Config: tc.config, + Stdout: os.Stdout, + Stderr: os.Stderr, + Verbose: testing.Verbose(), + }) + }) + } } func deployPostgres(stackName string) func(ctx *pulumi.Context) error { diff --git a/deployments/pulumi/pkg/component.go b/deployments/pulumi/pkg/component.go index 5964c6855..e8e7b7acc 100644 --- a/deployments/pulumi/pkg/component.go +++ b/deployments/pulumi/pkg/component.go @@ -15,6 +15,14 @@ import ( var ErrPostgresURIRequired = fmt.Errorf("postgresURI is required") +type UpgradeMode string + +const ( + UpgradeModeDisabled UpgradeMode = "disabled" + UpgradeModeJob UpgradeMode = "job" + UpgradeModeInApp UpgradeMode = "in-app" +) + type Component struct { pulumi.ResourceState @@ -22,7 +30,6 @@ type Component struct { ServiceNamespace pulumix.Output[string] ServicePort pulumix.Output[int] ServiceInternalURL pulumix.Output[string] - Migrations pulumix.Output[*batchv1.Job] } type PostgresArgs struct { @@ -73,13 +80,12 @@ type ComponentArgs struct { Debug pulumix.Input[bool] ReplicaCount pulumix.Input[int] GracePeriod pulumix.Input[string] - AutoUpgrade pulumix.Input[bool] - WaitUpgrade pulumix.Input[bool] BallastSizeInBytes pulumix.Input[int] NumscriptCacheMaxCount pulumix.Input[int] BulkMaxSize pulumix.Input[int] BulkParallel pulumix.Input[int] TerminationGracePeriodSeconds pulumix.Input[*int] + Upgrade pulumix.Input[UpgradeMode] ExperimentalFeatures pulumix.Input[bool] ExperimentalNumscriptInterpreter pulumix.Input[bool] @@ -129,14 +135,29 @@ func NewComponent(ctx *pulumi.Context, name string, args *ComponentArgs, opts .. } ledgerImage := pulumi.Sprintf("ghcr.io/formancehq/ledger:%s", tag) - autoUpgrade := pulumix.Val(true) - if args.AutoUpgrade != nil { - autoUpgrade = args.AutoUpgrade.ToOutput(ctx.Context()) + upgradeMode := UpgradeModeInApp + if args.Upgrade != nil { + var ( + upgradeModeChan = make(chan UpgradeMode, 1) + ) + pulumix.ApplyErr(args.Upgrade, func(upgradeMode UpgradeMode) (any, error) { + upgradeModeChan <- upgradeMode + close(upgradeModeChan) + return nil, nil + }) + + select { + case <-ctx.Context().Done(): + return nil, ctx.Context().Err() + case upgradeMode = <-upgradeModeChan: + if upgradeMode == "" { + upgradeMode = UpgradeModeInApp + } + } } - waitUpgrade := pulumix.Val(true) - if args.WaitUpgrade != nil { - waitUpgrade = args.WaitUpgrade.ToOutput(ctx.Context()) + if upgradeMode != "" && upgradeMode != UpgradeModeDisabled && upgradeMode != UpgradeModeJob && upgradeMode != UpgradeModeInApp { + return nil, fmt.Errorf("invalid upgrade mode: %s", upgradeMode) } imagePullPolicy := pulumix.Val("") @@ -351,18 +372,10 @@ func NewComponent(ctx *pulumi.Context, name string, args *ComponentArgs, opts .. }) } - if args.AutoUpgrade != nil { + if upgradeMode == UpgradeModeInApp { envVars = append(envVars, corev1.EnvVarArgs{ - Name: pulumi.String("AUTO_UPGRADE"), - Value: pulumix.Apply2Err(autoUpgrade, waitUpgrade, func(autoUpgrade, waitUpgrade bool) (string, error) { - if waitUpgrade && !autoUpgrade { - return "", fmt.Errorf("waitUpgrade requires autoUpgrade to be true") - } - if !autoUpgrade { - return "false", nil - } - return "true", nil - }).Untyped().(pulumi.StringOutput), + Name: pulumi.String("AUTO_UPGRADE"), + Value: pulumi.String("true"), }) } @@ -472,7 +485,8 @@ func NewComponent(ctx *pulumi.Context, name string, args *ComponentArgs, opts .. Port: pulumi.String("http"), }, FailureThreshold: pulumi.Int(1), - PeriodSeconds: pulumi.Int(10), + PeriodSeconds: pulumi.Int(60), + TimeoutSeconds: pulumi.IntPtr(3), }, ReadinessProbe: corev1.ProbeArgs{ HttpGet: corev1.HTTPGetActionArgs{ @@ -480,15 +494,17 @@ func NewComponent(ctx *pulumi.Context, name string, args *ComponentArgs, opts .. Port: pulumi.String("http"), }, FailureThreshold: pulumi.Int(1), - PeriodSeconds: pulumi.Int(10), + PeriodSeconds: pulumi.Int(60), + TimeoutSeconds: pulumi.IntPtr(3), }, StartupProbe: corev1.ProbeArgs{ HttpGet: corev1.HTTPGetActionArgs{ Path: pulumi.String("/_healthcheck"), Port: pulumi.String("http"), }, - FailureThreshold: pulumi.Int(60), - PeriodSeconds: pulumi.Int(5), + PeriodSeconds: pulumi.Int(5), + InitialDelaySeconds: pulumi.IntPtr(2), + TimeoutSeconds: pulumi.IntPtr(3), }, Env: envVars, }, @@ -501,11 +517,8 @@ func NewComponent(ctx *pulumi.Context, name string, args *ComponentArgs, opts .. return nil, err } - cmp.Migrations = pulumix.ApplyErr(waitUpgrade, func(waitUpgrade bool) (*batchv1.Job, error) { - if !waitUpgrade { - return nil, nil - } - return batchv1.NewJob(ctx, "wait-migration-completion", &batchv1.JobArgs{ + if upgradeMode == UpgradeModeJob { + _, err = batchv1.NewJob(ctx, "migrate", &batchv1.JobArgs{ Metadata: &metav1.ObjectMetaArgs{ Namespace: namespace.Untyped().(pulumi.StringOutput), }, @@ -515,7 +528,7 @@ func NewComponent(ctx *pulumi.Context, name string, args *ComponentArgs, opts .. RestartPolicy: pulumi.String("OnFailure"), Containers: corev1.ContainerArray{ corev1.ContainerArgs{ - Name: pulumi.String("check"), + Name: pulumi.String("migrate"), Args: pulumi.StringArray{ pulumi.String("migrate"), }, @@ -537,7 +550,10 @@ func NewComponent(ctx *pulumi.Context, name string, args *ComponentArgs, opts .. }, }, }, pulumi.Parent(cmp)) - }) + if err != nil { + return nil, err + } + } service, err := corev1.NewService(ctx, "ledger", &corev1.ServiceArgs{ Metadata: &metav1.ObjectMetaArgs{ diff --git a/docs/api/README.md b/docs/api/README.md index 52cc516f7..6dee2686e 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -597,7 +597,10 @@ Accept: application/json } } } - ] + ], + "errorCode": "VALIDATION", + "errorMessage": "[VALIDATION] invalid 'cursor' query param", + "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9" } ``` @@ -3244,7 +3247,7 @@ Authorization ( Scopes: ledger:write ) |*anonymous*|INTERPRETER_PARSE| |*anonymous*|INTERPRETER_RUNTIME| |*anonymous*|LEDGER_ALREADY_EXISTS| -|*anonymous*|BUCKET_OUTDATED| +|*anonymous*|OUTDATED_SCHEMA|

V2LedgerInfoResponse

@@ -3788,16 +3791,28 @@ and } } } - ] + ], + "errorCode": "VALIDATION", + "errorMessage": "[VALIDATION] invalid 'cursor' query param", + "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9" } ``` ### Properties +allOf + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|object|false|none|none| +|» data|[[V2BulkElementResult](#schemav2bulkelementresult)]|false|none|none| + +and + |Name|Type|Required|Restrictions|Description| |---|---|---|---|---| -|data|[[V2BulkElementResult](#schemav2bulkelementresult)]|true|none|none| +|*anonymous*|[V2ErrorResponse](#schemav2errorresponse)|false|none|none|

V2BulkElementResult

diff --git a/internal/api/common/errors.go b/internal/api/common/errors.go index 0e440d6c6..ff8b86e53 100644 --- a/internal/api/common/errors.go +++ b/internal/api/common/errors.go @@ -17,7 +17,6 @@ const ( ErrMetadataOverride = "METADATA_OVERRIDE" ErrBulkSizeExceeded = "BULK_SIZE_EXCEEDED" ErrLedgerAlreadyExists = "LEDGER_ALREADY_EXISTS" - ErrBucketOutdated = "BUCKET_OUTDATED" ErrInterpreterParse = "INTERPRETER_PARSE" ErrInterpreterRuntime = "INTERPRETER_RUNTIME" diff --git a/internal/api/v2/controllers_ledgers_create.go b/internal/api/v2/controllers_ledgers_create.go index 2c59eb0b1..3b7b20898 100644 --- a/internal/api/v2/controllers_ledgers_create.go +++ b/internal/api/v2/controllers_ledgers_create.go @@ -38,7 +38,7 @@ func createLedger(systemController system.Controller) http.HandlerFunc { errors.Is(err, ledger.ErrInvalidBucketName{}): api.BadRequest(w, common.ErrValidation, err) case errors.Is(err, system.ErrBucketOutdated): - api.BadRequest(w, common.ErrBucketOutdated, err) + api.BadRequest(w, common.ErrOutdatedSchema, err) case errors.Is(err, system.ErrLedgerAlreadyExists): api.BadRequest(w, common.ErrLedgerAlreadyExists, err) default: diff --git a/internal/api/v2/controllers_ledgers_create_test.go b/internal/api/v2/controllers_ledgers_create_test.go index 60bcca31c..eb4265715 100644 --- a/internal/api/v2/controllers_ledgers_create_test.go +++ b/internal/api/v2/controllers_ledgers_create_test.go @@ -84,7 +84,7 @@ func TestLedgersCreate(t *testing.T) { expectedBackendCall: true, returnErr: system.ErrBucketOutdated, expectStatusCode: http.StatusBadRequest, - expectErrorCode: common.ErrBucketOutdated, + expectErrorCode: common.ErrOutdatedSchema, }, { name: "unexpected error", diff --git a/internal/storage/bucket/bucket.go b/internal/storage/bucket/bucket.go index ff353a179..48a6b9316 100644 --- a/internal/storage/bucket/bucket.go +++ b/internal/storage/bucket/bucket.go @@ -10,7 +10,7 @@ import ( ) type Bucket interface { - Migrate(ctx context.Context, minimalVersionReached chan struct{}, opts ...migrations.Option) error + Migrate(ctx context.Context, opts ...migrations.Option) error AddLedger(ctx context.Context, ledger ledger.Ledger) error HasMinimalVersion(ctx context.Context) (bool, error) IsUpToDate(ctx context.Context) (bool, error) diff --git a/internal/storage/bucket/default_bucket.go b/internal/storage/bucket/default_bucket.go index 0b3b89409..1b9fb8233 100644 --- a/internal/storage/bucket/default_bucket.go +++ b/internal/storage/bucket/default_bucket.go @@ -38,8 +38,8 @@ func (b *DefaultBucket) IsUpToDate(ctx context.Context) (bool, error) { return GetMigrator(b.db, b.name).IsUpToDate(ctx) } -func (b *DefaultBucket) Migrate(ctx context.Context, minimalVersionReached chan struct{}, options ...migrations.Option) error { - return migrate(ctx, b.tracer, b.db, b.name, minimalVersionReached, options...) +func (b *DefaultBucket) Migrate(ctx context.Context, options ...migrations.Option) error { + return migrate(ctx, b.tracer, b.db, b.name, options...) } func (b *DefaultBucket) HasMinimalVersion(ctx context.Context) (bool, error) { diff --git a/internal/storage/bucket/default_bucket_test.go b/internal/storage/bucket/default_bucket_test.go index 2c81a1111..0bb3b971d 100644 --- a/internal/storage/bucket/default_bucket_test.go +++ b/internal/storage/bucket/default_bucket_test.go @@ -31,5 +31,5 @@ func TestBuckets(t *testing.T) { require.NoError(t, system.Migrate(ctx, db)) b := bucket.NewDefault(db, noop.Tracer{}, name) - require.NoError(t, b.Migrate(ctx, make(chan struct{}))) + require.NoError(t, b.Migrate(ctx)) } diff --git a/internal/storage/bucket/migrations.go b/internal/storage/bucket/migrations.go index 32e02802a..d44e702a3 100644 --- a/internal/storage/bucket/migrations.go +++ b/internal/storage/bucket/migrations.go @@ -24,21 +24,11 @@ func GetMigrator(db *bun.DB, name string, options ...migrations.Option) *migrati return migrator } -func migrate(ctx context.Context, tracer trace.Tracer, db *bun.DB, name string, minimalVersionReached chan struct{}, options ...migrations.Option) error { +func migrate(ctx context.Context, tracer trace.Tracer, db *bun.DB, name string, options ...migrations.Option) error { ctx, span := tracer.Start(ctx, "Migrate bucket") defer span.End() migrator := GetMigrator(db, name, options...) - version, err := migrator.GetLastVersion(ctx) - if err != nil { - if !errors.Is(err, migrations.ErrMissingVersionTable) { - return err - } - } - - if version >= MinimalSchemaVersion { - close(minimalVersionReached) - } for { err := migrator.UpByOne(ctx) @@ -48,15 +38,5 @@ func migrate(ctx context.Context, tracer trace.Tracer, db *bun.DB, name string, } return err } - version++ - - if version >= MinimalSchemaVersion { - select { - case <-minimalVersionReached: - // already closed - default: - close(minimalVersionReached) - } - } } } diff --git a/internal/storage/bucket/migrations/11-make-stateless/up.sql b/internal/storage/bucket/migrations/11-make-stateless/up.sql index 251d1448a..33c37515b 100644 --- a/internal/storage/bucket/migrations/11-make-stateless/up.sql +++ b/internal/storage/bucket/migrations/11-make-stateless/up.sql @@ -4,7 +4,7 @@ create or replace function transaction_date() returns timestamp as $$ declare ret timestamp without time zone; begin - create temporary table if not exists transaction_date on commit drop as + create temporary table if not exists transaction_date on commit delete rows as select statement_timestamp(); select * @@ -16,7 +16,7 @@ create or replace function transaction_date() returns timestamp as $$ ret = statement_timestamp(); insert into transaction_date - select ret; + select ret at time zone 'utc'; end if; return ret at time zone 'utc'; diff --git a/internal/storage/bucket/migrations/16-create-transaction-id-index-on-moves/notes.yaml b/internal/storage/bucket/migrations/16-create-transaction-id-index-on-moves/notes.yaml new file mode 100644 index 000000000..1a8995d82 --- /dev/null +++ b/internal/storage/bucket/migrations/16-create-transaction-id-index-on-moves/notes.yaml @@ -0,0 +1 @@ +name: Create transaction id index on moves diff --git a/internal/storage/bucket/migrations/16-create-transaction-id-index-on-moves/up.sql b/internal/storage/bucket/migrations/16-create-transaction-id-index-on-moves/up.sql new file mode 100644 index 000000000..de13279d0 --- /dev/null +++ b/internal/storage/bucket/migrations/16-create-transaction-id-index-on-moves/up.sql @@ -0,0 +1 @@ +create index concurrently moves_transactions_id on "{{ .Schema }}".moves(transactions_id); \ No newline at end of file diff --git a/internal/storage/bucket/migrations/16-moves-fill-transaction-id/notes.yaml b/internal/storage/bucket/migrations/17-moves-fill-transaction-id/notes.yaml similarity index 100% rename from internal/storage/bucket/migrations/16-moves-fill-transaction-id/notes.yaml rename to internal/storage/bucket/migrations/17-moves-fill-transaction-id/notes.yaml diff --git a/internal/storage/bucket/migrations/16-moves-fill-transaction-id/up.sql b/internal/storage/bucket/migrations/17-moves-fill-transaction-id/up.sql similarity index 83% rename from internal/storage/bucket/migrations/16-moves-fill-transaction-id/up.sql rename to internal/storage/bucket/migrations/17-moves-fill-transaction-id/up.sql index 5c486cf6e..735c891c8 100644 --- a/internal/storage/bucket/migrations/16-moves-fill-transaction-id/up.sql +++ b/internal/storage/bucket/migrations/17-moves-fill-transaction-id/up.sql @@ -1,12 +1,10 @@ do $$ declare - _batch_size integer := 100; + _batch_size integer := 1000; _max integer; begin set search_path = '{{.Schema}}'; - create index moves_transactions_id on moves(transactions_id); - select count(seq) from moves where transactions_id is null @@ -38,7 +36,9 @@ do $$ end loop; alter table moves - alter column transactions_id set not null; + add constraint transactions_id_not_null + check (transactions_id is not null) + not valid; end $$ language plpgsql; \ No newline at end of file diff --git a/internal/storage/bucket/migrations/16-moves-fill-transaction-id/up_tests_after.sql b/internal/storage/bucket/migrations/17-moves-fill-transaction-id/up_tests_after.sql similarity index 100% rename from internal/storage/bucket/migrations/16-moves-fill-transaction-id/up_tests_after.sql rename to internal/storage/bucket/migrations/17-moves-fill-transaction-id/up_tests_after.sql diff --git a/internal/storage/bucket/migrations/16-moves-fill-transaction-id/up_tests_before.sql b/internal/storage/bucket/migrations/17-moves-fill-transaction-id/up_tests_before.sql similarity index 100% rename from internal/storage/bucket/migrations/16-moves-fill-transaction-id/up_tests_before.sql rename to internal/storage/bucket/migrations/17-moves-fill-transaction-id/up_tests_before.sql diff --git a/internal/storage/bucket/migrations/17-transactions-fill-inserted-at/notes.yaml b/internal/storage/bucket/migrations/18-transactions-fill-inserted-at/notes.yaml similarity index 100% rename from internal/storage/bucket/migrations/17-transactions-fill-inserted-at/notes.yaml rename to internal/storage/bucket/migrations/18-transactions-fill-inserted-at/notes.yaml diff --git a/internal/storage/bucket/migrations/17-transactions-fill-inserted-at/up.sql b/internal/storage/bucket/migrations/18-transactions-fill-inserted-at/up.sql similarity index 98% rename from internal/storage/bucket/migrations/17-transactions-fill-inserted-at/up.sql rename to internal/storage/bucket/migrations/18-transactions-fill-inserted-at/up.sql index 6adca135f..fdc93ea71 100644 --- a/internal/storage/bucket/migrations/17-transactions-fill-inserted-at/up.sql +++ b/internal/storage/bucket/migrations/18-transactions-fill-inserted-at/up.sql @@ -1,6 +1,6 @@ do $$ declare - _batch_size integer := 100; + _batch_size integer := 1000; _date timestamp without time zone; _count integer := 0; begin diff --git a/internal/storage/bucket/migrations/17-transactions-fill-inserted-at/up_tests_after.sql b/internal/storage/bucket/migrations/18-transactions-fill-inserted-at/up_tests_after.sql similarity index 100% rename from internal/storage/bucket/migrations/17-transactions-fill-inserted-at/up_tests_after.sql rename to internal/storage/bucket/migrations/18-transactions-fill-inserted-at/up_tests_after.sql diff --git a/internal/storage/bucket/migrations/17-transactions-fill-inserted-at/up_tests_before.sql b/internal/storage/bucket/migrations/18-transactions-fill-inserted-at/up_tests_before.sql similarity index 100% rename from internal/storage/bucket/migrations/17-transactions-fill-inserted-at/up_tests_before.sql rename to internal/storage/bucket/migrations/18-transactions-fill-inserted-at/up_tests_before.sql diff --git a/internal/storage/bucket/migrations/18-transactions-fill-pcv/up.sql b/internal/storage/bucket/migrations/18-transactions-fill-pcv/up.sql deleted file mode 100644 index 39dd9e9f4..000000000 --- a/internal/storage/bucket/migrations/18-transactions-fill-pcv/up.sql +++ /dev/null @@ -1,62 +0,0 @@ -do $$ - declare - _batch_size integer := 100; - _count integer; - begin - set search_path = '{{.Schema}}'; - - select count(id) - from transactions - where post_commit_volumes is null - into _count; - - perform pg_notify('migrations-{{ .Schema }}', 'init: ' || _count); - - loop - -- disable triggers - set session_replication_role = replica; - - with _outdated_transactions as ( - select id - from transactions - where post_commit_volumes is null - limit _batch_size - ) - update transactions - set post_commit_volumes = ( - select public.aggregate_objects(post_commit_volumes::jsonb) as post_commit_volumes - from ( - select accounts_address, json_build_object(accounts_address, post_commit_volumes) post_commit_volumes - from ( - select accounts_address, json_build_object(asset, post_commit_volumes) as post_commit_volumes - from ( - select distinct on (accounts_address, asset) - accounts_address, - asset, - first_value(post_commit_volumes) over ( - partition by accounts_address, asset - order by seq desc - ) as post_commit_volumes - from moves - where transactions_id = transactions.id and ledger = transactions.ledger - ) moves - ) values - ) values - ) - from _outdated_transactions - where transactions.id in (_outdated_transactions.id); - - -- enable triggers - set session_replication_role = default; - - exit when not found; - - commit; - - perform pg_notify('migrations-{{ .Schema }}', 'continue: ' || _batch_size); - end loop; - - alter table transactions - alter column post_commit_volumes set not null; - end -$$; \ No newline at end of file diff --git a/internal/storage/bucket/migrations/18-transactions-fill-pcv/notes.yaml b/internal/storage/bucket/migrations/19-transactions-fill-pcv/notes.yaml similarity index 100% rename from internal/storage/bucket/migrations/18-transactions-fill-pcv/notes.yaml rename to internal/storage/bucket/migrations/19-transactions-fill-pcv/notes.yaml diff --git a/internal/storage/bucket/migrations/19-transactions-fill-pcv/up.sql b/internal/storage/bucket/migrations/19-transactions-fill-pcv/up.sql new file mode 100644 index 000000000..d3cf97488 --- /dev/null +++ b/internal/storage/bucket/migrations/19-transactions-fill-pcv/up.sql @@ -0,0 +1,63 @@ +do $$ + declare + _offset integer := 0; + _batch_size integer := 1000; + begin + set search_path = '{{ .Schema }}'; + + drop table if exists moves_view; + + create temp table moves_view as + select transactions_id::numeric, public.aggregate_objects(json_build_object(accounts_address, json_build_object(asset, post_commit_volumes))::jsonb) as volumes + from ( + SELECT DISTINCT ON (moves.transactions_id, accounts_address, asset) moves.transactions_id, accounts_address, asset, + first_value(post_commit_volumes) OVER ( + PARTITION BY moves.transactions_id, accounts_address, asset + ORDER BY seq DESC + ) AS post_commit_volumes + FROM moves + where insertion_date < ( + select tstamp from goose_db_version where version_id = 12 + ) + ) moves + group by transactions_id; + + perform pg_notify('migrations-{{ .Schema }}', 'init: ' || (select count(*) from moves_view)); + + create index moves_view_idx on moves_view(transactions_id); + + -- disable triggers + set session_replication_role = replica; + + loop + with data as ( + select transactions_id, volumes + from moves_view + -- play better than offset/limit + where transactions_id >= _offset and transactions_id < _offset + _batch_size + ) + update transactions + set post_commit_volumes = data.volumes + from data + where transactions.id = data.transactions_id; + + exit when not found; + + _offset = _offset + _batch_size; + + perform pg_notify('migrations-{{ .Schema }}', 'continue: ' || _batch_size); + + commit; + end loop; + + -- enable triggers + set session_replication_role = default; + + drop table if exists moves_view; + + alter table transactions + add constraint post_commit_volumes_not_null + check (post_commit_volumes is not null) + not valid; + end +$$; diff --git a/internal/storage/bucket/migrations/18-transactions-fill-pcv/up_tests_after.sql b/internal/storage/bucket/migrations/19-transactions-fill-pcv/up_tests_after.sql similarity index 100% rename from internal/storage/bucket/migrations/18-transactions-fill-pcv/up_tests_after.sql rename to internal/storage/bucket/migrations/19-transactions-fill-pcv/up_tests_after.sql diff --git a/internal/storage/bucket/migrations/18-transactions-fill-pcv/up_tests_before.sql b/internal/storage/bucket/migrations/19-transactions-fill-pcv/up_tests_before.sql similarity index 100% rename from internal/storage/bucket/migrations/18-transactions-fill-pcv/up_tests_before.sql rename to internal/storage/bucket/migrations/19-transactions-fill-pcv/up_tests_before.sql diff --git a/internal/storage/bucket/migrations/19-accounts-volumes-fill-history/notes.yaml b/internal/storage/bucket/migrations/20-accounts-volumes-fill-history/notes.yaml similarity index 100% rename from internal/storage/bucket/migrations/19-accounts-volumes-fill-history/notes.yaml rename to internal/storage/bucket/migrations/20-accounts-volumes-fill-history/notes.yaml diff --git a/internal/storage/bucket/migrations/19-accounts-volumes-fill-history/up.sql b/internal/storage/bucket/migrations/20-accounts-volumes-fill-history/up.sql similarity index 97% rename from internal/storage/bucket/migrations/19-accounts-volumes-fill-history/up.sql rename to internal/storage/bucket/migrations/20-accounts-volumes-fill-history/up.sql index f77f2a0ec..56083891f 100644 --- a/internal/storage/bucket/migrations/19-accounts-volumes-fill-history/up.sql +++ b/internal/storage/bucket/migrations/20-accounts-volumes-fill-history/up.sql @@ -1,7 +1,7 @@ do $$ declare _count integer; - _batch_size integer := 100; + _batch_size integer := 1000; begin set search_path = '{{.Schema}}'; diff --git a/internal/storage/bucket/migrations/19-accounts-volumes-fill-history/up_tests_after.sql b/internal/storage/bucket/migrations/20-accounts-volumes-fill-history/up_tests_after.sql similarity index 100% rename from internal/storage/bucket/migrations/19-accounts-volumes-fill-history/up_tests_after.sql rename to internal/storage/bucket/migrations/20-accounts-volumes-fill-history/up_tests_after.sql diff --git a/internal/storage/bucket/migrations/19-accounts-volumes-fill-history/up_tests_before.sql b/internal/storage/bucket/migrations/20-accounts-volumes-fill-history/up_tests_before.sql similarity index 100% rename from internal/storage/bucket/migrations/19-accounts-volumes-fill-history/up_tests_before.sql rename to internal/storage/bucket/migrations/20-accounts-volumes-fill-history/up_tests_before.sql diff --git a/internal/storage/bucket/migrations/20-transactions-metadata-fill-transaction-id/notes.yaml b/internal/storage/bucket/migrations/21-transactions-metadata-fill-transaction-id/notes.yaml similarity index 100% rename from internal/storage/bucket/migrations/20-transactions-metadata-fill-transaction-id/notes.yaml rename to internal/storage/bucket/migrations/21-transactions-metadata-fill-transaction-id/notes.yaml diff --git a/internal/storage/bucket/migrations/20-transactions-metadata-fill-transaction-id/up.sql b/internal/storage/bucket/migrations/21-transactions-metadata-fill-transaction-id/up.sql similarity index 87% rename from internal/storage/bucket/migrations/20-transactions-metadata-fill-transaction-id/up.sql rename to internal/storage/bucket/migrations/21-transactions-metadata-fill-transaction-id/up.sql index 7823fa915..b0f7d1690 100644 --- a/internal/storage/bucket/migrations/20-transactions-metadata-fill-transaction-id/up.sql +++ b/internal/storage/bucket/migrations/21-transactions-metadata-fill-transaction-id/up.sql @@ -1,7 +1,7 @@ do $$ declare - _batch_size integer := 100; + _batch_size integer := 1000; _count integer; begin set search_path = '{{.Schema}}'; @@ -38,7 +38,9 @@ do $$ end loop; alter table transactions_metadata - alter column transactions_id set not null ; + add constraint transactions_id_not_null + check (transactions_id is not null) + not valid; end $$; diff --git a/internal/storage/bucket/migrations/20-transactions-metadata-fill-transaction-id/up_tests_after.sql b/internal/storage/bucket/migrations/21-transactions-metadata-fill-transaction-id/up_tests_after.sql similarity index 100% rename from internal/storage/bucket/migrations/20-transactions-metadata-fill-transaction-id/up_tests_after.sql rename to internal/storage/bucket/migrations/21-transactions-metadata-fill-transaction-id/up_tests_after.sql diff --git a/internal/storage/bucket/migrations/20-transactions-metadata-fill-transaction-id/up_tests_before.sql b/internal/storage/bucket/migrations/21-transactions-metadata-fill-transaction-id/up_tests_before.sql similarity index 100% rename from internal/storage/bucket/migrations/20-transactions-metadata-fill-transaction-id/up_tests_before.sql rename to internal/storage/bucket/migrations/21-transactions-metadata-fill-transaction-id/up_tests_before.sql diff --git a/internal/storage/bucket/migrations/21-accounts-metadata-fill-address/notes.yaml b/internal/storage/bucket/migrations/22-accounts-metadata-fill-address/notes.yaml similarity index 100% rename from internal/storage/bucket/migrations/21-accounts-metadata-fill-address/notes.yaml rename to internal/storage/bucket/migrations/22-accounts-metadata-fill-address/notes.yaml diff --git a/internal/storage/bucket/migrations/21-accounts-metadata-fill-address/up.sql b/internal/storage/bucket/migrations/22-accounts-metadata-fill-address/up.sql similarity index 86% rename from internal/storage/bucket/migrations/21-accounts-metadata-fill-address/up.sql rename to internal/storage/bucket/migrations/22-accounts-metadata-fill-address/up.sql index 752ef3cfd..10d2a6849 100644 --- a/internal/storage/bucket/migrations/21-accounts-metadata-fill-address/up.sql +++ b/internal/storage/bucket/migrations/22-accounts-metadata-fill-address/up.sql @@ -1,7 +1,7 @@ do $$ declare - _batch_size integer := 100; + _batch_size integer := 1000; _count integer; begin set search_path = '{{.Schema}}'; @@ -38,7 +38,9 @@ do $$ end loop; alter table accounts_metadata - alter column accounts_address set not null ; + add constraint accounts_address_not_null + check (accounts_address is not null) + not valid; end $$; diff --git a/internal/storage/bucket/migrations/21-accounts-metadata-fill-address/up_tests_after.sql b/internal/storage/bucket/migrations/22-accounts-metadata-fill-address/up_tests_after.sql similarity index 100% rename from internal/storage/bucket/migrations/21-accounts-metadata-fill-address/up_tests_after.sql rename to internal/storage/bucket/migrations/22-accounts-metadata-fill-address/up_tests_after.sql diff --git a/internal/storage/bucket/migrations/21-accounts-metadata-fill-address/up_tests_before.sql b/internal/storage/bucket/migrations/22-accounts-metadata-fill-address/up_tests_before.sql similarity index 100% rename from internal/storage/bucket/migrations/21-accounts-metadata-fill-address/up_tests_before.sql rename to internal/storage/bucket/migrations/22-accounts-metadata-fill-address/up_tests_before.sql diff --git a/internal/storage/bucket/migrations/22-logs-fill-memento/notes.yaml b/internal/storage/bucket/migrations/23-logs-fill-memento/notes.yaml similarity index 100% rename from internal/storage/bucket/migrations/22-logs-fill-memento/notes.yaml rename to internal/storage/bucket/migrations/23-logs-fill-memento/notes.yaml diff --git a/internal/storage/bucket/migrations/22-logs-fill-memento/up.sql b/internal/storage/bucket/migrations/23-logs-fill-memento/up.sql similarity index 85% rename from internal/storage/bucket/migrations/22-logs-fill-memento/up.sql rename to internal/storage/bucket/migrations/23-logs-fill-memento/up.sql index 7923084b3..3169d6872 100644 --- a/internal/storage/bucket/migrations/22-logs-fill-memento/up.sql +++ b/internal/storage/bucket/migrations/23-logs-fill-memento/up.sql @@ -1,6 +1,6 @@ do $$ declare - _batch_size integer := 100; + _batch_size integer := 1000; _count integer; begin set search_path = '{{.Schema}}'; @@ -32,7 +32,9 @@ do $$ end loop; alter table logs - alter column memento set not null; + add constraint memento_not_null + check (memento is not null) + not valid; end $$; diff --git a/internal/storage/bucket/migrations/22-logs-fill-memento/up_tests_after.sql b/internal/storage/bucket/migrations/23-logs-fill-memento/up_tests_after.sql similarity index 100% rename from internal/storage/bucket/migrations/22-logs-fill-memento/up_tests_after.sql rename to internal/storage/bucket/migrations/23-logs-fill-memento/up_tests_after.sql diff --git a/internal/storage/bucket/migrations/22-logs-fill-memento/up_tests_before.sql b/internal/storage/bucket/migrations/23-logs-fill-memento/up_tests_before.sql similarity index 100% rename from internal/storage/bucket/migrations/22-logs-fill-memento/up_tests_before.sql rename to internal/storage/bucket/migrations/23-logs-fill-memento/up_tests_before.sql diff --git a/internal/storage/bucket/migrations/23-noop-keep-for-compatibility/notes.yaml b/internal/storage/bucket/migrations/23-noop-keep-for-compatibility/notes.yaml deleted file mode 100644 index ab4b7e1a2..000000000 --- a/internal/storage/bucket/migrations/23-noop-keep-for-compatibility/notes.yaml +++ /dev/null @@ -1 +0,0 @@ -name: Noop, keep for compatibility diff --git a/internal/storage/bucket/migrations/23-noop-keep-for-compatibility/up.sql b/internal/storage/bucket/migrations/23-noop-keep-for-compatibility/up.sql deleted file mode 100644 index e69de29bb..000000000 diff --git a/internal/storage/driver/buckets_generated_test.go b/internal/storage/driver/buckets_generated_test.go index 89d168b38..b71780813 100644 --- a/internal/storage/driver/buckets_generated_test.go +++ b/internal/storage/driver/buckets_generated_test.go @@ -113,9 +113,9 @@ func (mr *MockBucketMockRecorder) IsUpToDate(ctx any) *gomock.Call { } // Migrate mocks base method. -func (m *MockBucket) Migrate(ctx context.Context, minimalVersionReached chan struct{}, opts ...migrations.Option) error { +func (m *MockBucket) Migrate(ctx context.Context, opts ...migrations.Option) error { m.ctrl.T.Helper() - varargs := []any{ctx, minimalVersionReached} + varargs := []any{ctx} for _, a := range opts { varargs = append(varargs, a) } @@ -125,9 +125,9 @@ func (m *MockBucket) Migrate(ctx context.Context, minimalVersionReached chan str } // Migrate indicates an expected call of Migrate. -func (mr *MockBucketMockRecorder) Migrate(ctx, minimalVersionReached any, opts ...any) *gomock.Call { +func (mr *MockBucketMockRecorder) Migrate(ctx any, opts ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx, minimalVersionReached}, opts...) + varargs := append([]any{ctx}, opts...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Migrate", reflect.TypeOf((*MockBucket)(nil).Migrate), varargs...) } diff --git a/internal/storage/driver/driver.go b/internal/storage/driver/driver.go index 7c8b2b337..497bb406e 100644 --- a/internal/storage/driver/driver.go +++ b/internal/storage/driver/driver.go @@ -61,7 +61,6 @@ func (d *Driver) CreateLedger(ctx context.Context, l *ledger.Ledger) (*ledgersto if err := b.Migrate( ctx, - make(chan struct{}), migrations.WithLockRetryInterval(d.migratorLockRetryInterval), ); err != nil { return nil, fmt.Errorf("migrating bucket: %w", err) @@ -82,6 +81,7 @@ func (d *Driver) CreateLedger(ctx context.Context, l *ledger.Ledger) (*ledgersto } func (d *Driver) OpenLedger(ctx context.Context, name string) (*ledgerstore.Store, *ledger.Ledger, error) { + // todo: keep the ledger in cache somewhere to avoid read the ledger at each request, maybe in the factory ret, err := d.systemStore.GetLedger(ctx, name) if err != nil { return nil, nil, err @@ -159,20 +159,17 @@ func (d *Driver) GetLedger(ctx context.Context, name string) (*ledger.Ledger, er func (d *Driver) UpgradeBucket(ctx context.Context, name string) error { return d.bucketFactory.Create(name).Migrate( ctx, - make(chan struct{}), migrations.WithLockRetryInterval(d.migratorLockRetryInterval), ) } -func (d *Driver) UpgradeAllBuckets(ctx context.Context, minimalVersionReached chan struct{}) error { +func (d *Driver) UpgradeAllBuckets(ctx context.Context) error { buckets, err := d.systemStore.GetDistinctBuckets(ctx) if err != nil { return fmt.Errorf("getting distinct buckets: %w", err) } - sem := make(chan struct{}, len(buckets)) - wp := pond.New(d.parallelBucketMigrations, len(buckets), pond.Context(ctx)) for _, bucketName := range buckets { @@ -182,18 +179,13 @@ func (d *Driver) UpgradeAllBuckets(ctx context.Context, minimalVersionReached ch }) b := d.bucketFactory.Create(bucketName) - // copy semaphore to be able to nil it - sem := sem - l: for { - minimalVersionReached := make(chan struct{}) errChan := make(chan error, 1) go func() { logger.Infof("Upgrading...") errChan <- b.Migrate( logging.ContextWithLogger(ctx, logger), - minimalVersionReached, migrations.WithLockRetryInterval(d.migratorLockRetryInterval), ) }() @@ -213,40 +205,45 @@ func (d *Driver) UpgradeAllBuckets(ctx context.Context, minimalVersionReached ch return } } - if sem != nil { - logger.Infof("Reached minimal workable version") - sem <- struct{}{} - } logger.Info("Upgrade terminated") return - case <-minimalVersionReached: - minimalVersionReached = nil - if sem != nil { - logger.Infof("Reached minimal workable version") - sem <- struct{}{} - sem = nil - } } } } }) } - for i := 0; i < len(buckets); i++ { - select { - case <-ctx.Done(): - return ctx.Err() - case <-sem: - } + wp.StopAndWait() + + return nil +} + +func (d *Driver) HasReachMinimalVersion(ctx context.Context) (bool, error) { + isUpToDate, err := d.systemStore.IsUpToDate(ctx) + if err != nil { + return false, fmt.Errorf("checking if system store is up to date: %w", err) + } + if !isUpToDate { + return false, nil } - logging.FromContext(ctx).Infof("All buckets have reached minimal workable version") - close(minimalVersionReached) + buckets, err := d.systemStore.GetDistinctBuckets(ctx) + if err != nil { + return false, fmt.Errorf("getting distinct buckets: %w", err) + } - wp.StopAndWait() + for _, b := range buckets { + hasMinimalVersion, err := d.bucketFactory.Create(b).HasMinimalVersion(ctx) + if err != nil { + return false, fmt.Errorf("checking if bucket '%s' is up to date: %w", b, err) + } + if !hasMinimalVersion { + return false, nil + } + } - return nil + return true, nil } func New( diff --git a/internal/storage/driver/driver_test.go b/internal/storage/driver/driver_test.go index 615cb475e..ea18bcafd 100644 --- a/internal/storage/driver/driver_test.go +++ b/internal/storage/driver/driver_test.go @@ -51,16 +51,15 @@ func TestUpgradeAllLedgers(t *testing.T) { Return(bucket) bucket.EXPECT(). - Migrate(gomock.Any(), gomock.Any(), gomock.Any()). - DoAndReturn(func(ctx context.Context, minimalVersionReached chan struct{}, opts ...migrations.Option) error { - close(minimalVersionReached) + Migrate(gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, opts ...migrations.Option) error { return nil }) ctx, cancel := context.WithTimeout(ctx, 2*time.Second) t.Cleanup(cancel) - require.NoError(t, d.UpgradeAllBuckets(ctx, make(chan struct{}))) + require.NoError(t, d.UpgradeAllBuckets(ctx)) }) t.Run("with concurrent buckets", func(t *testing.T) { @@ -92,9 +91,8 @@ func TestUpgradeAllLedgers(t *testing.T) { Return(bucket) bucket.EXPECT(). - Migrate(gomock.Any(), gomock.Any(), gomock.Any()). - DoAndReturn(func(ctx context.Context, minimalVersionReached chan struct{}, opts ...migrations.Option) error { - close(minimalVersionReached) + Migrate(gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, opts ...migrations.Option) error { return nil }) } @@ -106,7 +104,7 @@ func TestUpgradeAllLedgers(t *testing.T) { ctx, cancel := context.WithTimeout(ctx, 2*time.Second) t.Cleanup(cancel) - require.NoError(t, d.UpgradeAllBuckets(ctx, make(chan struct{}))) + require.NoError(t, d.UpgradeAllBuckets(ctx)) }) t.Run("and error", func(t *testing.T) { @@ -124,7 +122,6 @@ func TestUpgradeAllLedgers(t *testing.T) { bucket1 := driver.NewMockBucket(ctrl) bucket2 := driver.NewMockBucket(ctrl) bucketList := []string{"bucket1", "bucket2"} - allBucketsMinimalVersionReached := make(chan struct{}) bucketFactory.EXPECT(). Create(gomock.AnyOf( @@ -141,10 +138,9 @@ func TestUpgradeAllLedgers(t *testing.T) { bucket1MigrationStarted := make(chan struct{}) bucket1.EXPECT(). - Migrate(gomock.Any(), gomock.Any(), gomock.Any()). + Migrate(gomock.Any(), gomock.Any()). AnyTimes(). - DoAndReturn(func(ctx context.Context, minimalVersionReached chan struct{}, opts ...migrations.Option) error { - close(minimalVersionReached) + DoAndReturn(func(ctx context.Context, opts ...migrations.Option) error { close(bucket1MigrationStarted) return nil @@ -152,9 +148,9 @@ func TestUpgradeAllLedgers(t *testing.T) { firstCall := true bucket2.EXPECT(). - Migrate(gomock.Any(), gomock.Any(), gomock.Any()). + Migrate(gomock.Any(), gomock.Any()). AnyTimes(). - DoAndReturn(func(ctx context.Context, minimalVersionReached chan struct{}, opts ...migrations.Option) error { + DoAndReturn(func(ctx context.Context, opts ...migrations.Option) error { select { case <-ctx.Done(): return ctx.Err() @@ -163,7 +159,6 @@ func TestUpgradeAllLedgers(t *testing.T) { firstCall = false return errors.New("unknown error") } - close(minimalVersionReached) return nil } }) @@ -177,7 +172,7 @@ func TestUpgradeAllLedgers(t *testing.T) { t.Cleanup(cancel) bucket1MigrationStarted = make(chan struct{}) - err := d.UpgradeAllBuckets(ctx, allBucketsMinimalVersionReached) + err := d.UpgradeAllBuckets(ctx) require.NoError(t, err) }) }) @@ -214,7 +209,7 @@ func TestLedgersCreate(t *testing.T) { Return(false, nil) bucket.EXPECT(). - Migrate(gomock.Any(), gomock.Any(), gomock.Any()). + Migrate(gomock.Any(), gomock.Any()). Return(nil) bucket.EXPECT(). diff --git a/internal/storage/driver/system_generated_test.go b/internal/storage/driver/system_generated_test.go index e646d8a75..d6afce573 100644 --- a/internal/storage/driver/system_generated_test.go +++ b/internal/storage/driver/system_generated_test.go @@ -116,6 +116,21 @@ func (mr *SystemStoreMockRecorder) GetMigrator(options ...any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMigrator", reflect.TypeOf((*SystemStore)(nil).GetMigrator), options...) } +// IsUpToDate mocks base method. +func (m *SystemStore) IsUpToDate(ctx context.Context) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsUpToDate", ctx) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsUpToDate indicates an expected call of IsUpToDate. +func (mr *SystemStoreMockRecorder) IsUpToDate(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsUpToDate", reflect.TypeOf((*SystemStore)(nil).IsUpToDate), ctx) +} + // ListLedgers mocks base method. func (m *SystemStore) ListLedgers(ctx context.Context, q ledger0.ListLedgersQuery) (*bunpaginate.Cursor[ledger.Ledger], error) { m.ctrl.T.Helper() diff --git a/internal/storage/ledger/legacy/main_test.go b/internal/storage/ledger/legacy/main_test.go index 364414da3..392ea3750 100644 --- a/internal/storage/ledger/legacy/main_test.go +++ b/internal/storage/ledger/legacy/main_test.go @@ -69,7 +69,7 @@ func newLedgerStore(t T) *testStore { l := ledger.MustNewWithDefault(ledgerName) b := bucket.NewDefault(db, noop.Tracer{}, ledger.DefaultBucket) - require.NoError(t, b.Migrate(ctx, make(chan struct{}))) + require.NoError(t, b.Migrate(ctx)) require.NoError(t, b.AddLedger(ctx, l)) return &testStore{ diff --git a/internal/storage/module.go b/internal/storage/module.go index 29597c847..fd1fe1a28 100644 --- a/internal/storage/module.go +++ b/internal/storage/module.go @@ -2,23 +2,43 @@ package storage import ( "context" + "errors" + "github.com/formancehq/go-libs/v2/health" "github.com/formancehq/go-libs/v2/logging" "github.com/formancehq/ledger/internal/storage/driver" "go.uber.org/fx" ) +const HealthCheckName = `storage-driver-up-to-date` + func NewFXModule(autoUpgrade bool) fx.Option { ret := []fx.Option{ driver.NewFXModule(), + health.ProvideHealthCheck(func(driver *driver.Driver) health.NamedCheck { + hasReachedMinimalVersion := false + return health.NewNamedCheck(HealthCheckName, health.CheckFn(func(ctx context.Context) error { + if hasReachedMinimalVersion { + return nil + } + var err error + hasReachedMinimalVersion, err = driver.HasReachMinimalVersion(ctx) + if err != nil { + return err + } + if !hasReachedMinimalVersion { + return errors.New("storage driver is not up to date") + } + return nil + })) + }), } if autoUpgrade { ret = append(ret, fx.Invoke(func(lc fx.Lifecycle, driver *driver.Driver) { var ( - upgradeContext context.Context - cancelContext func() - upgradeStopped = make(chan struct{}) - minimalVersionReached = make(chan struct{}) + upgradeContext context.Context + cancelContext func() + upgradeStopped = make(chan struct{}) ) lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { @@ -26,17 +46,12 @@ func NewFXModule(autoUpgrade bool) fx.Option { go func() { defer close(upgradeStopped) - if err := driver.UpgradeAllBuckets(upgradeContext, minimalVersionReached); err != nil { + if err := driver.UpgradeAllBuckets(upgradeContext); err != nil { logging.FromContext(ctx).Errorf("failed to upgrade all buckets: %v", err) } }() - select { - case <-ctx.Done(): - return ctx.Err() - case <-minimalVersionReached: - return nil - } + return nil }, OnStop: func(ctx context.Context) error { cancelContext() @@ -52,4 +67,4 @@ func NewFXModule(autoUpgrade bool) fx.Option { ) } return fx.Options(ret...) -} \ No newline at end of file +} diff --git a/internal/storage/system/store.go b/internal/storage/system/store.go index 1e32bad2c..4ebec7f79 100644 --- a/internal/storage/system/store.go +++ b/internal/storage/system/store.go @@ -24,6 +24,7 @@ type Store interface { Migrate(ctx context.Context, options ...migrations.Option) error GetMigrator(options ...migrations.Option) *migrations.Migrator + IsUpToDate(ctx context.Context) (bool, error) } const ( @@ -34,6 +35,10 @@ type DefaultStore struct { db *bun.DB } +func (d *DefaultStore) IsUpToDate(ctx context.Context) (bool, error) { + return d.GetMigrator().IsUpToDate(ctx) +} + func (d *DefaultStore) GetDistinctBuckets(ctx context.Context) ([]string, error) { var buckets []string err := d.db.NewSelect(). diff --git a/openapi.yaml b/openapi.yaml index 7e769bd79..f91ba4288 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -3492,7 +3492,7 @@ components: - INTERPRETER_PARSE - INTERPRETER_RUNTIME - LEDGER_ALREADY_EXISTS - - BUCKET_OUTDATED + - OUTDATED_SCHEMA example: VALIDATION V2LedgerInfoResponse: type: object @@ -3633,11 +3633,13 @@ components: - targetType - key V2BulkResponse: - properties: - data: - type: array - items: - $ref: '#/components/schemas/V2BulkElementResult' + allOf: + - properties: + data: + type: array + items: + $ref: '#/components/schemas/V2BulkElementResult' + - $ref: '#/components/schemas/V2ErrorResponse' type: object required: - data diff --git a/openapi/v2.yaml b/openapi/v2.yaml index 0c8c82d8d..4d879e322 100644 --- a/openapi/v2.yaml +++ b/openapi/v2.yaml @@ -1770,7 +1770,7 @@ components: - INTERPRETER_PARSE - INTERPRETER_RUNTIME - LEDGER_ALREADY_EXISTS - - BUCKET_OUTDATED + - OUTDATED_SCHEMA example: VALIDATION V2LedgerInfoResponse: type: object @@ -1911,11 +1911,13 @@ components: - targetType - key V2BulkResponse: - properties: - data: - type: array - items: - $ref: "#/components/schemas/V2BulkElementResult" + allOf: + - properties: + data: + type: array + items: + $ref: "#/components/schemas/V2BulkElementResult" + - $ref: "#/components/schemas/V2ErrorResponse" type: object required: - data diff --git a/pkg/client/.speakeasy/gen.yaml b/pkg/client/.speakeasy/gen.yaml index bfd90d79b..584f2ddbd 100644 --- a/pkg/client/.speakeasy/gen.yaml +++ b/pkg/client/.speakeasy/gen.yaml @@ -15,7 +15,7 @@ generation: auth: oAuth2ClientCredentialsEnabled: true go: - version: 0.5.0 + version: 0.5.1 additionalDependencies: {} allowUnknownFieldsInWeakUnions: false clientServerStatusCodesAsErrors: true diff --git a/pkg/client/docs/models/components/v2bulkresponse.md b/pkg/client/docs/models/components/v2bulkresponse.md index 64c495660..3d9269f2c 100644 --- a/pkg/client/docs/models/components/v2bulkresponse.md +++ b/pkg/client/docs/models/components/v2bulkresponse.md @@ -3,6 +3,9 @@ ## Fields -| Field | Type | Required | Description | -| ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -| `Data` | [][components.V2BulkElementResult](../../models/components/v2bulkelementresult.md) | :heavy_check_mark: | N/A | \ No newline at end of file +| Field | Type | Required | Description | Example | +| -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | +| `Data` | [][components.V2BulkElementResult](../../models/components/v2bulkelementresult.md) | :heavy_check_mark: | N/A | | +| `ErrorCode` | [components.V2ErrorsEnum](../../models/components/v2errorsenum.md) | :heavy_check_mark: | N/A | VALIDATION | +| `ErrorMessage` | *string* | :heavy_check_mark: | N/A | [VALIDATION] invalid 'cursor' query param | +| `Details` | **string* | :heavy_minus_sign: | N/A | https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9 | \ No newline at end of file diff --git a/pkg/client/formance.go b/pkg/client/formance.go index 65656349a..869df9420 100644 --- a/pkg/client/formance.go +++ b/pkg/client/formance.go @@ -143,9 +143,9 @@ func New(opts ...SDKOption) *Formance { sdkConfiguration: sdkConfiguration{ Language: "go", OpenAPIDocVersion: "v1", - SDKVersion: "0.5.0", + SDKVersion: "0.5.1", GenVersion: "2.384.1", - UserAgent: "speakeasy-sdk/go 0.5.0 2.384.1 v1 github.com/formancehq/ledger/pkg/client", + UserAgent: "speakeasy-sdk/go 0.5.1 2.384.1 v1 github.com/formancehq/ledger/pkg/client", Hooks: hooks.New(), }, } diff --git a/pkg/client/models/components/v2bulkresponse.go b/pkg/client/models/components/v2bulkresponse.go index 06eace58f..940f8cf49 100644 --- a/pkg/client/models/components/v2bulkresponse.go +++ b/pkg/client/models/components/v2bulkresponse.go @@ -3,7 +3,10 @@ package components type V2BulkResponse struct { - Data []V2BulkElementResult `json:"data"` + Data []V2BulkElementResult `json:"data"` + ErrorCode V2ErrorsEnum `json:"errorCode"` + ErrorMessage string `json:"errorMessage"` + Details *string `json:"details,omitempty"` } func (o *V2BulkResponse) GetData() []V2BulkElementResult { @@ -12,3 +15,24 @@ func (o *V2BulkResponse) GetData() []V2BulkElementResult { } return o.Data } + +func (o *V2BulkResponse) GetErrorCode() V2ErrorsEnum { + if o == nil { + return V2ErrorsEnum("") + } + return o.ErrorCode +} + +func (o *V2BulkResponse) GetErrorMessage() string { + if o == nil { + return "" + } + return o.ErrorMessage +} + +func (o *V2BulkResponse) GetDetails() *string { + if o == nil { + return nil + } + return o.Details +} diff --git a/pkg/generate/generator.go b/pkg/generate/generator.go index 11c2c2f43..395e049ea 100644 --- a/pkg/generate/generator.go +++ b/pkg/generate/generator.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/dop251/goja" "github.com/formancehq/go-libs/v2/collectionutils" + "github.com/formancehq/go-libs/v2/pointer" ledger "github.com/formancehq/ledger/internal" "github.com/formancehq/ledger/internal/api/bulking" "github.com/formancehq/ledger/pkg/client" @@ -14,6 +15,7 @@ import ( "github.com/formancehq/ledger/pkg/client/models/operations" "github.com/google/uuid" "math/big" + "net/http" "os" "path/filepath" "time" @@ -141,11 +143,30 @@ func (r Action) Apply(ctx context.Context, client *client.V2, l string) ([]compo response, err := client.CreateBulk(ctx, operations.V2CreateBulkRequest{ Ledger: l, RequestBody: bulkElements, + Atomic: pointer.For(true), }) if err != nil { return nil, fmt.Errorf("creating transaction: %w", err) } + if response.HTTPMeta.Response.StatusCode == http.StatusBadRequest { + return nil, fmt.Errorf( + "unexpected error: %s [%s]", + response.V2BulkResponse.ErrorMessage, + response.V2BulkResponse.ErrorCode, + ) + } + + for _, data := range response.V2BulkResponse.Data { + if data.Type == components.V2BulkElementResultTypeError { + return nil, fmt.Errorf( + "unexpected error: %s [%s]", + data.V2BulkElementResultError.ErrorDescription, + data.V2BulkElementResultError.ErrorCode, + ) + } + } + return response.V2BulkResponse.Data, nil } diff --git a/pkg/testserver/server.go b/pkg/testserver/server.go index c479887a9..d6dae85ab 100644 --- a/pkg/testserver/server.go +++ b/pkg/testserver/server.go @@ -57,11 +57,13 @@ type Logger interface { type Server struct { configuration Configuration logger Logger - httpClient *ledgerclient.Formance + sdkClient *ledgerclient.Formance cancel func() ctx context.Context errorChan chan error id string + httpClient *http.Client + serverURL string } func (s *Server) Start() error { @@ -221,11 +223,14 @@ func (s *Server) Start() error { transport = httpclient.NewDebugHTTPTransport(transport) } - s.httpClient = ledgerclient.New( - ledgerclient.WithServerURL(httpserver.URL(s.ctx)), - ledgerclient.WithClient(&http.Client{ - Transport: transport, - }), + s.httpClient = &http.Client{ + Transport: transport, + } + s.serverURL = httpserver.URL(s.ctx) + + s.sdkClient = ledgerclient.New( + ledgerclient.WithServerURL(s.serverURL), + ledgerclient.WithClient(s.httpClient), ) return nil @@ -255,9 +260,17 @@ func (s *Server) Stop(ctx context.Context) error { } func (s *Server) Client() *ledgerclient.Formance { + return s.sdkClient +} + +func (s *Server) HTTPClient() *http.Client { return s.httpClient } +func (s *Server) ServerURL() string { + return s.serverURL +} + func (s *Server) Restart(ctx context.Context) error { if err := s.Stop(ctx); err != nil { return err diff --git a/test/e2e/app_lifecycle_test.go b/test/e2e/app_lifecycle_test.go index 2fdfcda81..fda433f31 100644 --- a/test/e2e/app_lifecycle_test.go +++ b/test/e2e/app_lifecycle_test.go @@ -5,12 +5,14 @@ package test_suite import ( "context" "database/sql" + "encoding/json" "github.com/formancehq/go-libs/v2/bun/bunconnect" "github.com/formancehq/go-libs/v2/logging" "github.com/formancehq/go-libs/v2/pointer" "github.com/formancehq/go-libs/v2/testing/platform/pgtesting" "github.com/formancehq/go-libs/v2/time" ledger "github.com/formancehq/ledger/internal" + "github.com/formancehq/ledger/internal/storage" "github.com/formancehq/ledger/internal/storage/bucket" "github.com/formancehq/ledger/internal/storage/system" "github.com/formancehq/ledger/pkg/client/models/components" @@ -275,4 +277,15 @@ var _ = Context("Ledger downgrade tests", func() { }) }) }) + + It("should be ok when targeting health check endpoint", func() { + ret, err := testServer.GetValue().HTTPClient().Get(testServer.GetValue().ServerURL() + "/_healthcheck") + Expect(err).To(BeNil()) + + body := make(map[string]interface{}) + Expect(json.NewDecoder(ret.Body).Decode(&body)).To(BeNil()) + Expect(body).To(Equal(map[string]any{ + storage.HealthCheckName: "OK", + })) + }) }) diff --git a/test/migrations/upgrade_test.go b/test/migrations/upgrade_test.go index d43dbf7e0..3884c9deb 100644 --- a/test/migrations/upgrade_test.go +++ b/test/migrations/upgrade_test.go @@ -73,7 +73,7 @@ func TestMigrations(t *testing.T) { driver.WithParallelBucketMigration(1), ) require.NoError(t, driver.Initialize(ctx)) - require.NoError(t, driver.UpgradeAllBuckets(ctx, make(chan struct{}))) + require.NoError(t, driver.UpgradeAllBuckets(ctx)) } func copyDatabase(t *testing.T, dockerPool *docker.Pool, source, destination string) { diff --git a/tools/generator/cmd/root.go b/tools/generator/cmd/root.go index b031f17f4..61dcfd1ea 100644 --- a/tools/generator/cmd/root.go +++ b/tools/generator/cmd/root.go @@ -19,14 +19,7 @@ import ( "strings" ) -var ( - rootCmd = &cobra.Command{ - Use: "generator ", - Short: "Generate data for a ledger. WARNING: This is an experimental tool.", - RunE: run, - Args: cobra.ExactArgs(2), - SilenceUsage: true, - } +const ( parallelFlag = "parallel" ledgerFlag = "ledger" ledgerMetadataFlag = "ledger-metadata" @@ -37,6 +30,18 @@ var ( clientSecretFlag = "client-secret" authUrlFlag = "auth-url" insecureSkipVerifyFlag = "insecure-skip-verify" + httpClientTimeoutFlag = "http-client-timeout" + debugFlag = "debug" +) + +var ( + rootCmd = &cobra.Command{ + Use: "generator ", + Short: "Generate data for a ledger. WARNING: This is an experimental tool.", + RunE: run, + Args: cobra.ExactArgs(2), + SilenceUsage: true, + } ) func init() { @@ -50,6 +55,8 @@ func init() { rootCmd.Flags().String(ledgerBucketFlag, "", "Ledger bucket") rootCmd.Flags().StringSlice(ledgerMetadataFlag, []string{}, "Ledger metadata") rootCmd.Flags().StringSlice(ledgerFeatureFlag, []string{}, "Ledger features") + rootCmd.Flags().Duration(httpClientTimeoutFlag, 0, "HTTP client timeout (default: no timeout)") + rootCmd.Flags().Bool(debugFlag, false, "Enable debug logging") rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } @@ -106,7 +113,21 @@ func run(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get insecureSkipVerify: %w", err) } + httpClientTimeout, err := cmd.Flags().GetDuration(httpClientTimeoutFlag) + if err != nil { + return fmt.Errorf("failed to get http client timeout: %w", err) + } + + debug, err := cmd.Flags().GetBool(debugFlag) + if err != nil { + return fmt.Errorf("failed to get debug: %w", err) + } + + logger := logging.NewDefaultLogger(cmd.OutOrStdout(), debug, false, false) + ctx := logging.ContextWithLogger(cmd.Context(), logger) + httpClient := &http.Client{ + Timeout: httpClientTimeout, Transport: &http.Transport{ MaxIdleConns: vus, MaxConnsPerHost: vus, @@ -138,7 +159,7 @@ func run(cmd *cobra.Command, args []string) error { TokenURL: authUrl + "/oauth/token", Scopes: []string{"ledger:read", "ledger:write"}, }). - Client(context.WithValue(cmd.Context(), oauth2.HTTPClient, httpClient)) + Client(context.WithValue(ctx, oauth2.HTTPClient, httpClient)) } client := ledgerclient.New( @@ -146,8 +167,8 @@ func run(cmd *cobra.Command, args []string) error { ledgerclient.WithClient(httpClient), ) - logging.FromContext(cmd.Context()).Infof("Creating ledger '%s' if not exists", targetedLedger) - _, err = client.Ledger.V2.GetLedger(cmd.Context(), operations.V2GetLedgerRequest{ + logging.FromContext(ctx).Infof("Creating ledger '%s' if not exists", targetedLedger) + _, err = client.Ledger.V2.GetLedger(ctx, operations.V2GetLedgerRequest{ Ledger: targetedLedger, }) if err != nil { @@ -155,7 +176,7 @@ func run(cmd *cobra.Command, args []string) error { if !errors.As(err, &sdkError) || sdkError.ErrorCode != components.V2ErrorsEnumNotFound { return fmt.Errorf("failed to get ledger: %w", err) } - _, err = client.Ledger.V2.CreateLedger(cmd.Context(), operations.V2CreateLedgerRequest{ + _, err = client.Ledger.V2.CreateLedger(ctx, operations.V2CreateLedgerRequest{ Ledger: targetedLedger, V2CreateLedgerRequest: &components.V2CreateLedgerRequest{ Bucket: &ledgerBucket, @@ -171,11 +192,11 @@ func run(cmd *cobra.Command, args []string) error { } } - logging.FromContext(cmd.Context()).Infof("Starting to generate data with %d vus", vus) + logger.Infof("Starting to generate data with %d vus", vus) return generate. NewGeneratorSet(vus, string(fileContent), targetedLedger, client, uint64(untilLogID)). - Run(cmd.Context()) + Run(ctx) } func extractSliceSliceFlag(cmd *cobra.Command, flagName string) (map[string]string, error) { From 58145403fdef1540ba249ae7061eef4891bab51c Mon Sep 17 00:00:00 2001 From: Ragot Geoffrey Date: Mon, 9 Dec 2024 12:29:02 +0100 Subject: [PATCH 55/71] fix: otel configuration (#607) --- deployments/pulumi/pkg/component.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/deployments/pulumi/pkg/component.go b/deployments/pulumi/pkg/component.go index e8e7b7acc..2705c001f 100644 --- a/deployments/pulumi/pkg/component.go +++ b/deployments/pulumi/pkg/component.go @@ -190,18 +190,18 @@ func NewComponent(ctx *pulumi.Context, name string, args *ComponentArgs, opts .. if otel.ResourceAttributes != nil { envVars = append(envVars, corev1.EnvVarArgs{ Name: pulumi.String("OTEL_RESOURCE_ATTRIBUTES"), - Value: pulumi.All(otel.ResourceAttributes).ApplyT(func(v []map[string]string) string { + Value: pulumix.Apply(otel.ResourceAttributes, func(rawResourceAttributes map[string]string) string { ret := "" - keys := collectionutils.Keys(v[0]) + keys := collectionutils.Keys(rawResourceAttributes) slices.Sort(keys) for _, key := range keys { - ret += key + "=" + v[0][key] + "," + ret += key + "=" + rawResourceAttributes[key] + "," } if len(ret) > 0 { ret = ret[:len(ret)-1] } return ret - }).(pulumi.StringOutput), + }).Untyped().(pulumi.StringOutput), }) } if traces := args.Otel.Traces; traces != nil { From 471df930a94f036e8036702d1c8cde9f2499d5ae Mon Sep 17 00:00:00 2001 From: Ragot Geoffrey Date: Mon, 9 Dec 2024 20:39:24 +0100 Subject: [PATCH 56/71] feat: optimize volumes endpoint when not using pit (#608) --- internal/storage/ledger/volumes.go | 65 ++++++++++++++++++------------ 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/internal/storage/ledger/volumes.go b/internal/storage/ledger/volumes.go index c0a8644cf..8b7c26722 100644 --- a/internal/storage/ledger/volumes.go +++ b/internal/storage/ledger/volumes.go @@ -5,9 +5,8 @@ import ( "fmt" "github.com/formancehq/go-libs/v2/collectionutils" "github.com/formancehq/go-libs/v2/platform/postgres" - "github.com/formancehq/ledger/pkg/features" - "github.com/formancehq/ledger/internal/tracing" + "github.com/formancehq/ledger/pkg/features" "github.com/formancehq/go-libs/v2/bun/bunpaginate" lquery "github.com/formancehq/go-libs/v2/query" @@ -68,10 +67,6 @@ func (s *Store) UpdateVolumes(ctx context.Context, accountVolumes ...ledger.Acco func (s *Store) selectVolumes(oot, pit *time.Time, useInsertionDate bool, groupLevel int, q lquery.Builder) *bun.SelectQuery { ret := s.db.NewSelect() - if !s.ledger.HasFeature(features.FeatureMovesHistory, "ON") { - return ret.Err(ledgercontroller.NewErrMissingFeature(features.FeatureMovesHistory)) - } - var ( useMetadata bool needSegmentAddress bool @@ -107,27 +102,45 @@ func (s *Store) selectVolumes(oot, pit *time.Time, useInsertionDate bool, groupL } } - selectVolumes := s.db.NewSelect(). - ColumnExpr("accounts_address as address"). - Column("asset"). - ColumnExpr("sum(case when not is_source then amount else 0 end) as input"). - ColumnExpr("sum(case when is_source then amount else 0 end) as output"). - ColumnExpr("sum(case when not is_source then amount else -amount end) as balance"). - ModelTableExpr(s.GetPrefixedRelationName("moves")). - Where("ledger = ?", s.ledger.Name). - GroupExpr("accounts_address, asset"). - Order("accounts_address", "asset") - - dateFilterColumn := "effective_date" - if useInsertionDate { - dateFilterColumn = "insertion_date" - } + var selectVolumes *bun.SelectQuery - if pit != nil && !pit.IsZero() { - selectVolumes = selectVolumes.Where(dateFilterColumn+" <= ?", pit) - } - if oot != nil && !oot.IsZero() { - selectVolumes = selectVolumes.Where(dateFilterColumn+" >= ?", oot) + if (pit == nil || pit.IsZero()) && (oot == nil || oot.IsZero()) { + selectVolumes = s.db.NewSelect(). + DistinctOn("accounts_address, asset"). + ColumnExpr("accounts_address as address"). + Column("asset", "input", "output"). + ColumnExpr("input - output as balance"). + ModelTableExpr(s.GetPrefixedRelationName("accounts_volumes")). + Where("ledger = ?", s.ledger.Name). + Order("accounts_address", "asset") + } else { + if !s.ledger.HasFeature(features.FeatureMovesHistory, "ON") { + return ret.Err(ledgercontroller.NewErrMissingFeature(features.FeatureMovesHistory)) + } + + dateFilterColumn := "effective_date" + if useInsertionDate { + dateFilterColumn = "insertion_date" + } + + selectVolumes = s.db.NewSelect(). + ColumnExpr("accounts_address as address"). + Column("asset"). + ColumnExpr("sum(case when not is_source then amount else 0 end) as input"). + ColumnExpr("sum(case when is_source then amount else 0 end) as output"). + ColumnExpr("sum(case when not is_source then amount else -amount end) as balance"). + ModelTableExpr(s.GetPrefixedRelationName("moves")). + Where("ledger = ?", s.ledger.Name). + GroupExpr("accounts_address, asset"). + Order("accounts_address", "asset") + + if pit != nil && !pit.IsZero() { + selectVolumes = selectVolumes.Where(dateFilterColumn+" <= ?", pit) + } + + if oot != nil && !oot.IsZero() { + selectVolumes = selectVolumes.Where(dateFilterColumn+" >= ?", oot) + } } ret = ret. From ed8b08d4ede0ad604ae011746753f8ed7032c2f6 Mon Sep 17 00:00:00 2001 From: Ragot Geoffrey Date: Mon, 9 Dec 2024 21:09:31 +0100 Subject: [PATCH 57/71] fix: error handling at storage level (#609) --- internal/storage/ledger/accounts.go | 84 +++++++++++++++++-------- internal/storage/ledger/balances.go | 46 +++++++++----- internal/storage/ledger/moves.go | 16 +++-- internal/storage/ledger/transactions.go | 50 +++++++++------ internal/storage/ledger/volumes.go | 28 +++++---- 5 files changed, 146 insertions(+), 78 deletions(-) diff --git a/internal/storage/ledger/accounts.go b/internal/storage/ledger/accounts.go index 01d2d844e..6fa8545bd 100644 --- a/internal/storage/ledger/accounts.go +++ b/internal/storage/ledger/accounts.go @@ -42,22 +42,26 @@ func convertOperatorToSQL(operator string) string { panic("unreachable") } -func (s *Store) selectBalance(date *time.Time) *bun.SelectQuery { +func (s *Store) selectBalance(date *time.Time) (*bun.SelectQuery, error) { if date != nil && !date.IsZero() { - sortedMoves := s.SelectDistinctMovesBySeq(date). + selectDistinctMovesBySeq, err := s.SelectDistinctMovesBySeq(date) + if err != nil { + return nil, err + } + sortedMoves := selectDistinctMovesBySeq. ColumnExpr("(post_commit_volumes).inputs - (post_commit_volumes).outputs as balance") return s.db.NewSelect(). ModelTableExpr("(?) moves", sortedMoves). Where("ledger = ?", s.ledger.Name). - ColumnExpr("accounts_address, asset, balance") + ColumnExpr("accounts_address, asset, balance"), nil } return s.db.NewSelect(). ModelTableExpr(s.GetPrefixedRelationName("accounts_volumes")). Where("ledger = ?", s.ledger.Name). - ColumnExpr("input - output as balance") + ColumnExpr("input - output as balance"), nil } func (s *Store) selectDistinctAccountMetadataHistories(date *time.Time) *bun.SelectQuery { @@ -75,7 +79,7 @@ func (s *Store) selectDistinctAccountMetadataHistories(date *time.Time) *bun.Sel return ret } -func (s *Store) selectAccounts(date *time.Time, expandVolumes, expandEffectiveVolumes bool, qb query.Builder) *bun.SelectQuery { +func (s *Store) selectAccounts(date *time.Time, expandVolumes, expandEffectiveVolumes bool, qb query.Builder) (*bun.SelectQuery, error) { ret := s.db.NewSelect() @@ -104,7 +108,7 @@ func (s *Store) selectAccounts(date *time.Time, expandVolumes, expandEffectiveVo return nil }); err != nil { - return ret.Err(fmt.Errorf("failed to check filters: %w", err)) + return nil, fmt.Errorf("failed to check filters: %w", err) } } @@ -131,16 +135,24 @@ func (s *Store) selectAccounts(date *time.Time, expandVolumes, expandEffectiveVo } if s.ledger.HasFeature(features.FeatureMovesHistory, "ON") && needVolumes { + selectAccountWithAggregatedVolumes, err := s.selectAccountWithAggregatedVolumes(date, true, "volumes") + if err != nil { + return nil, err + } ret = ret.Join( `left join (?) volumes on volumes.accounts_address = accounts.address`, - s.selectAccountWithAggregatedVolumes(date, true, "volumes"), + selectAccountWithAggregatedVolumes, ).Column("volumes.*") } if s.ledger.HasFeature(features.FeatureMovesHistoryPostCommitEffectiveVolumes, "SYNC") && expandEffectiveVolumes { + selectAccountWithAggregatedVolumes, err := s.selectAccountWithAggregatedVolumes(date, false, "effective_volumes") + if err != nil { + return nil, err + } ret = ret.Join( `left join (?) effective_volumes on effective_volumes.accounts_address = accounts.address`, - s.selectAccountWithAggregatedVolumes(date, false, "effective_volumes"), + selectAccountWithAggregatedVolumes, ).Column("effective_volumes.*") } @@ -156,20 +168,30 @@ func (s *Store) selectAccounts(date *time.Time, expandVolumes, expandEffectiveVo match := balanceRegex.FindAllStringSubmatch(key, 2) asset := match[0][1] + selectBalance, err := s.selectBalance(date) + if err != nil { + return "", nil, err + } + return s.db.NewSelect(). TableExpr( "(?) balance", - s.selectBalance(date). + selectBalance. Where("asset = ? and accounts_address = accounts.address", asset), ). ColumnExpr(fmt.Sprintf("balance %s ?", convertOperatorToSQL(operator)), value). String(), nil, nil case key == "balance": + selectBalance, err := s.selectBalance(date) + if err != nil { + return "", nil, err + } + return s.db.NewSelect(). TableExpr( "(?) balance", - s.selectBalance(date). + selectBalance. Where("accounts_address = accounts.address"), ). ColumnExpr(fmt.Sprintf("balance %s ?", convertOperatorToSQL(operator)), value). @@ -198,7 +220,7 @@ func (s *Store) selectAccounts(date *time.Time, expandVolumes, expandEffectiveVo panic("unreachable") })) if err != nil { - return ret.Err(fmt.Errorf("evaluating filters: %w", err)) + return nil, fmt.Errorf("evaluating filters: %w", err) } if len(args) > 0 { ret = ret.Where(where, args...) @@ -207,10 +229,19 @@ func (s *Store) selectAccounts(date *time.Time, expandVolumes, expandEffectiveVo } } - return ret + return ret, nil } func (s *Store) ListAccounts(ctx context.Context, q ledgercontroller.ListAccountsQuery) (*Cursor[ledger.Account], error) { + selectAccounts, err := s.selectAccounts( + q.Options.Options.PIT, + q.Options.Options.ExpandVolumes, + q.Options.Options.ExpandEffectiveVolumes, + q.Options.QueryBuilder, + ) + if err != nil { + return nil, err + } return tracing.TraceWithMetric( ctx, "ListAccounts", @@ -219,12 +250,7 @@ func (s *Store) ListAccounts(ctx context.Context, q ledgercontroller.ListAccount func(ctx context.Context) (*Cursor[ledger.Account], error) { ret, err := UsingOffset[ledgercontroller.PaginatedQueryOptions[ledgercontroller.PITFilterWithVolumes], ledger.Account]( ctx, - s.selectAccounts( - q.Options.Options.PIT, - q.Options.Options.ExpandVolumes, - q.Options.Options.ExpandEffectiveVolumes, - q.Options.QueryBuilder, - ), + selectAccounts, OffsetPaginatedQuery[ledgercontroller.PaginatedQueryOptions[ledgercontroller.PITFilterWithVolumes]](q), ) @@ -245,7 +271,11 @@ func (s *Store) GetAccount(ctx context.Context, q ledgercontroller.GetAccountQue s.getAccountHistogram, func(ctx context.Context) (*ledger.Account, error) { ret := &ledger.Account{} - if err := s.selectAccounts(q.PIT, q.ExpandVolumes, q.ExpandEffectiveVolumes, nil). + selectAccounts, err := s.selectAccounts(q.PIT, q.ExpandVolumes, q.ExpandEffectiveVolumes, nil) + if err != nil { + return nil, err + } + if err := selectAccounts. Model(ret). Where("accounts.address = ?", q.Addr). Limit(1). @@ -265,13 +295,17 @@ func (s *Store) CountAccounts(ctx context.Context, q ledgercontroller.ListAccoun s.tracer, s.countAccountsHistogram, func(ctx context.Context) (int, error) { + selectAccounts, err := s.selectAccounts( + q.Options.Options.PIT, + q.Options.Options.ExpandVolumes, + q.Options.Options.ExpandEffectiveVolumes, + q.Options.QueryBuilder, + ) + if err != nil { + return 0, err + } return s.db.NewSelect(). - TableExpr("(?) data", s.selectAccounts( - q.Options.Options.PIT, - q.Options.Options.ExpandVolumes, - q.Options.Options.ExpandEffectiveVolumes, - q.Options.QueryBuilder, - )). + TableExpr("(?) data", selectAccounts). Count(ctx) }, ) diff --git a/internal/storage/ledger/balances.go b/internal/storage/ledger/balances.go index 1ddbf6eee..7bf95e7d8 100644 --- a/internal/storage/ledger/balances.go +++ b/internal/storage/ledger/balances.go @@ -18,9 +18,8 @@ import ( "github.com/uptrace/bun" ) -func (s *Store) selectAccountWithAssetAndVolumes(date *time.Time, useInsertionDate bool, builder query.Builder) *bun.SelectQuery { +func (s *Store) selectAccountWithAssetAndVolumes(date *time.Time, useInsertionDate bool, builder query.Builder) (*bun.SelectQuery, error) { - ret := s.db.NewSelect() var ( needMetadata bool needAddressSegment bool @@ -53,27 +52,31 @@ func (s *Store) selectAccountWithAssetAndVolumes(date *time.Time, useInsertionDa } return nil }); err != nil { - return ret.Err(err) + return nil, err } } if needAddressSegment && !s.ledger.HasFeature(features.FeatureIndexAddressSegments, "ON") { - return ret.Err(ledgercontroller.NewErrMissingFeature(features.FeatureIndexAddressSegments)) + return nil, ledgercontroller.NewErrMissingFeature(features.FeatureIndexAddressSegments) } var selectAccountsWithVolumes *bun.SelectQuery if date != nil && !date.IsZero() { if useInsertionDate { if !s.ledger.HasFeature(features.FeatureMovesHistory, "ON") { - return ret.Err(ledgercontroller.NewErrMissingFeature(features.FeatureMovesHistory)) + return nil, ledgercontroller.NewErrMissingFeature(features.FeatureMovesHistory) + } + selectDistinctMovesBySeq, err := s.SelectDistinctMovesBySeq(date) + if err != nil { + return nil, err } selectAccountsWithVolumes = s.db.NewSelect(). - TableExpr("(?) moves", s.SelectDistinctMovesBySeq(date)). + TableExpr("(?) moves", selectDistinctMovesBySeq). Column("asset", "accounts_address"). ColumnExpr("post_commit_volumes as volumes") } else { if !s.ledger.HasFeature(features.FeatureMovesHistoryPostCommitEffectiveVolumes, "SYNC") { - return ret.Err(ledgercontroller.NewErrMissingFeature(features.FeatureMovesHistoryPostCommitEffectiveVolumes)) + return nil, ledgercontroller.NewErrMissingFeature(features.FeatureMovesHistoryPostCommitEffectiveVolumes) } selectAccountsWithVolumes = s.db.NewSelect(). TableExpr("(?) moves", s.SelectDistinctMovesByEffectiveDate(date)). @@ -143,26 +146,32 @@ func (s *Store) selectAccountWithAssetAndVolumes(date *time.Time, useInsertionDa } })) if err != nil { - return ret.Err(fmt.Errorf("building where clause: %w", err)) + return nil, fmt.Errorf("building where clause: %w", err) } finalQuery = finalQuery.Where(where, args...) } - return finalQuery + return finalQuery, nil } -func (s *Store) selectAccountWithAggregatedVolumes(date *time.Time, useInsertionDate bool, alias string) *bun.SelectQuery { - selectAccountWithAssetAndVolumes := s.selectAccountWithAssetAndVolumes(date, useInsertionDate, nil) +func (s *Store) selectAccountWithAggregatedVolumes(date *time.Time, useInsertionDate bool, alias string) (*bun.SelectQuery, error) { + selectAccountWithAssetAndVolumes, err := s.selectAccountWithAssetAndVolumes(date, useInsertionDate, nil) + if err != nil { + return nil, err + } return s.db.NewSelect(). TableExpr("(?) values", selectAccountWithAssetAndVolumes). Group("accounts_address"). Column("accounts_address"). - ColumnExpr("public.aggregate_objects(json_build_object(asset, json_build_object('input', (volumes).inputs, 'output', (volumes).outputs))::jsonb) as " + alias) + ColumnExpr("public.aggregate_objects(json_build_object(asset, json_build_object('input', (volumes).inputs, 'output', (volumes).outputs))::jsonb) as " + alias), nil } -func (s *Store) SelectAggregatedBalances(date *time.Time, useInsertionDate bool, builder query.Builder) *bun.SelectQuery { +func (s *Store) SelectAggregatedBalances(date *time.Time, useInsertionDate bool, builder query.Builder) (*bun.SelectQuery, error) { - selectAccountsWithVolumes := s.selectAccountWithAssetAndVolumes(date, useInsertionDate, builder) + selectAccountsWithVolumes, err := s.selectAccountWithAssetAndVolumes(date, useInsertionDate, builder) + if err != nil { + return nil, err + } sumVolumesForAsset := s.db.NewSelect(). TableExpr("(?) values", selectAccountsWithVolumes). Group("asset"). @@ -171,7 +180,7 @@ func (s *Store) SelectAggregatedBalances(date *time.Time, useInsertionDate bool, return s.db.NewSelect(). TableExpr("(?) values", sumVolumesForAsset). - ColumnExpr("aggregate_objects(json_build_object(asset, volumes)::jsonb) as aggregated") + ColumnExpr("aggregate_objects(json_build_object(asset, volumes)::jsonb) as aggregated"), nil } func (s *Store) GetAggregatedBalances(ctx context.Context, q ledgercontroller.GetAggregatedBalanceQuery) (ledger.BalancesByAssets, error) { @@ -179,9 +188,14 @@ func (s *Store) GetAggregatedBalances(ctx context.Context, q ledgercontroller.Ge Aggregated ledger.VolumesByAssets `bun:"aggregated,type:jsonb"` } + selectAggregatedBalances, err := s.SelectAggregatedBalances(q.PIT, q.UseInsertionDate, q.QueryBuilder) + if err != nil { + return nil, err + } + aggregatedVolumes := AggregatedVolumes{} if err := s.db.NewSelect(). - ModelTableExpr("(?) aggregated_volumes", s.SelectAggregatedBalances(q.PIT, q.UseInsertionDate, q.QueryBuilder)). + ModelTableExpr("(?) aggregated_volumes", selectAggregatedBalances). Model(&aggregatedVolumes). Scan(ctx); err != nil { return nil, err diff --git a/internal/storage/ledger/moves.go b/internal/storage/ledger/moves.go index 2c32228c2..6f5e55343 100644 --- a/internal/storage/ledger/moves.go +++ b/internal/storage/ledger/moves.go @@ -12,11 +12,11 @@ import ( "github.com/uptrace/bun" ) -func (s *Store) SortMovesBySeq(date *time.Time) *bun.SelectQuery { +func (s *Store) SortMovesBySeq(date *time.Time) (*bun.SelectQuery, error) { ret := s.db.NewSelect() if !s.ledger.HasFeature(features.FeatureMovesHistory, "ON") { - return ret.Err(ledgercontroller.NewErrMissingFeature(features.FeatureMovesHistory)) + return nil, ledgercontroller.NewErrMissingFeature(features.FeatureMovesHistory) } ret = ret. @@ -28,12 +28,16 @@ func (s *Store) SortMovesBySeq(date *time.Time) *bun.SelectQuery { ret = ret.Where("insertion_date <= ?", date) } - return ret + return ret, nil } -func (s *Store) SelectDistinctMovesBySeq(date *time.Time) *bun.SelectQuery { +func (s *Store) SelectDistinctMovesBySeq(date *time.Time) (*bun.SelectQuery, error) { + sortMovesBySeq, err := s.SortMovesBySeq(date) + if err != nil { + return nil, err + } ret := s.db.NewSelect(). - TableExpr("(?) moves", s.SortMovesBySeq(date)). + TableExpr("(?) moves", sortMovesBySeq). DistinctOn("accounts_address, asset"). Column("accounts_address", "asset"). ColumnExpr("first_value(post_commit_volumes) over (partition by (accounts_address, asset) order by seq desc) as post_commit_volumes"). @@ -43,7 +47,7 @@ func (s *Store) SelectDistinctMovesBySeq(date *time.Time) *bun.SelectQuery { ret = ret.Where("insertion_date <= ?", date) } - return ret + return ret, nil } func (s *Store) SelectDistinctMovesByEffectiveDate(date *time.Time) *bun.SelectQuery { diff --git a/internal/storage/ledger/transactions.go b/internal/storage/ledger/transactions.go index e0fb7b612..9de7d93fd 100644 --- a/internal/storage/ledger/transactions.go +++ b/internal/storage/ledger/transactions.go @@ -50,11 +50,11 @@ func (s *Store) selectDistinctTransactionMetadataHistories(date *time.Time) *bun return ret } -func (s *Store) selectTransactions(date *time.Time, expandVolumes, expandEffectiveVolumes bool, q query.Builder) *bun.SelectQuery { +func (s *Store) selectTransactions(date *time.Time, expandVolumes, expandEffectiveVolumes bool, q query.Builder) (*bun.SelectQuery, error) { ret := s.db.NewSelect() if expandEffectiveVolumes && !s.ledger.HasFeature(features.FeatureMovesHistoryPostCommitEffectiveVolumes, "SYNC") { - return ret.Err(ledgercontroller.NewErrMissingFeature(features.FeatureMovesHistoryPostCommitEffectiveVolumes)) + return nil, ledgercontroller.NewErrMissingFeature(features.FeatureMovesHistoryPostCommitEffectiveVolumes) } if q != nil { @@ -91,7 +91,7 @@ func (s *Store) selectTransactions(date *time.Time, expandVolumes, expandEffecti return nil }); err != nil { - return ret.Err(err) + return nil, err } } @@ -229,7 +229,7 @@ func (s *Store) selectTransactions(date *time.Time, expandVolumes, expandEffecti } })) if err != nil { - return ret.Err(err) + return nil, err } if len(args) > 0 { @@ -239,7 +239,7 @@ func (s *Store) selectTransactions(date *time.Time, expandVolumes, expandEffecti } } - return ret + return ret, nil } func (s *Store) CommitTransaction(ctx context.Context, tx *ledger.Transaction) error { @@ -317,14 +317,18 @@ func (s *Store) ListTransactions(ctx context.Context, q ledgercontroller.ListTra s.tracer, s.listTransactionsHistogram, func(ctx context.Context) (*bunpaginate.Cursor[ledger.Transaction], error) { + selectTransactions, err := s.selectTransactions( + q.Options.Options.PIT, + q.Options.Options.ExpandVolumes, + q.Options.Options.ExpandEffectiveVolumes, + q.Options.QueryBuilder, + ) + if err != nil { + return nil, err + } cursor, err := bunpaginate.UsingColumn[ledgercontroller.PaginatedQueryOptions[ledgercontroller.PITFilterWithVolumes], ledger.Transaction]( ctx, - s.selectTransactions( - q.Options.Options.PIT, - q.Options.Options.ExpandVolumes, - q.Options.Options.ExpandEffectiveVolumes, - q.Options.QueryBuilder, - ), + selectTransactions, bunpaginate.ColumnPaginatedQuery[ledgercontroller.PaginatedQueryOptions[ledgercontroller.PITFilterWithVolumes]](q), ) if err != nil { @@ -343,13 +347,17 @@ func (s *Store) CountTransactions(ctx context.Context, q ledgercontroller.ListTr s.tracer, s.countTransactionsHistogram, func(ctx context.Context) (int, error) { + selectTransactions, err := s.selectTransactions( + q.Options.Options.PIT, + q.Options.Options.ExpandVolumes, + q.Options.Options.ExpandEffectiveVolumes, + q.Options.QueryBuilder, + ) + if err != nil { + return 0, err + } return s.db.NewSelect(). - TableExpr("(?) data", s.selectTransactions( - q.Options.Options.PIT, - q.Options.Options.ExpandVolumes, - q.Options.Options.ExpandEffectiveVolumes, - q.Options.QueryBuilder, - )). + TableExpr("(?) data", selectTransactions). Count(ctx) }, ) @@ -364,12 +372,16 @@ func (s *Store) GetTransaction(ctx context.Context, filter ledgercontroller.GetT func(ctx context.Context) (*ledger.Transaction, error) { ret := &ledger.Transaction{} - if err := s.selectTransactions( + selectTransactions, err := s.selectTransactions( filter.PIT, filter.ExpandVolumes, filter.ExpandEffectiveVolumes, nil, - ). + ) + if err != nil { + return nil, err + } + if err := selectTransactions. Where("transactions.id = ?", filter.ID). Limit(1). Model(ret). diff --git a/internal/storage/ledger/volumes.go b/internal/storage/ledger/volumes.go index 8b7c26722..48f207e21 100644 --- a/internal/storage/ledger/volumes.go +++ b/internal/storage/ledger/volumes.go @@ -64,7 +64,7 @@ func (s *Store) UpdateVolumes(ctx context.Context, accountVolumes ...ledger.Acco ) } -func (s *Store) selectVolumes(oot, pit *time.Time, useInsertionDate bool, groupLevel int, q lquery.Builder) *bun.SelectQuery { +func (s *Store) selectVolumes(oot, pit *time.Time, useInsertionDate bool, groupLevel int, q lquery.Builder) (*bun.SelectQuery, error) { ret := s.db.NewSelect() var ( @@ -98,7 +98,7 @@ func (s *Store) selectVolumes(oot, pit *time.Time, useInsertionDate bool, groupL return nil }) if err != nil { - return ret.Err(err) + return nil, err } } @@ -115,7 +115,7 @@ func (s *Store) selectVolumes(oot, pit *time.Time, useInsertionDate bool, groupL Order("accounts_address", "asset") } else { if !s.ledger.HasFeature(features.FeatureMovesHistory, "ON") { - return ret.Err(ledgercontroller.NewErrMissingFeature(features.FeatureMovesHistory)) + return nil, ledgercontroller.NewErrMissingFeature(features.FeatureMovesHistory) } dateFilterColumn := "effective_date" @@ -210,7 +210,7 @@ func (s *Store) selectVolumes(oot, pit *time.Time, useInsertionDate bool, groupL } })) if err != nil { - return ret.Err(err) + return nil, err } ret = ret.Where(where, args...) } @@ -232,7 +232,7 @@ func (s *Store) selectVolumes(oot, pit *time.Time, useInsertionDate bool, groupL globalQuery = globalQuery.ColumnExpr("address as account, asset, input, output, balance") } - return globalQuery + return globalQuery, nil } func (s *Store) GetVolumesWithBalances(ctx context.Context, q ledgercontroller.GetVolumesWithBalancesQuery) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { @@ -242,15 +242,19 @@ func (s *Store) GetVolumesWithBalances(ctx context.Context, q ledgercontroller.G s.tracer, s.getVolumesWithBalancesHistogram, func(ctx context.Context) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { + selectVolumes, err := s.selectVolumes( + q.Options.Options.OOT, + q.Options.Options.PIT, + q.Options.Options.UseInsertionDate, + q.Options.Options.GroupLvl, + q.Options.QueryBuilder, + ) + if err != nil { + return nil, err + } return bunpaginate.UsingOffset[ledgercontroller.PaginatedQueryOptions[ledgercontroller.FiltersForVolumes], ledger.VolumesWithBalanceByAssetByAccount]( ctx, - s.selectVolumes( - q.Options.Options.OOT, - q.Options.Options.PIT, - q.Options.Options.UseInsertionDate, - q.Options.Options.GroupLvl, - q.Options.QueryBuilder, - ), + selectVolumes, bunpaginate.OffsetPaginatedQuery[ledgercontroller.PaginatedQueryOptions[ledgercontroller.FiltersForVolumes]](q), ) }, From c8a3f5b591f21d32d0f5b9d998c80a3fa427359f Mon Sep 17 00:00:00 2001 From: Ragot Geoffrey Date: Mon, 9 Dec 2024 21:24:25 +0100 Subject: [PATCH 58/71] fix: balance aggregated with address and metadata filters (#610) * fix: error handling at storage level * fix: balance aggregation combining partial address filtering and metadata --- internal/storage/ledger/balances.go | 11 +++-------- internal/storage/ledger/balances_test.go | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/internal/storage/ledger/balances.go b/internal/storage/ledger/balances.go index 7bf95e7d8..adbb07950 100644 --- a/internal/storage/ledger/balances.go +++ b/internal/storage/ledger/balances.go @@ -95,6 +95,7 @@ func (s *Store) selectAccountWithAssetAndVolumes(date *time.Time, useInsertionDa ColumnExpr("*"). TableExpr("(?) accounts_volumes", selectAccountsWithVolumes) + needAccount := needAddressSegment if needMetadata { if s.ledger.HasFeature(features.FeatureAccountMetadataHistory, "SYNC") && date != nil && !date.IsZero() { selectAccountsWithVolumes = selectAccountsWithVolumes. @@ -103,17 +104,11 @@ func (s *Store) selectAccountWithAssetAndVolumes(date *time.Time, useInsertionDa s.selectDistinctAccountMetadataHistories(date), ) } else { - selectAccountsWithVolumes = selectAccountsWithVolumes. - Join( - `join (?) accounts on accounts.address = accounts_volumes.accounts_address`, - s.db.NewSelect(). - ModelTableExpr(s.GetPrefixedRelationName("accounts")). - Where("ledger = ?", s.ledger.Name), - ) + needAccount = true } } - if needAddressSegment { + if needAccount { selectAccountsWithVolumes = s.db.NewSelect(). TableExpr( "(?) accounts", diff --git a/internal/storage/ledger/balances_test.go b/internal/storage/ledger/balances_test.go index 5b32c9aaa..3507962df 100644 --- a/internal/storage/ledger/balances_test.go +++ b/internal/storage/ledger/balances_test.go @@ -325,4 +325,20 @@ func TestBalancesAggregates(t *testing.T) { ), }, ret) }) + + t.Run("using a filter on metadata and on address", func(t *testing.T) { + t.Parallel() + ret, err := store.GetAggregatedBalances(ctx, ledgercontroller.NewGetAggregatedBalancesQuery( + ledgercontroller.PITFilter{}, + query.And( + query.Match("address", "users:"), + query.Match("metadata[category]", "premium"), + ), + false, + )) + require.NoError(t, err) + require.Equal(t, ledger.BalancesByAssets{ + "USD": big.NewInt(0).Mul(bigInt, big.NewInt(2)), + }, ret) + }) } From 4a5d23d46d6fdaa8ca3cb4dc9b13a38cacf4dc99 Mon Sep 17 00:00:00 2001 From: Ragot Geoffrey Date: Fri, 13 Dec 2024 12:38:00 +0100 Subject: [PATCH 59/71] feat: refine metrics (#613) --- .../ledger/controller_with_traces.go | 434 ++++++++++++++---- internal/controller/system/controller.go | 46 +- internal/controller/system/module.go | 4 +- 3 files changed, 384 insertions(+), 100 deletions(-) diff --git a/internal/controller/ledger/controller_with_traces.go b/internal/controller/ledger/controller_with_traces.go index 214e415a5..b95bb13f0 100644 --- a/internal/controller/ledger/controller_with_traces.go +++ b/internal/controller/ledger/controller_with_traces.go @@ -5,6 +5,7 @@ import ( "database/sql" "github.com/formancehq/go-libs/v2/migrations" "github.com/formancehq/ledger/internal/tracing" + "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/trace" "github.com/formancehq/go-libs/v2/bun/bunpaginate" @@ -14,163 +15,434 @@ import ( type ControllerWithTraces struct { underlying Controller tracer trace.Tracer + + beginTxHistogram metric.Int64Histogram + commitHistogram metric.Int64Histogram + rollbackHistogram metric.Int64Histogram + listTransactionsHistogram metric.Int64Histogram + countTransactionsHistogram metric.Int64Histogram + getTransactionHistogram metric.Int64Histogram + countAccountsHistogram metric.Int64Histogram + listAccountsHistogram metric.Int64Histogram + getAccountHistogram metric.Int64Histogram + getAggregatedBalancesHistogram metric.Int64Histogram + listLogsHistogram metric.Int64Histogram + importHistogram metric.Int64Histogram + exportHistogram metric.Int64Histogram + isDatabaseUpToDateHistogram metric.Int64Histogram + getVolumesWithBalancesHistogram metric.Int64Histogram + getStatsHistogram metric.Int64Histogram + createTransactionHistogram metric.Int64Histogram + revertTransactionHistogram metric.Int64Histogram + saveTransactionMetadataHistogram metric.Int64Histogram + saveAccountMetadataHistogram metric.Int64Histogram + deleteTransactionMetadataHistogram metric.Int64Histogram + deleteAccountMetadataHistogram metric.Int64Histogram } -func NewControllerWithTraces(underlying Controller, tracer trace.Tracer) *ControllerWithTraces { - return &ControllerWithTraces{ +func NewControllerWithTraces(underlying Controller, tracer trace.Tracer, meter metric.Meter) *ControllerWithTraces { + ret := &ControllerWithTraces{ underlying: underlying, tracer: tracer, } + + var err error + ret.beginTxHistogram, err = meter.Int64Histogram("BeginTX") + if err != nil { + panic(err) + } + ret.listTransactionsHistogram, err = meter.Int64Histogram("ListTransactions") + if err != nil { + panic(err) + } + ret.commitHistogram, err = meter.Int64Histogram("Commit") + if err != nil { + panic(err) + } + ret.rollbackHistogram, err = meter.Int64Histogram("Rollback") + if err != nil { + panic(err) + } + ret.countTransactionsHistogram, err = meter.Int64Histogram("CountTransactions") + if err != nil { + panic(err) + } + ret.getTransactionHistogram, err = meter.Int64Histogram("GetTransaction") + if err != nil { + panic(err) + } + ret.countAccountsHistogram, err = meter.Int64Histogram("CountAccounts") + if err != nil { + panic(err) + } + ret.listAccountsHistogram, err = meter.Int64Histogram("ListAccounts") + if err != nil { + panic(err) + } + ret.getAccountHistogram, err = meter.Int64Histogram("GetAccount") + if err != nil { + panic(err) + } + ret.getAggregatedBalancesHistogram, err = meter.Int64Histogram("GetAggregatedBalances") + if err != nil { + panic(err) + } + ret.listLogsHistogram, err = meter.Int64Histogram("ListLogs") + if err != nil { + panic(err) + } + ret.importHistogram, err = meter.Int64Histogram("Import") + if err != nil { + panic(err) + } + ret.exportHistogram, err = meter.Int64Histogram("Export") + if err != nil { + panic(err) + } + ret.isDatabaseUpToDateHistogram, err = meter.Int64Histogram("IsDatabaseUpToDate") + if err != nil { + panic(err) + } + ret.getVolumesWithBalancesHistogram, err = meter.Int64Histogram("GetVolumesWithBalances") + if err != nil { + panic(err) + } + ret.getStatsHistogram, err = meter.Int64Histogram("GetStats") + if err != nil { + panic(err) + } + ret.createTransactionHistogram, err = meter.Int64Histogram("CreateTransaction") + if err != nil { + panic(err) + } + ret.revertTransactionHistogram, err = meter.Int64Histogram("RevertTransaction") + if err != nil { + panic(err) + } + ret.saveTransactionMetadataHistogram, err = meter.Int64Histogram("SaveTransactionMetadata") + if err != nil { + panic(err) + } + ret.saveAccountMetadataHistogram, err = meter.Int64Histogram("SaveAccountMetadata") + if err != nil { + panic(err) + } + ret.deleteTransactionMetadataHistogram, err = meter.Int64Histogram("DeleteTransactionMetadata") + if err != nil { + panic(err) + } + ret.deleteAccountMetadataHistogram, err = meter.Int64Histogram("DeleteAccountMetadata") + if err != nil { + panic(err) + } + + return ret } func (c *ControllerWithTraces) BeginTX(ctx context.Context, options *sql.TxOptions) (Controller, error) { - return tracing.Trace(ctx, c.tracer, "BeginTX", func(ctx context.Context) (Controller, error) { - ctrl, err := c.underlying.BeginTX(ctx, options) - if err != nil { - return nil, err - } - - return &ControllerWithTraces{ - underlying: ctrl, - tracer: c.tracer, - }, nil - }) + return tracing.TraceWithMetric( + ctx, + "BeginTX", + c.tracer, + c.beginTxHistogram, + func(ctx context.Context) (Controller, error) { + ctrl, err := c.underlying.BeginTX(ctx, options) + if err != nil { + return nil, err + } + + ret := *c + ret.underlying = ctrl + + return &ret, nil + }, + ) } func (c *ControllerWithTraces) Commit(ctx context.Context) error { - return tracing.SkipResult(tracing.Trace(ctx, c.tracer, "BeginTX", tracing.NoResult(func(ctx context.Context) error { - return c.underlying.Commit(ctx) - }))) + return tracing.SkipResult(tracing.TraceWithMetric( + ctx, + "Commit", + c.tracer, + c.commitHistogram, + tracing.NoResult(func(ctx context.Context) error { + return c.underlying.Commit(ctx) + }), + )) } func (c *ControllerWithTraces) Rollback(ctx context.Context) error { - return tracing.SkipResult(tracing.Trace(ctx, c.tracer, "BeginTX", tracing.NoResult(func(ctx context.Context) error { - return c.underlying.Rollback(ctx) - }))) + return tracing.SkipResult(tracing.TraceWithMetric( + ctx, + "Rollback", + c.tracer, + c.rollbackHistogram, + tracing.NoResult(func(ctx context.Context) error { + return c.underlying.Rollback(ctx) + }), + )) } func (c *ControllerWithTraces) GetMigrationsInfo(ctx context.Context) ([]migrations.Info, error) { - return c.underlying.GetMigrationsInfo(ctx) + return tracing.TraceWithMetric( + ctx, + "GetMigrationsInfo", + c.tracer, + c.listTransactionsHistogram, + func(ctx context.Context) ([]migrations.Info, error) { + return c.underlying.GetMigrationsInfo(ctx) + }, + ) } func (c *ControllerWithTraces) ListTransactions(ctx context.Context, q ListTransactionsQuery) (*bunpaginate.Cursor[ledger.Transaction], error) { - return tracing.Trace(ctx, c.tracer, "ListTransactions", func(ctx context.Context) (*bunpaginate.Cursor[ledger.Transaction], error) { - return c.underlying.ListTransactions(ctx, q) - }) + return tracing.TraceWithMetric( + ctx, + "ListTransactions", + c.tracer, + c.listTransactionsHistogram, + func(ctx context.Context) (*bunpaginate.Cursor[ledger.Transaction], error) { + return c.underlying.ListTransactions(ctx, q) + }, + ) } func (c *ControllerWithTraces) CountTransactions(ctx context.Context, q ListTransactionsQuery) (int, error) { - return tracing.Trace(ctx, c.tracer, "CountTransactions", func(ctx context.Context) (int, error) { - return c.underlying.CountTransactions(ctx, q) - }) + return tracing.TraceWithMetric( + ctx, + "CountTransactions", + c.tracer, + c.countTransactionsHistogram, + func(ctx context.Context) (int, error) { + return c.underlying.CountTransactions(ctx, q) + }, + ) } func (c *ControllerWithTraces) GetTransaction(ctx context.Context, query GetTransactionQuery) (*ledger.Transaction, error) { - return tracing.Trace(ctx, c.tracer, "GetTransaction", func(ctx context.Context) (*ledger.Transaction, error) { - return c.underlying.GetTransaction(ctx, query) - }) + return tracing.TraceWithMetric( + ctx, + "GetTransaction", + c.tracer, + c.getTransactionHistogram, + func(ctx context.Context) (*ledger.Transaction, error) { + return c.underlying.GetTransaction(ctx, query) + }, + ) } func (c *ControllerWithTraces) CountAccounts(ctx context.Context, a ListAccountsQuery) (int, error) { - return tracing.Trace(ctx, c.tracer, "CountAccounts", func(ctx context.Context) (int, error) { - return c.underlying.CountAccounts(ctx, a) - }) + return tracing.TraceWithMetric( + ctx, + "CountAccounts", + c.tracer, + c.countAccountsHistogram, + func(ctx context.Context) (int, error) { + return c.underlying.CountAccounts(ctx, a) + }, + ) } func (c *ControllerWithTraces) ListAccounts(ctx context.Context, a ListAccountsQuery) (*bunpaginate.Cursor[ledger.Account], error) { - return tracing.Trace(ctx, c.tracer, "ListAccounts", func(ctx context.Context) (*bunpaginate.Cursor[ledger.Account], error) { - return c.underlying.ListAccounts(ctx, a) - }) + return tracing.TraceWithMetric( + ctx, + "ListAccounts", + c.tracer, + c.listAccountsHistogram, + func(ctx context.Context) (*bunpaginate.Cursor[ledger.Account], error) { + return c.underlying.ListAccounts(ctx, a) + }, + ) } func (c *ControllerWithTraces) GetAccount(ctx context.Context, q GetAccountQuery) (*ledger.Account, error) { - return tracing.Trace(ctx, c.tracer, "GetAccount", func(ctx context.Context) (*ledger.Account, error) { - return c.underlying.GetAccount(ctx, q) - }) + return tracing.TraceWithMetric( + ctx, + "GetAccount", + c.tracer, + c.getAccountHistogram, + func(ctx context.Context) (*ledger.Account, error) { + return c.underlying.GetAccount(ctx, q) + }, + ) } func (c *ControllerWithTraces) GetAggregatedBalances(ctx context.Context, q GetAggregatedBalanceQuery) (ledger.BalancesByAssets, error) { - return tracing.Trace(ctx, c.tracer, "GetAggregatedBalances", func(ctx context.Context) (ledger.BalancesByAssets, error) { - return c.underlying.GetAggregatedBalances(ctx, q) - }) + return tracing.TraceWithMetric( + ctx, + "GetAggregatedBalances", + c.tracer, + c.getAggregatedBalancesHistogram, + func(ctx context.Context) (ledger.BalancesByAssets, error) { + return c.underlying.GetAggregatedBalances(ctx, q) + }, + ) } func (c *ControllerWithTraces) ListLogs(ctx context.Context, q GetLogsQuery) (*bunpaginate.Cursor[ledger.Log], error) { - return tracing.Trace(ctx, c.tracer, "ListLogs", func(ctx context.Context) (*bunpaginate.Cursor[ledger.Log], error) { - return c.underlying.ListLogs(ctx, q) - }) + return tracing.TraceWithMetric( + ctx, + "ListLogs", + c.tracer, + c.listLogsHistogram, + func(ctx context.Context) (*bunpaginate.Cursor[ledger.Log], error) { + return c.underlying.ListLogs(ctx, q) + }, + ) } func (c *ControllerWithTraces) Import(ctx context.Context, stream chan ledger.Log) error { - return tracing.SkipResult(tracing.Trace(ctx, c.tracer, "Import", tracing.NoResult(func(ctx context.Context) error { - return c.underlying.Import(ctx, stream) - }))) + return tracing.SkipResult(tracing.TraceWithMetric( + ctx, + "Import", + c.tracer, + c.importHistogram, + tracing.NoResult(func(ctx context.Context) error { + return c.underlying.Import(ctx, stream) + }), + )) } func (c *ControllerWithTraces) Export(ctx context.Context, w ExportWriter) error { - return tracing.SkipResult(tracing.Trace(ctx, c.tracer, "Export", tracing.NoResult(func(ctx context.Context) error { - return c.underlying.Export(ctx, w) - }))) + return tracing.SkipResult(tracing.TraceWithMetric( + ctx, + "Export", + c.tracer, + c.exportHistogram, + tracing.NoResult(func(ctx context.Context) error { + return c.underlying.Export(ctx, w) + }), + )) } func (c *ControllerWithTraces) IsDatabaseUpToDate(ctx context.Context) (bool, error) { - return tracing.Trace(ctx, c.tracer, "IsDatabaseUpToDate", func(ctx context.Context) (bool, error) { - return c.underlying.IsDatabaseUpToDate(ctx) - }) + return tracing.TraceWithMetric( + ctx, + "IsDatabaseUpToDate", + c.tracer, + c.isDatabaseUpToDateHistogram, + func(ctx context.Context) (bool, error) { + return c.underlying.IsDatabaseUpToDate(ctx) + }, + ) } func (c *ControllerWithTraces) GetVolumesWithBalances(ctx context.Context, q GetVolumesWithBalancesQuery) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { - return tracing.Trace(ctx, c.tracer, "GetVolumesWithBalances", func(ctx context.Context) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { - return c.underlying.GetVolumesWithBalances(ctx, q) - }) + return tracing.TraceWithMetric( + ctx, + "GetVolumesWithBalances", + c.tracer, + c.getVolumesWithBalancesHistogram, + func(ctx context.Context) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { + return c.underlying.GetVolumesWithBalances(ctx, q) + }, + ) } func (c *ControllerWithTraces) CreateTransaction(ctx context.Context, parameters Parameters[RunScript]) (*ledger.Log, *ledger.CreatedTransaction, error) { - ctx, span := c.tracer.Start(ctx, "CreateTransaction") - defer span.End() + var ( + createdTransaction *ledger.CreatedTransaction + log *ledger.Log + err error + ) + _, err = tracing.TraceWithMetric( + ctx, + "CreateTransaction", + c.tracer, + c.createTransactionHistogram, + func(ctx context.Context) (any, error) { + log, createdTransaction, err = c.underlying.CreateTransaction(ctx, parameters) + return nil, err + }, + ) + if err != nil { + return nil, nil, err + } - return c.underlying.CreateTransaction(ctx, parameters) + return log, createdTransaction, nil } func (c *ControllerWithTraces) RevertTransaction(ctx context.Context, parameters Parameters[RevertTransaction]) (*ledger.Log, *ledger.RevertedTransaction, error) { - ctx, span := c.tracer.Start(ctx, "RevertTransaction") - defer span.End() + var ( + revertedTransaction *ledger.RevertedTransaction + log *ledger.Log + err error + ) + _, err = tracing.TraceWithMetric( + ctx, + "RevertTransaction", + c.tracer, + c.revertTransactionHistogram, + func(ctx context.Context) (any, error) { + log, revertedTransaction, err = c.underlying.RevertTransaction(ctx, parameters) + return nil, err + }, + ) + if err != nil { + return nil, nil, err + } - return c.underlying.RevertTransaction(ctx, parameters) + return log, revertedTransaction, nil } func (c *ControllerWithTraces) SaveTransactionMetadata(ctx context.Context, parameters Parameters[SaveTransactionMetadata]) (*ledger.Log, error) { - ctx, span := c.tracer.Start(ctx, "SaveTransactionMetadata") - defer span.End() - - return c.underlying.SaveTransactionMetadata(ctx, parameters) + return tracing.TraceWithMetric( + ctx, + "SaveTransactionMetadata", + c.tracer, + c.saveTransactionMetadataHistogram, + func(ctx context.Context) (*ledger.Log, error) { + return c.underlying.SaveTransactionMetadata(ctx, parameters) + }, + ) } func (c *ControllerWithTraces) SaveAccountMetadata(ctx context.Context, parameters Parameters[SaveAccountMetadata]) (*ledger.Log, error) { - ctx, span := c.tracer.Start(ctx, "SaveAccountMetadata") - defer span.End() - - return c.underlying.SaveAccountMetadata(ctx, parameters) + return tracing.TraceWithMetric( + ctx, + "SaveAccountMetadata", + c.tracer, + c.saveAccountMetadataHistogram, + func(ctx context.Context) (*ledger.Log, error) { + return c.underlying.SaveAccountMetadata(ctx, parameters) + }, + ) } func (c *ControllerWithTraces) DeleteTransactionMetadata(ctx context.Context, parameters Parameters[DeleteTransactionMetadata]) (*ledger.Log, error) { - ctx, span := c.tracer.Start(ctx, "DeleteTransactionMetadata") - defer span.End() - - return c.underlying.DeleteTransactionMetadata(ctx, parameters) + return tracing.TraceWithMetric( + ctx, + "DeleteTransactionMetadata", + c.tracer, + c.deleteTransactionMetadataHistogram, + func(ctx context.Context) (*ledger.Log, error) { + return c.underlying.DeleteTransactionMetadata(ctx, parameters) + }, + ) } func (c *ControllerWithTraces) DeleteAccountMetadata(ctx context.Context, parameters Parameters[DeleteAccountMetadata]) (*ledger.Log, error) { - ctx, span := c.tracer.Start(ctx, "DeleteAccountMetadata") - defer span.End() - - return c.underlying.DeleteAccountMetadata(ctx, parameters) + return tracing.TraceWithMetric( + ctx, + "DeleteAccountMetadata", + c.tracer, + c.deleteAccountMetadataHistogram, + func(ctx context.Context) (*ledger.Log, error) { + return c.underlying.DeleteAccountMetadata(ctx, parameters) + }, + ) } func (c *ControllerWithTraces) GetStats(ctx context.Context) (Stats, error) { - return tracing.Trace(ctx, c.tracer, "GetStats", func(ctx context.Context) (Stats, error) { - return c.underlying.GetStats(ctx) - }) + return tracing.TraceWithMetric( + ctx, + "GetStats", + c.tracer, + c.getStatsHistogram, + func(ctx context.Context) (Stats, error) { + return c.underlying.GetStats(ctx) + }, + ) } var _ Controller = (*ControllerWithTraces)(nil) diff --git a/internal/controller/system/controller.go b/internal/controller/system/controller.go index 44265079f..ca2f836c0 100644 --- a/internal/controller/system/controller.go +++ b/internal/controller/system/controller.go @@ -3,6 +3,7 @@ package system import ( "context" "github.com/formancehq/ledger/pkg/features" + "go.opentelemetry.io/otel/attribute" "reflect" "time" @@ -38,29 +39,40 @@ type DefaultController struct { registry *ledgercontroller.StateRegistry databaseRetryConfiguration DatabaseRetryConfiguration - tracer trace.Tracer - meter metric.Meter + tracerProvider trace.TracerProvider + meterProvider metric.MeterProvider enableFeatures bool } func (ctrl *DefaultController) GetLedgerController(ctx context.Context, name string) (ledgercontroller.Controller, error) { - return tracing.Trace(ctx, ctrl.tracer, "GetLedgerController", func(ctx context.Context) (ledgercontroller.Controller, error) { + return tracing.Trace(ctx, ctrl.tracerProvider.Tracer("system"), "GetLedgerController", func(ctx context.Context) (ledgercontroller.Controller, error) { store, l, err := ctrl.store.OpenLedger(ctx, name) if err != nil { return nil, err } + instrumentationAttributes := []attribute.KeyValue{ + attribute.String("ledger", name), + } + + meter := ctrl.meterProvider.Meter("ledger", metric.WithInstrumentationAttributes( + instrumentationAttributes..., + )) + tracer := ctrl.tracerProvider.Tracer("ledger", trace.WithInstrumentationAttributes( + instrumentationAttributes..., + )) + var ledgerController ledgercontroller.Controller = ledgercontroller.NewDefaultController( *l, store, ctrl.parser, - ledgercontroller.WithMeter(ctrl.meter), + ledgercontroller.WithMeter(meter), ) // Add too many client error handling ledgerController = ledgercontroller.NewControllerWithTooManyClientHandling( ledgerController, - ctrl.tracer, + tracer, ledgercontroller.DelayCalculatorFn(func(i int) time.Duration { if i < ctrl.databaseRetryConfiguration.MaxRetry { return time.Duration(i+1) * ctrl.databaseRetryConfiguration.Delay @@ -74,7 +86,7 @@ func (ctrl *DefaultController) GetLedgerController(ctx context.Context, name str ledgerController = ledgercontroller.NewControllerWithCache(*l, ledgerController, ctrl.registry) // Add traces - ledgerController = ledgercontroller.NewControllerWithTraces(ledgerController, ctrl.tracer) + ledgerController = ledgercontroller.NewControllerWithTraces(ledgerController, tracer, meter) // Add events listener if ctrl.listener != nil { @@ -86,7 +98,7 @@ func (ctrl *DefaultController) GetLedgerController(ctx context.Context, name str } func (ctrl *DefaultController) CreateLedger(ctx context.Context, name string, configuration ledger.Configuration) error { - return tracing.SkipResult(tracing.Trace(ctx, ctrl.tracer, "CreateLedger", tracing.NoResult(func(ctx context.Context) error { + return tracing.SkipResult(tracing.Trace(ctx, ctrl.tracerProvider.Tracer("system"), "CreateLedger", tracing.NoResult(func(ctx context.Context) error { configuration.SetDefaults() if !ctrl.enableFeatures { @@ -105,25 +117,25 @@ func (ctrl *DefaultController) CreateLedger(ctx context.Context, name string, co } func (ctrl *DefaultController) GetLedger(ctx context.Context, name string) (*ledger.Ledger, error) { - return tracing.Trace(ctx, ctrl.tracer, "GetLedger", func(ctx context.Context) (*ledger.Ledger, error) { + return tracing.Trace(ctx, ctrl.tracerProvider.Tracer("system"), "GetLedger", func(ctx context.Context) (*ledger.Ledger, error) { return ctrl.store.GetLedger(ctx, name) }) } func (ctrl *DefaultController) ListLedgers(ctx context.Context, query ledgercontroller.ListLedgersQuery) (*bunpaginate.Cursor[ledger.Ledger], error) { - return tracing.Trace(ctx, ctrl.tracer, "ListLedgers", func(ctx context.Context) (*bunpaginate.Cursor[ledger.Ledger], error) { + return tracing.Trace(ctx, ctrl.tracerProvider.Tracer("system"), "ListLedgers", func(ctx context.Context) (*bunpaginate.Cursor[ledger.Ledger], error) { return ctrl.store.ListLedgers(ctx, query) }) } func (ctrl *DefaultController) UpdateLedgerMetadata(ctx context.Context, name string, m map[string]string) error { - return tracing.SkipResult(tracing.Trace(ctx, ctrl.tracer, "UpdateLedgerMetadata", tracing.NoResult(func(ctx context.Context) error { + return tracing.SkipResult(tracing.Trace(ctx, ctrl.tracerProvider.Tracer("system"), "UpdateLedgerMetadata", tracing.NoResult(func(ctx context.Context) error { return ctrl.store.UpdateLedgerMetadata(ctx, name, m) }))) } func (ctrl *DefaultController) DeleteLedgerMetadata(ctx context.Context, param string, key string) error { - return tracing.SkipResult(tracing.Trace(ctx, ctrl.tracer, "DeleteLedgerMetadata", tracing.NoResult(func(ctx context.Context) error { + return tracing.SkipResult(tracing.Trace(ctx, ctrl.tracerProvider.Tracer("system"), "DeleteLedgerMetadata", tracing.NoResult(func(ctx context.Context) error { return ctrl.store.DeleteLedgerMetadata(ctx, param, key) }))) } @@ -155,15 +167,15 @@ func WithDatabaseRetryConfiguration(configuration DatabaseRetryConfiguration) Op } } -func WithMeter(m metric.Meter) Option { +func WithMeterProvider(mp metric.MeterProvider) Option { return func(ctrl *DefaultController) { - ctrl.meter = m + ctrl.meterProvider = mp } } -func WithTracer(t trace.Tracer) Option { +func WithTracerProvider(t trace.TracerProvider) Option { return func(ctrl *DefaultController) { - ctrl.tracer = t + ctrl.tracerProvider = t } } @@ -174,6 +186,6 @@ func WithEnableFeatures(v bool) Option { } var defaultOptions = []Option{ - WithMeter(noopmetrics.Meter{}), - WithTracer(nooptracer.Tracer{}), + WithMeterProvider(noopmetrics.MeterProvider{}), + WithTracerProvider(nooptracer.TracerProvider{}), } diff --git a/internal/controller/system/module.go b/internal/controller/system/module.go index b8d4b5691..694c7bed0 100644 --- a/internal/controller/system/module.go +++ b/internal/controller/system/module.go @@ -48,8 +48,8 @@ func NewFXModule(configuration ModuleConfiguration) fx.Option { listener, WithParser(parser), WithDatabaseRetryConfiguration(configuration.DatabaseRetryConfiguration), - WithMeter(meterProvider.Meter("core")), - WithTracer(tracerProvider.Tracer("core")), + WithMeterProvider(meterProvider), + WithTracerProvider(tracerProvider), WithEnableFeatures(configuration.EnableFeatures), ) }), From faf326e7fb0a134a8c62a3fb7e7476b047618945 Mon Sep 17 00:00:00 2001 From: Ragot Geoffrey Date: Wed, 18 Dec 2024 16:42:40 +0100 Subject: [PATCH 60/71] fix: too many loops while running migrations (#618) --- .../migrations/18-transactions-fill-inserted-at/up.sql | 2 +- .../bucket/migrations/19-transactions-fill-pcv/up.sql | 8 ++++++-- .../migrations/20-accounts-volumes-fill-history/up.sql | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/internal/storage/bucket/migrations/18-transactions-fill-inserted-at/up.sql b/internal/storage/bucket/migrations/18-transactions-fill-inserted-at/up.sql index fdc93ea71..c87d6ec9a 100644 --- a/internal/storage/bucket/migrations/18-transactions-fill-inserted-at/up.sql +++ b/internal/storage/bucket/migrations/18-transactions-fill-inserted-at/up.sql @@ -24,7 +24,7 @@ do $$ perform pg_notify('migrations-{{ .Schema }}', 'init: ' || _count); - for i in 0.._count by _batch_size loop + for i in 0.._count-1 by _batch_size loop -- disable triggers set session_replication_role = replica; diff --git a/internal/storage/bucket/migrations/19-transactions-fill-pcv/up.sql b/internal/storage/bucket/migrations/19-transactions-fill-pcv/up.sql index d3cf97488..0c2d9d944 100644 --- a/internal/storage/bucket/migrations/19-transactions-fill-pcv/up.sql +++ b/internal/storage/bucket/migrations/19-transactions-fill-pcv/up.sql @@ -22,10 +22,14 @@ do $$ ) moves group by transactions_id; - perform pg_notify('migrations-{{ .Schema }}', 'init: ' || (select count(*) from moves_view)); - create index moves_view_idx on moves_view(transactions_id); + if (select count(*) from moves_view) = 0 then + return; + end if; + + perform pg_notify('migrations-{{ .Schema }}', 'init: ' || (select count(*) from moves_view)); + -- disable triggers set session_replication_role = replica; diff --git a/internal/storage/bucket/migrations/20-accounts-volumes-fill-history/up.sql b/internal/storage/bucket/migrations/20-accounts-volumes-fill-history/up.sql index 56083891f..6817d885e 100644 --- a/internal/storage/bucket/migrations/20-accounts-volumes-fill-history/up.sql +++ b/internal/storage/bucket/migrations/20-accounts-volumes-fill-history/up.sql @@ -31,7 +31,7 @@ do $$ raise info '_count: %', _count; - for i in 0.._count by _batch_size loop + for i in 0.._count-1 by _batch_size loop with _rows as ( select * from tmp_volumes From ea3c6b947852afac2c3eaed025841aa281a059cc Mon Sep 17 00:00:00 2001 From: Ragot Geoffrey Date: Thu, 19 Dec 2024 16:37:35 +0100 Subject: [PATCH 61/71] fix: too many indexes and missing indexes (#621) --- docker-compose.yml | 2 +- internal/storage/bucket/default_bucket.go | 56 ------------------- .../24-accounts-metadata-index/notes.yaml | 1 + .../24-accounts-metadata-index/up.sql | 1 + 4 files changed, 3 insertions(+), 57 deletions(-) create mode 100644 internal/storage/bucket/migrations/24-accounts-metadata-index/notes.yaml create mode 100644 internal/storage/bucket/migrations/24-accounts-metadata-index/up.sql diff --git a/docker-compose.yml b/docker-compose.yml index 40a521713..ab044c1c0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,7 +43,7 @@ services: - "16686:16686/tcp" ledger: - image: golang:1.22-alpine + image: golang:1.23-alpine entrypoint: go run main.go serve volumes: - .:/src diff --git a/internal/storage/bucket/default_bucket.go b/internal/storage/bucket/default_bucket.go index 1b9fb8233..13e3ca8af 100644 --- a/internal/storage/bucket/default_bucket.go +++ b/internal/storage/bucket/default_bucket.go @@ -117,12 +117,6 @@ var ledgerSetups = []ledgerSetup{ ), 1)::bigint, false); `, }, - { - requireFeatures: features.FeatureSet{ - features.FeatureMovesHistoryPostCommitEffectiveVolumes: "SYNC", - }, - script: `create index "pcev_{{.ID}}" on "{{.Bucket}}".moves (accounts_address, asset, effective_date desc) where ledger = '{{.Name}}';`, - }, { requireFeatures: features.FeatureSet{ features.FeatureMovesHistoryPostCommitEffectiveVolumes: "SYNC", @@ -228,22 +222,6 @@ var ledgerSetups = []ledgerSetup{ execute procedure "{{.Bucket}}".insert_transaction_metadata_history(); `, }, - { - requireFeatures: features.FeatureSet{ - features.FeatureIndexTransactionAccounts: "SYNC", - }, - script: ` - create index "transactions_sources_{{.ID}}" on "{{.Bucket}}".transactions using gin (sources jsonb_path_ops) where ledger = '{{.Name}}'; - `, - }, - { - requireFeatures: features.FeatureSet{ - features.FeatureIndexTransactionAccounts: "ON", - }, - script: ` - create index "transactions_destinations_{{.ID}}" on "{{.Bucket}}".transactions using gin (destinations jsonb_path_ops) where ledger = '{{.Name}}'; - `, - }, { requireFeatures: features.FeatureSet{ features.FeatureIndexTransactionAccounts: "ON", @@ -259,22 +237,6 @@ var ledgerSetups = []ledgerSetup{ execute procedure "{{.Bucket}}".set_transaction_addresses(); `, }, - { - requireFeatures: features.FeatureSet{ - features.FeatureIndexAddressSegments: "ON", - }, - script: ` - create index "accounts_address_array_{{.ID}}" on "{{.Bucket}}".accounts using gin (address_array jsonb_ops) where ledger = '{{.Name}}'; - `, - }, - { - requireFeatures: features.FeatureSet{ - features.FeatureIndexAddressSegments: "ON", - }, - script: ` - create index "accounts_address_array_length_{{.ID}}" on "{{.Bucket}}".accounts (jsonb_array_length(address_array)) where ledger = '{{.Name}}'; - `, - }, { requireFeatures: features.FeatureSet{ features.FeatureIndexAddressSegments: "ON", @@ -290,24 +252,6 @@ var ledgerSetups = []ledgerSetup{ execute procedure "{{.Bucket}}".set_address_array_for_account(); `, }, - { - requireFeatures: features.FeatureSet{ - features.FeatureIndexAddressSegments: "ON", - features.FeatureIndexTransactionAccounts: "ON", - }, - script: ` - create index "transactions_sources_arrays_{{.ID}}" on "{{.Bucket}}".transactions using gin (sources_arrays jsonb_path_ops) where ledger = '{{.Name}}'; - `, - }, - { - requireFeatures: features.FeatureSet{ - features.FeatureIndexAddressSegments: "ON", - features.FeatureIndexTransactionAccounts: "ON", - }, - script: ` - create index "transactions_destinations_arrays_{{.ID}}" on "{{.Bucket}}".transactions using gin (destinations_arrays jsonb_path_ops) where ledger = '{{.Name}}'; - `, - }, { requireFeatures: features.FeatureSet{ features.FeatureIndexAddressSegments: "ON", diff --git a/internal/storage/bucket/migrations/24-accounts-metadata-index/notes.yaml b/internal/storage/bucket/migrations/24-accounts-metadata-index/notes.yaml new file mode 100644 index 000000000..621ded983 --- /dev/null +++ b/internal/storage/bucket/migrations/24-accounts-metadata-index/notes.yaml @@ -0,0 +1 @@ +name: Create accounts metadata index concurrently diff --git a/internal/storage/bucket/migrations/24-accounts-metadata-index/up.sql b/internal/storage/bucket/migrations/24-accounts-metadata-index/up.sql new file mode 100644 index 000000000..a8fa79bb1 --- /dev/null +++ b/internal/storage/bucket/migrations/24-accounts-metadata-index/up.sql @@ -0,0 +1 @@ +create index concurrently accounts_metadata_idx on "{{.Schema}}".accounts using gin(metadata JSONB_PATH_OPS); \ No newline at end of file From 1560f5473bebb8757358f667be31791707331b9a Mon Sep 17 00:00:00 2001 From: Ragot Geoffrey Date: Thu, 19 Dec 2024 19:46:50 +0100 Subject: [PATCH 62/71] fix: invalid benchmark scripts (#622) --- .../performance/scripts/any_bounded_to_any.js | 34 +++++++++++-------- .../scripts/any_unbounded_to_any.js | 34 +++++++++++-------- test/performance/scripts/world_to_any.js | 30 +++++++++------- test/performance/scripts/world_to_bank.js | 22 +++++++----- 4 files changed, 68 insertions(+), 52 deletions(-) diff --git a/test/performance/scripts/any_bounded_to_any.js b/test/performance/scripts/any_bounded_to_any.js index b96b24b9e..6d12de459 100644 --- a/test/performance/scripts/any_bounded_to_any.js +++ b/test/performance/scripts/any_bounded_to_any.js @@ -1,19 +1,23 @@ function next() { - return { - action: 'CREATE_TRANSACTION', - data: { - plain: `vars { - account $source - account $destination - } - send [USD/2 100] ( - source = $source allowing overdraft up to [USD/2 100] - destination = $destination - )`, - vars: { - destination: "dst:" + uuid(), - source: "src:" + uuid() + return [ + { + action: 'CREATE_TRANSACTION', + data: { + script: { + plain: `vars { + account $source + account $destination + } + send [USD/2 100] ( + source = $source allowing overdraft up to [USD/2 100] + destination = $destination + )`, + vars: { + destination: "dst:" + uuid(), + source: "src:" + uuid() + } + } } } - } + ] } \ No newline at end of file diff --git a/test/performance/scripts/any_unbounded_to_any.js b/test/performance/scripts/any_unbounded_to_any.js index ffbc02224..428d78505 100644 --- a/test/performance/scripts/any_unbounded_to_any.js +++ b/test/performance/scripts/any_unbounded_to_any.js @@ -1,19 +1,23 @@ function next() { - return { - action: 'CREATE_TRANSACTION', - data: { - plain: `vars { - account $source - account $destination - } - send [USD/2 100] ( - source = $source allowing unbounded overdraft - destination = $destination - )`, - vars: { - destination: "dst:" + uuid(), - source: "src:" + uuid() + return [ + { + action: 'CREATE_TRANSACTION', + data: { + script: { + plain: `vars { + account $source + account $destination + } + send [USD/2 100] ( + source = $source allowing unbounded overdraft + destination = $destination + )`, + vars: { + destination: "dst:" + uuid(), + source: "src:" + uuid() + } + } } } - } + ] } \ No newline at end of file diff --git a/test/performance/scripts/world_to_any.js b/test/performance/scripts/world_to_any.js index cf61ae531..504da2583 100644 --- a/test/performance/scripts/world_to_any.js +++ b/test/performance/scripts/world_to_any.js @@ -1,19 +1,23 @@ function next() { - return { - action: 'CREATE_TRANSACTION', - data: { - plain: `vars { - account $destination - } - send [USD/2 100] ( - source = @world - destination = $destination - )`, - vars: { - destination: "dst:" + uuid() + return [ + { + action: 'CREATE_TRANSACTION', + data: { + script: { + plain: `vars { + account $destination + } + send [USD/2 100] ( + source = @world + destination = $destination + )`, + vars: { + destination: "dst:" + uuid() + } + } } } - } + ] } diff --git a/test/performance/scripts/world_to_bank.js b/test/performance/scripts/world_to_bank.js index 66338267d..411439dd2 100644 --- a/test/performance/scripts/world_to_bank.js +++ b/test/performance/scripts/world_to_bank.js @@ -1,12 +1,16 @@ function next() { - return { - action: 'CREATE_TRANSACTION', - data: { - plain: `send [USD/2 100] ( - source = @world - destination = @bank - )`, - vars: {} + return [ + { + action: 'CREATE_TRANSACTION', + data: { + script: { + plain: `send [USD/2 100] ( + source = @world + destination = @bank + )`, + vars: {} + } + } } - } + ] } \ No newline at end of file From ee5e88c7bf232fda721f718363d3fc6d57a8ae99 Mon Sep 17 00:00:00 2001 From: Ragot Geoffrey Date: Thu, 19 Dec 2024 23:08:44 +0100 Subject: [PATCH 63/71] feat: improve volumes computation (#623) --- .../bucket/migrations/25-accounts-volumes-index/notes.yaml | 1 + .../storage/bucket/migrations/25-accounts-volumes-index/up.sql | 1 + pkg/generate/generator.go | 1 - 3 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 internal/storage/bucket/migrations/25-accounts-volumes-index/notes.yaml create mode 100644 internal/storage/bucket/migrations/25-accounts-volumes-index/up.sql diff --git a/internal/storage/bucket/migrations/25-accounts-volumes-index/notes.yaml b/internal/storage/bucket/migrations/25-accounts-volumes-index/notes.yaml new file mode 100644 index 000000000..b33208a91 --- /dev/null +++ b/internal/storage/bucket/migrations/25-accounts-volumes-index/notes.yaml @@ -0,0 +1 @@ +name: Create accounts volumes index concurrently diff --git a/internal/storage/bucket/migrations/25-accounts-volumes-index/up.sql b/internal/storage/bucket/migrations/25-accounts-volumes-index/up.sql new file mode 100644 index 000000000..321c2b86b --- /dev/null +++ b/internal/storage/bucket/migrations/25-accounts-volumes-index/up.sql @@ -0,0 +1 @@ +create index concurrently accounts_volumes_idx on "{{.Schema}}".accounts_volumes (ledger, accounts_address, asset) include (input, output); diff --git a/pkg/generate/generator.go b/pkg/generate/generator.go index 395e049ea..c21108487 100644 --- a/pkg/generate/generator.go +++ b/pkg/generate/generator.go @@ -219,7 +219,6 @@ func NewGenerator(script string, opts ...Option) (*Generator, error) { } err = runtime.Set("read_file", func(path string) string { - fmt.Println("read file", path) f, err := os.ReadFile(filepath.Join(cfg.rootPath, path)) if err != nil { panic(err) From b41fda386173a063b7c2cccf486dd6dd5956111c Mon Sep 17 00:00:00 2001 From: Ragot Geoffrey Date: Fri, 20 Dec 2024 13:50:05 +0100 Subject: [PATCH 64/71] feat: refactor read store (#615) * feat: improve queries performance and refactor storage read part * fix: post commit effective volumes rendering * fix: from review --- go.mod | 1 + go.sum | 2 + internal/README.md | 12 + .../bulking/mocks_ledger_controller_test.go | 18 +- .../common/mocks_ledger_controller_test.go | 18 +- internal/api/v1/controllers_accounts_count.go | 22 +- .../api/v1/controllers_accounts_count_test.go | 79 +--- internal/api/v1/controllers_accounts_list.go | 22 +- .../api/v1/controllers_accounts_list_test.go | 50 ++- internal/api/v1/controllers_accounts_read.go | 9 +- .../api/v1/controllers_accounts_read_test.go | 30 +- .../api/v1/controllers_balances_aggregates.go | 13 +- .../controllers_balances_aggregates_test.go | 42 +- internal/api/v1/controllers_balances_list.go | 23 +- internal/api/v1/controllers_logs_list.go | 33 +- internal/api/v1/controllers_logs_list_test.go | 36 +- .../api/v1/controllers_transactions_count.go | 9 +- .../v1/controllers_transactions_count_test.go | 41 +- .../api/v1/controllers_transactions_list.go | 16 +- .../v1/controllers_transactions_list_test.go | 111 ++++- .../api/v1/controllers_transactions_read.go | 16 +- .../v1/controllers_transactions_read_test.go | 5 +- .../api/v1/mocks_ledger_controller_test.go | 18 +- internal/api/v1/utils.go | 109 +++-- internal/api/v2/common.go | 193 ++++----- internal/api/v2/controllers_accounts_count.go | 4 +- .../api/v2/controllers_accounts_count_test.go | 111 ++--- internal/api/v2/controllers_accounts_list.go | 10 +- .../api/v2/controllers_accounts_list_test.go | 121 +++--- internal/api/v2/controllers_accounts_read.go | 17 +- .../api/v2/controllers_accounts_read_test.go | 20 +- internal/api/v2/controllers_balances.go | 14 +- internal/api/v2/controllers_balances_test.go | 47 +- internal/api/v2/controllers_logs_list.go | 34 +- internal/api/v2/controllers_logs_list_test.go | 79 +++- .../api/v2/controllers_transactions_count.go | 5 +- .../v2/controllers_transactions_count_test.go | 117 +++-- .../api/v2/controllers_transactions_list.go | 23 +- .../v2/controllers_transactions_list_test.go | 180 +++++--- .../api/v2/controllers_transactions_read.go | 18 +- .../v2/controllers_transactions_read_test.go | 10 +- internal/api/v2/controllers_volumes.go | 39 +- internal/api/v2/controllers_volumes_test.go | 80 ++-- .../api/v2/mocks_ledger_controller_test.go | 18 +- internal/controller/ledger/controller.go | 18 +- .../controller/ledger/controller_default.go | 67 +-- .../ledger/controller_default_test.go | 118 +++-- .../ledger/controller_generated_test.go | 18 +- .../ledger/controller_with_traces.go | 18 +- internal/controller/ledger/stats.go | 4 +- internal/controller/ledger/stats_test.go | 13 +- internal/controller/ledger/store.go | 250 ++++------- .../controller/ledger/store_generated_test.go | 328 ++++++++------ internal/storage/ledger/accounts.go | 332 +------------- internal/storage/ledger/accounts_test.go | 230 ++++++---- internal/storage/ledger/balances.go | 222 +--------- internal/storage/ledger/balances_test.go | 188 +++++--- internal/storage/ledger/debug.go | 14 +- internal/storage/ledger/errors.go | 11 - internal/storage/ledger/legacy/accounts.go | 16 +- .../storage/ledger/legacy/accounts_test.go | 92 ++-- internal/storage/ledger/legacy/adapters.go | 96 +--- internal/storage/ledger/legacy/balances.go | 3 +- .../storage/ledger/legacy/balances_test.go | 18 +- internal/storage/ledger/legacy/logs.go | 2 +- internal/storage/ledger/legacy/logs_test.go | 7 +- internal/storage/ledger/legacy/queries.go | 159 +++++++ .../storage/ledger/legacy/transactions.go | 16 +- .../ledger/legacy/transactions_test.go | 41 +- internal/storage/ledger/legacy/volumes.go | 10 +- .../storage/ledger/legacy/volumes_test.go | 143 +++--- internal/storage/ledger/logs.go | 69 +-- internal/storage/ledger/logs_test.go | 28 +- internal/storage/ledger/moves.go | 70 +-- internal/storage/ledger/moves_test.go | 22 +- internal/storage/ledger/paginator.go | 11 + internal/storage/ledger/paginator_column.go | 240 ++++++++++ internal/storage/ledger/paginator_offset.go | 79 ++++ internal/storage/ledger/resource.go | 364 ++++++++++++++++ internal/storage/ledger/resource_accounts.go | 187 ++++++++ .../ledger/resource_aggregated_balances.go | 172 ++++++++ internal/storage/ledger/resource_logs.go | 45 ++ .../storage/ledger/resource_transactions.go | 191 ++++++++ internal/storage/ledger/resource_volumes.go | 216 +++++++++ internal/storage/ledger/store.go | 150 +++---- internal/storage/ledger/transactions.go | 409 +++--------------- internal/storage/ledger/transactions_test.go | 160 ++++--- internal/storage/ledger/utils.go | 24 +- internal/storage/ledger/volumes.go | 219 +--------- internal/storage/ledger/volumes_test.go | 377 ++++++++-------- internal/volumes.go | 4 + test/e2e/api_transactions_list_test.go | 62 +-- tools/generator/go.sum | 2 + 93 files changed, 4063 insertions(+), 3347 deletions(-) delete mode 100644 internal/storage/ledger/errors.go create mode 100644 internal/storage/ledger/legacy/queries.go create mode 100644 internal/storage/ledger/paginator.go create mode 100644 internal/storage/ledger/paginator_column.go create mode 100644 internal/storage/ledger/paginator_offset.go create mode 100644 internal/storage/ledger/resource.go create mode 100644 internal/storage/ledger/resource_accounts.go create mode 100644 internal/storage/ledger/resource_aggregated_balances.go create mode 100644 internal/storage/ledger/resource_logs.go create mode 100644 internal/storage/ledger/resource_transactions.go create mode 100644 internal/storage/ledger/resource_volumes.go diff --git a/go.mod b/go.mod index ae74083e8..d321b71cd 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,7 @@ require ( github.com/shomali11/xsql v0.0.0-20190608141458-bf76292144df github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 + github.com/stoewer/go-strcase v1.3.0 github.com/stretchr/testify v1.10.0 github.com/uptrace/bun v1.2.6 github.com/uptrace/bun/dialect/pgdialect v1.2.6 diff --git a/go.sum b/go.sum index b526814ed..103fec330 100644 --- a/go.sum +++ b/go.sum @@ -316,6 +316,8 @@ github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= +github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/internal/README.md b/internal/README.md index debde2f3e..42282d7d3 100644 --- a/internal/README.md +++ b/internal/README.md @@ -15,6 +15,7 @@ import "github.com/formancehq/ledger/internal" - [func \(a Account\) GetAddress\(\) string](<#Account.GetAddress>) - [type AccountMetadata](<#AccountMetadata>) - [type AccountsVolumes](<#AccountsVolumes>) +- [type AggregatedVolumes](<#AggregatedVolumes>) - [type BalancesByAssets](<#BalancesByAssets>) - [type BalancesByAssetsByAccounts](<#BalancesByAssetsByAccounts>) - [type Configuration](<#Configuration>) @@ -207,6 +208,17 @@ type AccountsVolumes struct { } ``` +
+## type AggregatedVolumes + + + +```go +type AggregatedVolumes struct { + Aggregated VolumesByAssets `bun:"aggregated,type:jsonb"` +} +``` + ## type BalancesByAssets diff --git a/internal/api/bulking/mocks_ledger_controller_test.go b/internal/api/bulking/mocks_ledger_controller_test.go index 7786df0be..2cede2100 100644 --- a/internal/api/bulking/mocks_ledger_controller_test.go +++ b/internal/api/bulking/mocks_ledger_controller_test.go @@ -70,7 +70,7 @@ func (mr *LedgerControllerMockRecorder) Commit(ctx any) *gomock.Call { } // CountAccounts mocks base method. -func (m *LedgerController) CountAccounts(ctx context.Context, query ledger0.ListAccountsQuery) (int, error) { +func (m *LedgerController) CountAccounts(ctx context.Context, query ledger0.ResourceQuery[any]) (int, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CountAccounts", ctx, query) ret0, _ := ret[0].(int) @@ -85,7 +85,7 @@ func (mr *LedgerControllerMockRecorder) CountAccounts(ctx, query any) *gomock.Ca } // CountTransactions mocks base method. -func (m *LedgerController) CountTransactions(ctx context.Context, query ledger0.ListTransactionsQuery) (int, error) { +func (m *LedgerController) CountTransactions(ctx context.Context, query ledger0.ResourceQuery[any]) (int, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CountTransactions", ctx, query) ret0, _ := ret[0].(int) @@ -160,7 +160,7 @@ func (mr *LedgerControllerMockRecorder) Export(ctx, w any) *gomock.Call { } // GetAccount mocks base method. -func (m *LedgerController) GetAccount(ctx context.Context, query ledger0.GetAccountQuery) (*ledger.Account, error) { +func (m *LedgerController) GetAccount(ctx context.Context, query ledger0.ResourceQuery[any]) (*ledger.Account, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetAccount", ctx, query) ret0, _ := ret[0].(*ledger.Account) @@ -175,7 +175,7 @@ func (mr *LedgerControllerMockRecorder) GetAccount(ctx, query any) *gomock.Call } // GetAggregatedBalances mocks base method. -func (m *LedgerController) GetAggregatedBalances(ctx context.Context, q ledger0.GetAggregatedBalanceQuery) (ledger.BalancesByAssets, error) { +func (m *LedgerController) GetAggregatedBalances(ctx context.Context, q ledger0.ResourceQuery[ledger0.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetAggregatedBalances", ctx, q) ret0, _ := ret[0].(ledger.BalancesByAssets) @@ -220,7 +220,7 @@ func (mr *LedgerControllerMockRecorder) GetStats(ctx any) *gomock.Call { } // GetTransaction mocks base method. -func (m *LedgerController) GetTransaction(ctx context.Context, query ledger0.GetTransactionQuery) (*ledger.Transaction, error) { +func (m *LedgerController) GetTransaction(ctx context.Context, query ledger0.ResourceQuery[any]) (*ledger.Transaction, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetTransaction", ctx, query) ret0, _ := ret[0].(*ledger.Transaction) @@ -235,7 +235,7 @@ func (mr *LedgerControllerMockRecorder) GetTransaction(ctx, query any) *gomock.C } // GetVolumesWithBalances mocks base method. -func (m *LedgerController) GetVolumesWithBalances(ctx context.Context, q ledger0.GetVolumesWithBalancesQuery) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { +func (m *LedgerController) GetVolumesWithBalances(ctx context.Context, q ledger0.OffsetPaginatedQuery[ledger0.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetVolumesWithBalances", ctx, q) ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount]) @@ -279,7 +279,7 @@ func (mr *LedgerControllerMockRecorder) IsDatabaseUpToDate(ctx any) *gomock.Call } // ListAccounts mocks base method. -func (m *LedgerController) ListAccounts(ctx context.Context, query ledger0.ListAccountsQuery) (*bunpaginate.Cursor[ledger.Account], error) { +func (m *LedgerController) ListAccounts(ctx context.Context, query ledger0.OffsetPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Account], error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListAccounts", ctx, query) ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Account]) @@ -294,7 +294,7 @@ func (mr *LedgerControllerMockRecorder) ListAccounts(ctx, query any) *gomock.Cal } // ListLogs mocks base method. -func (m *LedgerController) ListLogs(ctx context.Context, query ledger0.GetLogsQuery) (*bunpaginate.Cursor[ledger.Log], error) { +func (m *LedgerController) ListLogs(ctx context.Context, query ledger0.ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Log], error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListLogs", ctx, query) ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Log]) @@ -309,7 +309,7 @@ func (mr *LedgerControllerMockRecorder) ListLogs(ctx, query any) *gomock.Call { } // ListTransactions mocks base method. -func (m *LedgerController) ListTransactions(ctx context.Context, query ledger0.ListTransactionsQuery) (*bunpaginate.Cursor[ledger.Transaction], error) { +func (m *LedgerController) ListTransactions(ctx context.Context, query ledger0.ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Transaction], error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListTransactions", ctx, query) ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Transaction]) diff --git a/internal/api/common/mocks_ledger_controller_test.go b/internal/api/common/mocks_ledger_controller_test.go index 85e72e1b8..c263cfa1f 100644 --- a/internal/api/common/mocks_ledger_controller_test.go +++ b/internal/api/common/mocks_ledger_controller_test.go @@ -70,7 +70,7 @@ func (mr *LedgerControllerMockRecorder) Commit(ctx any) *gomock.Call { } // CountAccounts mocks base method. -func (m *LedgerController) CountAccounts(ctx context.Context, query ledger0.ListAccountsQuery) (int, error) { +func (m *LedgerController) CountAccounts(ctx context.Context, query ledger0.ResourceQuery[any]) (int, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CountAccounts", ctx, query) ret0, _ := ret[0].(int) @@ -85,7 +85,7 @@ func (mr *LedgerControllerMockRecorder) CountAccounts(ctx, query any) *gomock.Ca } // CountTransactions mocks base method. -func (m *LedgerController) CountTransactions(ctx context.Context, query ledger0.ListTransactionsQuery) (int, error) { +func (m *LedgerController) CountTransactions(ctx context.Context, query ledger0.ResourceQuery[any]) (int, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CountTransactions", ctx, query) ret0, _ := ret[0].(int) @@ -160,7 +160,7 @@ func (mr *LedgerControllerMockRecorder) Export(ctx, w any) *gomock.Call { } // GetAccount mocks base method. -func (m *LedgerController) GetAccount(ctx context.Context, query ledger0.GetAccountQuery) (*ledger.Account, error) { +func (m *LedgerController) GetAccount(ctx context.Context, query ledger0.ResourceQuery[any]) (*ledger.Account, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetAccount", ctx, query) ret0, _ := ret[0].(*ledger.Account) @@ -175,7 +175,7 @@ func (mr *LedgerControllerMockRecorder) GetAccount(ctx, query any) *gomock.Call } // GetAggregatedBalances mocks base method. -func (m *LedgerController) GetAggregatedBalances(ctx context.Context, q ledger0.GetAggregatedBalanceQuery) (ledger.BalancesByAssets, error) { +func (m *LedgerController) GetAggregatedBalances(ctx context.Context, q ledger0.ResourceQuery[ledger0.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetAggregatedBalances", ctx, q) ret0, _ := ret[0].(ledger.BalancesByAssets) @@ -220,7 +220,7 @@ func (mr *LedgerControllerMockRecorder) GetStats(ctx any) *gomock.Call { } // GetTransaction mocks base method. -func (m *LedgerController) GetTransaction(ctx context.Context, query ledger0.GetTransactionQuery) (*ledger.Transaction, error) { +func (m *LedgerController) GetTransaction(ctx context.Context, query ledger0.ResourceQuery[any]) (*ledger.Transaction, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetTransaction", ctx, query) ret0, _ := ret[0].(*ledger.Transaction) @@ -235,7 +235,7 @@ func (mr *LedgerControllerMockRecorder) GetTransaction(ctx, query any) *gomock.C } // GetVolumesWithBalances mocks base method. -func (m *LedgerController) GetVolumesWithBalances(ctx context.Context, q ledger0.GetVolumesWithBalancesQuery) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { +func (m *LedgerController) GetVolumesWithBalances(ctx context.Context, q ledger0.OffsetPaginatedQuery[ledger0.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetVolumesWithBalances", ctx, q) ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount]) @@ -279,7 +279,7 @@ func (mr *LedgerControllerMockRecorder) IsDatabaseUpToDate(ctx any) *gomock.Call } // ListAccounts mocks base method. -func (m *LedgerController) ListAccounts(ctx context.Context, query ledger0.ListAccountsQuery) (*bunpaginate.Cursor[ledger.Account], error) { +func (m *LedgerController) ListAccounts(ctx context.Context, query ledger0.OffsetPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Account], error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListAccounts", ctx, query) ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Account]) @@ -294,7 +294,7 @@ func (mr *LedgerControllerMockRecorder) ListAccounts(ctx, query any) *gomock.Cal } // ListLogs mocks base method. -func (m *LedgerController) ListLogs(ctx context.Context, query ledger0.GetLogsQuery) (*bunpaginate.Cursor[ledger.Log], error) { +func (m *LedgerController) ListLogs(ctx context.Context, query ledger0.ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Log], error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListLogs", ctx, query) ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Log]) @@ -309,7 +309,7 @@ func (mr *LedgerControllerMockRecorder) ListLogs(ctx, query any) *gomock.Call { } // ListTransactions mocks base method. -func (m *LedgerController) ListTransactions(ctx context.Context, query ledger0.ListTransactionsQuery) (*bunpaginate.Cursor[ledger.Transaction], error) { +func (m *LedgerController) ListTransactions(ctx context.Context, query ledger0.ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Transaction], error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListTransactions", ctx, query) ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Transaction]) diff --git a/internal/api/v1/controllers_accounts_count.go b/internal/api/v1/controllers_accounts_count.go index 9a5bb94be..b6efe8ddd 100644 --- a/internal/api/v1/controllers_accounts_count.go +++ b/internal/api/v1/controllers_accounts_count.go @@ -6,8 +6,6 @@ import ( "errors" "github.com/formancehq/go-libs/v2/api" - "github.com/formancehq/go-libs/v2/bun/bunpaginate" - "github.com/formancehq/go-libs/v2/pointer" "github.com/formancehq/ledger/internal/api/common" ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" ) @@ -15,23 +13,19 @@ import ( func countAccounts(w http.ResponseWriter, r *http.Request) { l := common.LedgerFromContext(r.Context()) - query, err := bunpaginate.Extract[ledgercontroller.ListAccountsQuery](r, func() (*ledgercontroller.ListAccountsQuery, error) { - options, err := getPaginatedQueryOptionsOfPITFilterWithVolumes(r) - if err != nil { - return nil, err - } - options.QueryBuilder, err = buildAccountsFilterQuery(r) - if err != nil { - return nil, err - } - return pointer.For(ledgercontroller.NewListAccountsQuery(*options)), nil - }) + rq, err := getResourceQuery[any](r) + if err != nil { + api.BadRequest(w, common.ErrValidation, err) + return + } + + rq.Builder, err = buildAccountsFilterQuery(r) if err != nil { api.BadRequest(w, common.ErrValidation, err) return } - count, err := l.CountAccounts(r.Context(), *query) + count, err := l.CountAccounts(r.Context(), *rq) if err != nil { switch { case errors.Is(err, ledgercontroller.ErrInvalidQuery{}) || errors.Is(err, ledgercontroller.ErrMissingFeature{}): diff --git a/internal/api/v1/controllers_accounts_count_test.go b/internal/api/v1/controllers_accounts_count_test.go index 1ebea8349..3ce9e9416 100644 --- a/internal/api/v1/controllers_accounts_count_test.go +++ b/internal/api/v1/controllers_accounts_count_test.go @@ -5,7 +5,7 @@ import ( "net/http" "net/http/httptest" "net/url" - os "os" + "os" "testing" "errors" @@ -24,7 +24,7 @@ func TestAccountsCount(t *testing.T) { type testCase struct { name string queryParams url.Values - expectQuery ledgercontroller.PaginatedQueryOptions[ledgercontroller.PITFilterWithVolumes] + expectQuery ledgercontroller.ResourceQuery[any] expectStatusCode int expectedErrorCode string returnErr error @@ -34,13 +34,8 @@ func TestAccountsCount(t *testing.T) { testCases := []testCase{ { - name: "nominal", - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, - }, - }). - WithPageSize(DefaultPageSize), + name: "nominal", + expectQuery: ledgercontroller.ResourceQuery[any]{}, expectBackendCall: true, }, { @@ -49,33 +44,17 @@ func TestAccountsCount(t *testing.T) { "metadata[roles]": []string{"admin"}, }, expectBackendCall: true, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, - }, - }). - WithQueryBuilder(query.Match("metadata[roles]", "admin")). - WithPageSize(DefaultPageSize), + expectQuery: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("metadata[roles]", "admin"), + }, }, { name: "using address", queryParams: url.Values{"address": []string{"foo"}}, expectBackendCall: true, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, - }, - }). - WithQueryBuilder(query.Match("address", "foo")). - WithPageSize(DefaultPageSize), - }, - { - name: "invalid page size", - queryParams: url.Values{ - "pageSize": []string{"nan"}, + expectQuery: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("address", "foo"), }, - expectStatusCode: http.StatusBadRequest, - expectedErrorCode: common.ErrValidation, }, { name: "page size over maximum", @@ -83,12 +62,7 @@ func TestAccountsCount(t *testing.T) { queryParams: url.Values{ "pageSize": []string{"1000000"}, }, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, - }, - }). - WithPageSize(MaxPageSize), + expectQuery: ledgercontroller.ResourceQuery[any]{}, }, { name: "using balance filter", @@ -97,13 +71,9 @@ func TestAccountsCount(t *testing.T) { "balance": []string{"100"}, }, expectBackendCall: true, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, - }, - }). - WithQueryBuilder(query.Lt("balance", int64(100))). - WithPageSize(DefaultPageSize), + expectQuery: ledgercontroller.ResourceQuery[any]{ + Builder: query.Lt("balance", int64(100)), + }, }, { name: "with invalid query from core point of view", @@ -111,12 +81,7 @@ func TestAccountsCount(t *testing.T) { expectedErrorCode: common.ErrValidation, expectBackendCall: true, returnErr: ledgercontroller.ErrInvalidQuery{}, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, - }, - }). - WithPageSize(DefaultPageSize), + expectQuery: ledgercontroller.ResourceQuery[any]{}, }, { name: "with missing feature", @@ -124,12 +89,7 @@ func TestAccountsCount(t *testing.T) { expectedErrorCode: common.ErrValidation, expectBackendCall: true, returnErr: ledgercontroller.ErrMissingFeature{}, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, - }, - }). - WithPageSize(DefaultPageSize), + expectQuery: ledgercontroller.ResourceQuery[any]{}, }, { name: "with unexpected error", @@ -137,12 +97,7 @@ func TestAccountsCount(t *testing.T) { expectedErrorCode: api.ErrorInternal, expectBackendCall: true, returnErr: errors.New("undefined error"), - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, - }, - }). - WithPageSize(DefaultPageSize), + expectQuery: ledgercontroller.ResourceQuery[any]{}, }, } for _, testCase := range testCases { @@ -156,7 +111,7 @@ func TestAccountsCount(t *testing.T) { systemController, ledgerController := newTestingSystemController(t, true) if testCase.expectBackendCall { ledgerController.EXPECT(). - CountAccounts(gomock.Any(), ledgercontroller.NewListAccountsQuery(testCase.expectQuery)). + CountAccounts(gomock.Any(), testCase.expectQuery). Return(10, testCase.returnErr) } diff --git a/internal/api/v1/controllers_accounts_list.go b/internal/api/v1/controllers_accounts_list.go index 9c1c29bac..6b358547f 100644 --- a/internal/api/v1/controllers_accounts_list.go +++ b/internal/api/v1/controllers_accounts_list.go @@ -5,8 +5,6 @@ import ( "errors" "github.com/formancehq/go-libs/v2/api" - "github.com/formancehq/go-libs/v2/bun/bunpaginate" - "github.com/formancehq/go-libs/v2/pointer" "github.com/formancehq/ledger/internal/api/common" ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" ) @@ -14,23 +12,19 @@ import ( func listAccounts(w http.ResponseWriter, r *http.Request) { l := common.LedgerFromContext(r.Context()) - query, err := bunpaginate.Extract[ledgercontroller.ListAccountsQuery](r, func() (*ledgercontroller.ListAccountsQuery, error) { - options, err := getPaginatedQueryOptionsOfPITFilterWithVolumes(r) - if err != nil { - return nil, err - } - options.QueryBuilder, err = buildAccountsFilterQuery(r) - if err != nil { - return nil, err - } - return pointer.For(ledgercontroller.NewListAccountsQuery(*options)), nil - }) + rq, err := getOffsetPaginatedQuery[any](r) + if err != nil { + api.BadRequest(w, common.ErrValidation, err) + return + } + + rq.Options.Builder, err = buildAccountsFilterQuery(r) if err != nil { api.BadRequest(w, common.ErrValidation, err) return } - cursor, err := l.ListAccounts(r.Context(), *query) + cursor, err := l.ListAccounts(r.Context(), *rq) if err != nil { switch { case errors.Is(err, ledgercontroller.ErrMissingFeature{}): diff --git a/internal/api/v1/controllers_accounts_list_test.go b/internal/api/v1/controllers_accounts_list_test.go index 8f1677953..2784c4df0 100644 --- a/internal/api/v1/controllers_accounts_list_test.go +++ b/internal/api/v1/controllers_accounts_list_test.go @@ -25,7 +25,7 @@ func TestAccountsList(t *testing.T) { type testCase struct { name string queryParams url.Values - expectQuery ledgercontroller.PaginatedQueryOptions[ledgercontroller.PITFilterWithVolumes] + expectQuery ledgercontroller.OffsetPaginatedQuery[any] expectStatusCode int expectedErrorCode string expectBackendCall bool @@ -36,8 +36,9 @@ func TestAccountsList(t *testing.T) { { name: "nominal", expectBackendCall: true, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithPageSize(DefaultPageSize), + expectQuery: ledgercontroller.OffsetPaginatedQuery[any]{ + PageSize: DefaultPageSize, + }, }, { name: "using metadata", @@ -45,9 +46,12 @@ func TestAccountsList(t *testing.T) { "metadata[roles]": []string{"admin"}, }, expectBackendCall: true, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Match("metadata[roles]", "admin")). - WithPageSize(DefaultPageSize), + expectQuery: ledgercontroller.OffsetPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("metadata[roles]", "admin"), + }, + }, }, { name: "using address", @@ -55,17 +59,20 @@ func TestAccountsList(t *testing.T) { "address": []string{"foo"}, }, expectBackendCall: true, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Match("address", "foo")). - WithPageSize(DefaultPageSize), + expectQuery: ledgercontroller.OffsetPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("address", "foo"), + }, + }, }, { name: "using empty cursor", queryParams: url.Values{ - "cursor": []string{bunpaginate.EncodeCursor(ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{})))}, + "cursor": []string{bunpaginate.EncodeCursor(ledgercontroller.OffsetPaginatedQuery[any]{})}, }, expectBackendCall: true, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}), + expectQuery: ledgercontroller.OffsetPaginatedQuery[any]{}, }, { name: "using invalid cursor", @@ -89,8 +96,9 @@ func TestAccountsList(t *testing.T) { "pageSize": []string{"1000000"}, }, expectBackendCall: true, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithPageSize(MaxPageSize), + expectQuery: ledgercontroller.OffsetPaginatedQuery[any]{ + PageSize: MaxPageSize, + }, }, { name: "using balance filter", @@ -99,9 +107,12 @@ func TestAccountsList(t *testing.T) { "balanceOperator": []string{"e"}, }, expectBackendCall: true, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Match("balance", int64(100))). - WithPageSize(DefaultPageSize), + expectQuery: ledgercontroller.OffsetPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("balance", int64(100)), + }, + }, }, { name: "with missing feature", @@ -109,8 +120,9 @@ func TestAccountsList(t *testing.T) { expectedErrorCode: common.ErrValidation, returnErr: ledgercontroller.ErrMissingFeature{}, expectBackendCall: true, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithPageSize(DefaultPageSize), + expectQuery: ledgercontroller.OffsetPaginatedQuery[any]{ + PageSize: DefaultPageSize, + }, }, } for _, testCase := range testCases { @@ -133,7 +145,7 @@ func TestAccountsList(t *testing.T) { systemController, ledgerController := newTestingSystemController(t, true) if testCase.expectBackendCall { ledgerController.EXPECT(). - ListAccounts(gomock.Any(), ledgercontroller.NewListAccountsQuery(testCase.expectQuery)). + ListAccounts(gomock.Any(), testCase.expectQuery). Return(&expectedCursor, testCase.returnErr) } diff --git a/internal/api/v1/controllers_accounts_read.go b/internal/api/v1/controllers_accounts_read.go index 3a56a9f66..9b0ebe209 100644 --- a/internal/api/v1/controllers_accounts_read.go +++ b/internal/api/v1/controllers_accounts_read.go @@ -1,6 +1,7 @@ package v1 import ( + "github.com/formancehq/go-libs/v2/query" "net/http" "net/url" @@ -22,10 +23,10 @@ func getAccount(w http.ResponseWriter, r *http.Request) { return } - query := ledgercontroller.NewGetAccountQuery(address) - query = query.WithExpandVolumes() - - acc, err := l.GetAccount(r.Context(), query) + acc, err := l.GetAccount(r.Context(), ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("address", address), + Expand: []string{"volumes"}, + }) if err != nil { switch { case postgres.IsNotFoundError(err): diff --git a/internal/api/v1/controllers_accounts_read_test.go b/internal/api/v1/controllers_accounts_read_test.go index 027415873..1a57dc407 100644 --- a/internal/api/v1/controllers_accounts_read_test.go +++ b/internal/api/v1/controllers_accounts_read_test.go @@ -2,6 +2,7 @@ package v1 import ( "bytes" + "github.com/formancehq/go-libs/v2/query" "github.com/formancehq/ledger/internal/api/common" "net/http" "net/http/httptest" @@ -25,7 +26,7 @@ func TestAccountsRead(t *testing.T) { name string queryParams url.Values body string - expectQuery ledgercontroller.GetAccountQuery + expectQuery ledgercontroller.ResourceQuery[any] expectStatusCode int expectedErrorCode string expectBackendCall bool @@ -35,15 +36,21 @@ func TestAccountsRead(t *testing.T) { testCases := []testCase{ { - name: "nominal", - account: "foo", - expectQuery: ledgercontroller.NewGetAccountQuery("foo").WithExpandVolumes(), + name: "nominal", + account: "foo", + expectQuery: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("address", "foo"), + Expand: []string{"volumes"}, + }, expectBackendCall: true, }, { - name: "with expand volumes", - account: "foo", - expectQuery: ledgercontroller.NewGetAccountQuery("foo").WithExpandVolumes(), + name: "with expand volumes", + account: "foo", + expectQuery: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("address", "foo"), + Expand: []string{"volumes"}, + }, expectBackendCall: true, queryParams: url.Values{ "expand": {"volumes"}, @@ -56,9 +63,12 @@ func TestAccountsRead(t *testing.T) { expectedErrorCode: common.ErrValidation, }, { - name: "with not existing account", - account: "foo", - expectQuery: ledgercontroller.NewGetAccountQuery("foo").WithExpandVolumes(), + name: "with not existing account", + account: "foo", + expectQuery: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("address", "foo"), + Expand: []string{"volumes"}, + }, expectBackendCall: true, returnErr: postgres.ErrNotFound, }, diff --git a/internal/api/v1/controllers_balances_aggregates.go b/internal/api/v1/controllers_balances_aggregates.go index dd0147346..135a5791a 100644 --- a/internal/api/v1/controllers_balances_aggregates.go +++ b/internal/api/v1/controllers_balances_aggregates.go @@ -18,20 +18,19 @@ func buildAggregatedBalancesQuery(r *http.Request) query.Builder { } func getBalancesAggregated(w http.ResponseWriter, r *http.Request) { + rq, err := getResourceQuery[ledgercontroller.GetAggregatedVolumesOptions](r, func(q *ledgercontroller.GetAggregatedVolumesOptions) error { + q.UseInsertionDate = true - pitFilter, err := getPITFilter(r) + return nil + }) if err != nil { api.BadRequest(w, common.ErrValidation, err) return } - queryBuilder := buildAggregatedBalancesQuery(r) + rq.Builder = buildAggregatedBalancesQuery(r) - query := ledgercontroller.NewGetAggregatedBalancesQuery(*pitFilter, queryBuilder, - // notes(gfyrag): if pit is not specified, always use insertion date to be backward compatible - r.URL.Query().Get("pit") == "" || api.QueryParamBool(r, "useInsertionDate") || api.QueryParamBool(r, "use_insertion_date")) - - balances, err := common.LedgerFromContext(r.Context()).GetAggregatedBalances(r.Context(), query) + balances, err := common.LedgerFromContext(r.Context()).GetAggregatedBalances(r.Context(), *rq) if err != nil { common.HandleCommonErrors(w, r, err) return diff --git a/internal/api/v1/controllers_balances_aggregates_test.go b/internal/api/v1/controllers_balances_aggregates_test.go index 18e0f535a..7da73d388 100644 --- a/internal/api/v1/controllers_balances_aggregates_test.go +++ b/internal/api/v1/controllers_balances_aggregates_test.go @@ -10,8 +10,6 @@ import ( ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" - "github.com/formancehq/go-libs/v2/time" - "github.com/formancehq/go-libs/v2/api" "github.com/formancehq/go-libs/v2/auth" "github.com/formancehq/go-libs/v2/query" @@ -26,16 +24,16 @@ func TestBalancesAggregates(t *testing.T) { type testCase struct { name string queryParams url.Values - expectQuery ledgercontroller.GetAggregatedBalanceQuery + expectQuery ledgercontroller.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions] } - now := time.Now() - testCases := []testCase{ { name: "nominal", - expectQuery: ledgercontroller.GetAggregatedBalanceQuery{ - UseInsertionDate: true, + expectQuery: ledgercontroller.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{ + Opts: ledgercontroller.GetAggregatedVolumesOptions{ + UseInsertionDate: true, + }, }, }, { @@ -43,33 +41,11 @@ func TestBalancesAggregates(t *testing.T) { queryParams: url.Values{ "address": []string{"foo"}, }, - expectQuery: ledgercontroller.GetAggregatedBalanceQuery{ - QueryBuilder: query.Match("address", "foo"), - UseInsertionDate: true, - }, - }, - { - name: "using pit", - queryParams: url.Values{ - "pit": []string{now.Format(time.RFC3339Nano)}, - }, - expectQuery: ledgercontroller.GetAggregatedBalanceQuery{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &now, - }, - }, - }, - { - name: "using pit + insertion date", - queryParams: url.Values{ - "pit": []string{now.Format(time.RFC3339Nano)}, - "useInsertionDate": []string{"true"}, - }, - expectQuery: ledgercontroller.GetAggregatedBalanceQuery{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &now, + expectQuery: ledgercontroller.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{ + Opts: ledgercontroller.GetAggregatedVolumesOptions{ + UseInsertionDate: true, }, - UseInsertionDate: true, + Builder: query.Match("address", "foo"), }, }, } diff --git a/internal/api/v1/controllers_balances_list.go b/internal/api/v1/controllers_balances_list.go index fb7217e15..397e1bb93 100644 --- a/internal/api/v1/controllers_balances_list.go +++ b/internal/api/v1/controllers_balances_list.go @@ -6,31 +6,26 @@ import ( "github.com/formancehq/go-libs/v2/api" "github.com/formancehq/go-libs/v2/bun/bunpaginate" - "github.com/formancehq/go-libs/v2/pointer" "github.com/formancehq/ledger/internal/api/common" - ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" ) func getBalances(w http.ResponseWriter, r *http.Request) { l := common.LedgerFromContext(r.Context()) - q, err := bunpaginate.Extract[ledgercontroller.ListAccountsQuery](r, func() (*ledgercontroller.ListAccountsQuery, error) { - options, err := getPaginatedQueryOptionsOfPITFilterWithVolumes(r) - if err != nil { - return nil, err - } - options.QueryBuilder, err = buildAccountsFilterQuery(r) - if err != nil { - return nil, err - } - return pointer.For(ledgercontroller.NewListAccountsQuery(*options)), nil - }) + rq, err := getOffsetPaginatedQuery[any](r) + if err != nil { + api.BadRequest(w, common.ErrValidation, err) + return + } + + rq.Options.Builder, err = buildAccountsFilterQuery(r) if err != nil { api.BadRequest(w, common.ErrValidation, err) return } + rq.Options.Expand = []string{"volumes"} - cursor, err := l.ListAccounts(r.Context(), q.WithExpandVolumes()) + cursor, err := l.ListAccounts(r.Context(), *rq) if err != nil { common.HandleCommonErrors(w, r, err) return diff --git a/internal/api/v1/controllers_logs_list.go b/internal/api/v1/controllers_logs_list.go index 1fd7d1777..ca4d41ae5 100644 --- a/internal/api/v1/controllers_logs_list.go +++ b/internal/api/v1/controllers_logs_list.go @@ -1,14 +1,12 @@ package v1 import ( - "fmt" "net/http" "github.com/formancehq/go-libs/v2/api" "github.com/formancehq/go-libs/v2/bun/bunpaginate" "github.com/formancehq/go-libs/v2/query" "github.com/formancehq/ledger/internal/api/common" - ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" ) func buildGetLogsQuery(r *http.Request) query.Builder { @@ -37,32 +35,15 @@ func buildGetLogsQuery(r *http.Request) query.Builder { func getLogs(w http.ResponseWriter, r *http.Request) { l := common.LedgerFromContext(r.Context()) - query := ledgercontroller.GetLogsQuery{} - - if r.URL.Query().Get(QueryKeyCursor) != "" { - err := bunpaginate.UnmarshalCursor(r.URL.Query().Get(QueryKeyCursor), &query) - if err != nil { - api.BadRequest(w, common.ErrValidation, fmt.Errorf("invalid '%s' query param: %w", QueryKeyCursor, err)) - return - } - } else { - var err error - - pageSize, err := bunpaginate.GetPageSize(r, - bunpaginate.WithDefaultPageSize(DefaultPageSize), - bunpaginate.WithMaxPageSize(MaxPageSize)) - if err != nil { - common.HandleCommonErrors(w, r, err) - return - } - - query = ledgercontroller.NewListLogsQuery(ledgercontroller.PaginatedQueryOptions[any]{ - QueryBuilder: buildGetLogsQuery(r), - PageSize: pageSize, - }) + paginatedQuery, err := getColumnPaginatedQuery[any](r, "id", bunpaginate.OrderDesc) + if err != nil { + api.BadRequest(w, common.ErrValidation, err) + return } - cursor, err := l.ListLogs(r.Context(), query) + paginatedQuery.Options.Builder = buildGetLogsQuery(r) + + cursor, err := l.ListLogs(r.Context(), *paginatedQuery) if err != nil { common.HandleCommonErrors(w, r, err) return diff --git a/internal/api/v1/controllers_logs_list_test.go b/internal/api/v1/controllers_logs_list_test.go index 44a419535..20d381db6 100644 --- a/internal/api/v1/controllers_logs_list_test.go +++ b/internal/api/v1/controllers_logs_list_test.go @@ -2,6 +2,7 @@ package v1 import ( "encoding/json" + "github.com/formancehq/go-libs/v2/pointer" "github.com/formancehq/ledger/internal/api/common" "net/http" "net/http/httptest" @@ -26,7 +27,7 @@ func TestGetLogs(t *testing.T) { type testCase struct { name string queryParams url.Values - expectQuery ledgercontroller.PaginatedQueryOptions[any] + expectQuery ledgercontroller.ColumnPaginatedQuery[any] expectStatusCode int expectedErrorCode string } @@ -34,30 +35,47 @@ func TestGetLogs(t *testing.T) { now := time.Now() testCases := []testCase{ { - name: "nominal", - expectQuery: ledgercontroller.NewPaginatedQueryOptions[any](nil), + name: "nominal", + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Column: "id", + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + }, }, { name: "using start time", queryParams: url.Values{ "start_time": []string{now.Format(time.DateFormat)}, }, - expectQuery: ledgercontroller.NewPaginatedQueryOptions[any](nil).WithQueryBuilder(query.Gte("date", now.Format(time.DateFormat))), + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Column: "id", + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Gte("date", now.Format(time.DateFormat)), + }, + }, }, { name: "using end time", queryParams: url.Values{ "end_time": []string{now.Format(time.DateFormat)}, }, - expectQuery: ledgercontroller.NewPaginatedQueryOptions[any](nil). - WithQueryBuilder(query.Lt("date", now.Format(time.DateFormat))), + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Column: "id", + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Lt("date", now.Format(time.DateFormat)), + }, + }, }, { name: "using empty cursor", queryParams: url.Values{ - "cursor": []string{bunpaginate.EncodeCursor(ledgercontroller.NewListLogsQuery(ledgercontroller.NewPaginatedQueryOptions[any](nil)))}, + "cursor": []string{bunpaginate.EncodeCursor(ledgercontroller.ColumnPaginatedQuery[any]{})}, }, - expectQuery: ledgercontroller.NewPaginatedQueryOptions[any](nil), + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{}, }, { name: "using invalid cursor", @@ -88,7 +106,7 @@ func TestGetLogs(t *testing.T) { systemController, ledgerController := newTestingSystemController(t, true) if testCase.expectStatusCode < 300 && testCase.expectStatusCode >= 200 { ledgerController.EXPECT(). - ListLogs(gomock.Any(), ledgercontroller.NewListLogsQuery(testCase.expectQuery)). + ListLogs(gomock.Any(), testCase.expectQuery). Return(&expectedCursor, nil) } diff --git a/internal/api/v1/controllers_transactions_count.go b/internal/api/v1/controllers_transactions_count.go index a36e009f9..d67ec9f01 100644 --- a/internal/api/v1/controllers_transactions_count.go +++ b/internal/api/v1/controllers_transactions_count.go @@ -6,20 +6,17 @@ import ( "github.com/formancehq/go-libs/v2/api" "github.com/formancehq/ledger/internal/api/common" - ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" ) func countTransactions(w http.ResponseWriter, r *http.Request) { - options, err := getPaginatedQueryOptionsOfPITFilterWithVolumes(r) + rq, err := getResourceQuery[any](r) if err != nil { - api.BadRequest(w, common.ErrValidation, err) return } - options.QueryBuilder = buildGetTransactionsQuery(r) + rq.Builder = buildGetTransactionsQuery(r) - count, err := common.LedgerFromContext(r.Context()). - CountTransactions(r.Context(), ledgercontroller.NewListTransactionsQuery(*options)) + count, err := common.LedgerFromContext(r.Context()).CountTransactions(r.Context(), *rq) if err != nil { common.HandleCommonErrors(w, r, err) return diff --git a/internal/api/v1/controllers_transactions_count_test.go b/internal/api/v1/controllers_transactions_count_test.go index 1985abe49..805c611b0 100644 --- a/internal/api/v1/controllers_transactions_count_test.go +++ b/internal/api/v1/controllers_transactions_count_test.go @@ -22,7 +22,7 @@ func TestCountTransactions(t *testing.T) { type testCase struct { name string queryParams url.Values - expectQuery ledgercontroller.PaginatedQueryOptions[ledgercontroller.PITFilterWithVolumes] + expectQuery ledgercontroller.ResourceQuery[any] expectStatusCode int expectedErrorCode string } @@ -31,63 +31,70 @@ func TestCountTransactions(t *testing.T) { testCases := []testCase{ { name: "nominal", - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}), + expectQuery: ledgercontroller.ResourceQuery[any]{}, }, { name: "using metadata", queryParams: url.Values{ "metadata[roles]": []string{"admin"}, }, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Match("metadata[roles]", "admin")), + expectQuery: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("metadata[roles]", "admin"), + }, }, { name: "using startTime", queryParams: url.Values{ "start_time": []string{now.Format(time.DateFormat)}, }, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Gte("date", now.Format(time.DateFormat))), + expectQuery: ledgercontroller.ResourceQuery[any]{ + Builder: query.Gte("date", now.Format(time.DateFormat)), + }, }, { name: "using endTime", queryParams: url.Values{ "end_time": []string{now.Format(time.DateFormat)}, }, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Lt("date", now.Format(time.DateFormat))), + expectQuery: ledgercontroller.ResourceQuery[any]{ + Builder: query.Lt("date", now.Format(time.DateFormat)), + }, }, { name: "using account", queryParams: url.Values{ "account": []string{"xxx"}, }, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Match("account", "xxx")), + expectQuery: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("account", "xxx"), + }, }, { name: "using reference", queryParams: url.Values{ "reference": []string{"xxx"}, }, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Match("reference", "xxx")), + expectQuery: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("reference", "xxx"), + }, }, { name: "using destination", queryParams: url.Values{ "destination": []string{"xxx"}, }, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Match("destination", "xxx")), + expectQuery: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("destination", "xxx"), + }, }, { name: "using source", queryParams: url.Values{ "source": []string{"xxx"}, }, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Match("source", "xxx")), + expectQuery: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("source", "xxx"), + }, }, } for _, testCase := range testCases { @@ -101,7 +108,7 @@ func TestCountTransactions(t *testing.T) { systemController, ledgerController := newTestingSystemController(t, true) if testCase.expectStatusCode < 300 && testCase.expectStatusCode >= 200 { ledgerController.EXPECT(). - CountTransactions(gomock.Any(), ledgercontroller.NewListTransactionsQuery(testCase.expectQuery)). + CountTransactions(gomock.Any(), testCase.expectQuery). Return(10, nil) } diff --git a/internal/api/v1/controllers_transactions_list.go b/internal/api/v1/controllers_transactions_list.go index ae1c11fa8..bf575cb8c 100644 --- a/internal/api/v1/controllers_transactions_list.go +++ b/internal/api/v1/controllers_transactions_list.go @@ -5,29 +5,21 @@ import ( "github.com/formancehq/go-libs/v2/api" "github.com/formancehq/go-libs/v2/bun/bunpaginate" - "github.com/formancehq/go-libs/v2/pointer" "github.com/formancehq/ledger/internal/api/common" - ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" ) func listTransactions(w http.ResponseWriter, r *http.Request) { l := common.LedgerFromContext(r.Context()) - query, err := bunpaginate.Extract[ledgercontroller.ListTransactionsQuery](r, func() (*ledgercontroller.ListTransactionsQuery, error) { - options, err := getPaginatedQueryOptionsOfPITFilterWithVolumes(r) - if err != nil { - return nil, err - } - options.QueryBuilder = buildGetTransactionsQuery(r) - - return pointer.For(ledgercontroller.NewListTransactionsQuery(*options)), nil - }) + paginatedQuery, err := getColumnPaginatedQuery[any](r, "id", bunpaginate.OrderDesc) if err != nil { api.BadRequest(w, common.ErrValidation, err) return } + paginatedQuery.Options.Builder = buildGetTransactionsQuery(r) + paginatedQuery.Options.Expand = []string{"volumes"} - cursor, err := l.ListTransactions(r.Context(), *query) + cursor, err := l.ListTransactions(r.Context(), *paginatedQuery) if err != nil { common.HandleCommonErrors(w, r, err) return diff --git a/internal/api/v1/controllers_transactions_list_test.go b/internal/api/v1/controllers_transactions_list_test.go index 45bbaf2cc..e8aa5cf75 100644 --- a/internal/api/v1/controllers_transactions_list_test.go +++ b/internal/api/v1/controllers_transactions_list_test.go @@ -1,6 +1,7 @@ package v1 import ( + "github.com/formancehq/go-libs/v2/pointer" "github.com/formancehq/ledger/internal/api/common" "math/big" "net/http" @@ -26,7 +27,7 @@ func TestTransactionsList(t *testing.T) { type testCase struct { name string queryParams url.Values - expectQuery ledgercontroller.PaginatedQueryOptions[ledgercontroller.PITFilterWithVolumes] + expectQuery ledgercontroller.ColumnPaginatedQuery[any] expectStatusCode int expectedErrorCode string } @@ -34,71 +35,131 @@ func TestTransactionsList(t *testing.T) { testCases := []testCase{ { - name: "nominal", - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}), + name: "nominal", + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Column: "id", + Options: ledgercontroller.ResourceQuery[any]{ + Expand: []string{"volumes"}, + }, + }, }, { name: "using metadata", queryParams: url.Values{ "metadata[roles]": []string{"admin"}, }, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Match("metadata[roles]", "admin")), + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Column: "id", + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("metadata[roles]", "admin"), + Expand: []string{"volumes"}, + }, + }, }, { name: "using startTime", queryParams: url.Values{ "start_time": []string{now.Format(time.DateFormat)}, }, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Gte("date", now.Format(time.DateFormat))), + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Column: "id", + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Gte("date", now.Format(time.DateFormat)), + Expand: []string{"volumes"}, + }, + }, }, { name: "using endTime", queryParams: url.Values{ "end_time": []string{now.Format(time.DateFormat)}, }, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Lt("date", now.Format(time.DateFormat))), + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Column: "id", + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Lt("date", now.Format(time.DateFormat)), + Expand: []string{"volumes"}, + }, + }, }, { name: "using account", queryParams: url.Values{ "account": []string{"xxx"}, }, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Match("account", "xxx")), + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Column: "id", + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("account", "xxx"), + Expand: []string{"volumes"}, + }, + }, }, { name: "using reference", queryParams: url.Values{ "reference": []string{"xxx"}, }, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Match("reference", "xxx")), + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Column: "id", + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("reference", "xxx"), + Expand: []string{"volumes"}, + }, + }, }, { name: "using destination", queryParams: url.Values{ "destination": []string{"xxx"}, }, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Match("destination", "xxx")), + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Column: "id", + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("destination", "xxx"), + Expand: []string{"volumes"}, + }, + }, }, { name: "using source", queryParams: url.Values{ "source": []string{"xxx"}, }, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Match("source", "xxx")), + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Column: "id", + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("source", "xxx"), + Expand: []string{"volumes"}, + }, + }, }, { name: "using empty cursor", queryParams: url.Values{ - "cursor": []string{bunpaginate.EncodeCursor(ledgercontroller.NewListTransactionsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{})))}, + "cursor": []string{bunpaginate.EncodeCursor(ledgercontroller.ColumnPaginatedQuery[any]{})}, + }, + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ + Options: ledgercontroller.ResourceQuery[any]{ + Expand: []string{"volumes"}, + }, }, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}), }, { name: "using invalid cursor", @@ -121,8 +182,14 @@ func TestTransactionsList(t *testing.T) { queryParams: url.Values{ "pageSize": []string{"1000000"}, }, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithPageSize(MaxPageSize), + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: MaxPageSize, + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Column: "id", + Options: ledgercontroller.ResourceQuery[any]{ + Expand: []string{"volumes"}, + }, + }, }, } for _, testCase := range testCases { @@ -144,7 +211,7 @@ func TestTransactionsList(t *testing.T) { systemController, ledgerController := newTestingSystemController(t, true) if testCase.expectStatusCode < 300 && testCase.expectStatusCode >= 200 { ledgerController.EXPECT(). - ListTransactions(gomock.Any(), ledgercontroller.NewListTransactionsQuery(testCase.expectQuery)). + ListTransactions(gomock.Any(), testCase.expectQuery). Return(&expectedCursor, nil) } diff --git a/internal/api/v1/controllers_transactions_read.go b/internal/api/v1/controllers_transactions_read.go index d1b2dd147..b2102ed88 100644 --- a/internal/api/v1/controllers_transactions_read.go +++ b/internal/api/v1/controllers_transactions_read.go @@ -1,14 +1,13 @@ package v1 import ( + "github.com/formancehq/go-libs/v2/query" "net/http" "strconv" "github.com/formancehq/go-libs/v2/api" - "github.com/formancehq/go-libs/v2/collectionutils" "github.com/formancehq/go-libs/v2/platform/postgres" "github.com/formancehq/ledger/internal/api/common" - ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" "github.com/go-chi/chi/v5" ) @@ -21,15 +20,14 @@ func readTransaction(w http.ResponseWriter, r *http.Request) { return } - query := ledgercontroller.NewGetTransactionQuery(int(txId)) - if collectionutils.Contains(r.URL.Query()["expand"], "volumes") { - query = query.WithExpandVolumes() - } - if collectionutils.Contains(r.URL.Query()["expand"], "effectiveVolumes") { - query = query.WithExpandEffectiveVolumes() + rq, err := getResourceQuery[any](r) + if err != nil { + api.BadRequest(w, common.ErrValidation, err) + return } + rq.Builder = query.Match("id", txId) - tx, err := l.GetTransaction(r.Context(), query) + tx, err := l.GetTransaction(r.Context(), *rq) if err != nil { switch { case postgres.IsNotFoundError(err): diff --git a/internal/api/v1/controllers_transactions_read_test.go b/internal/api/v1/controllers_transactions_read_test.go index 7369ff644..d3e683773 100644 --- a/internal/api/v1/controllers_transactions_read_test.go +++ b/internal/api/v1/controllers_transactions_read_test.go @@ -1,6 +1,7 @@ package v1 import ( + "github.com/formancehq/go-libs/v2/query" "math/big" "net/http" "net/http/httptest" @@ -24,7 +25,9 @@ func TestTransactionsRead(t *testing.T) { systemController, ledgerController := newTestingSystemController(t, true) ledgerController.EXPECT(). - GetTransaction(gomock.Any(), ledgercontroller.NewGetTransactionQuery(0)). + GetTransaction(gomock.Any(), ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("id", int64(0)), + }). Return(&tx, nil) router := NewRouter(systemController, auth.NewNoAuth(), "develop", os.Getenv("DEBUG") == "true") diff --git a/internal/api/v1/mocks_ledger_controller_test.go b/internal/api/v1/mocks_ledger_controller_test.go index e619609c9..f89439826 100644 --- a/internal/api/v1/mocks_ledger_controller_test.go +++ b/internal/api/v1/mocks_ledger_controller_test.go @@ -70,7 +70,7 @@ func (mr *LedgerControllerMockRecorder) Commit(ctx any) *gomock.Call { } // CountAccounts mocks base method. -func (m *LedgerController) CountAccounts(ctx context.Context, query ledger0.ListAccountsQuery) (int, error) { +func (m *LedgerController) CountAccounts(ctx context.Context, query ledger0.ResourceQuery[any]) (int, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CountAccounts", ctx, query) ret0, _ := ret[0].(int) @@ -85,7 +85,7 @@ func (mr *LedgerControllerMockRecorder) CountAccounts(ctx, query any) *gomock.Ca } // CountTransactions mocks base method. -func (m *LedgerController) CountTransactions(ctx context.Context, query ledger0.ListTransactionsQuery) (int, error) { +func (m *LedgerController) CountTransactions(ctx context.Context, query ledger0.ResourceQuery[any]) (int, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CountTransactions", ctx, query) ret0, _ := ret[0].(int) @@ -160,7 +160,7 @@ func (mr *LedgerControllerMockRecorder) Export(ctx, w any) *gomock.Call { } // GetAccount mocks base method. -func (m *LedgerController) GetAccount(ctx context.Context, query ledger0.GetAccountQuery) (*ledger.Account, error) { +func (m *LedgerController) GetAccount(ctx context.Context, query ledger0.ResourceQuery[any]) (*ledger.Account, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetAccount", ctx, query) ret0, _ := ret[0].(*ledger.Account) @@ -175,7 +175,7 @@ func (mr *LedgerControllerMockRecorder) GetAccount(ctx, query any) *gomock.Call } // GetAggregatedBalances mocks base method. -func (m *LedgerController) GetAggregatedBalances(ctx context.Context, q ledger0.GetAggregatedBalanceQuery) (ledger.BalancesByAssets, error) { +func (m *LedgerController) GetAggregatedBalances(ctx context.Context, q ledger0.ResourceQuery[ledger0.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetAggregatedBalances", ctx, q) ret0, _ := ret[0].(ledger.BalancesByAssets) @@ -220,7 +220,7 @@ func (mr *LedgerControllerMockRecorder) GetStats(ctx any) *gomock.Call { } // GetTransaction mocks base method. -func (m *LedgerController) GetTransaction(ctx context.Context, query ledger0.GetTransactionQuery) (*ledger.Transaction, error) { +func (m *LedgerController) GetTransaction(ctx context.Context, query ledger0.ResourceQuery[any]) (*ledger.Transaction, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetTransaction", ctx, query) ret0, _ := ret[0].(*ledger.Transaction) @@ -235,7 +235,7 @@ func (mr *LedgerControllerMockRecorder) GetTransaction(ctx, query any) *gomock.C } // GetVolumesWithBalances mocks base method. -func (m *LedgerController) GetVolumesWithBalances(ctx context.Context, q ledger0.GetVolumesWithBalancesQuery) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { +func (m *LedgerController) GetVolumesWithBalances(ctx context.Context, q ledger0.OffsetPaginatedQuery[ledger0.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetVolumesWithBalances", ctx, q) ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount]) @@ -279,7 +279,7 @@ func (mr *LedgerControllerMockRecorder) IsDatabaseUpToDate(ctx any) *gomock.Call } // ListAccounts mocks base method. -func (m *LedgerController) ListAccounts(ctx context.Context, query ledger0.ListAccountsQuery) (*bunpaginate.Cursor[ledger.Account], error) { +func (m *LedgerController) ListAccounts(ctx context.Context, query ledger0.OffsetPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Account], error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListAccounts", ctx, query) ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Account]) @@ -294,7 +294,7 @@ func (mr *LedgerControllerMockRecorder) ListAccounts(ctx, query any) *gomock.Cal } // ListLogs mocks base method. -func (m *LedgerController) ListLogs(ctx context.Context, query ledger0.GetLogsQuery) (*bunpaginate.Cursor[ledger.Log], error) { +func (m *LedgerController) ListLogs(ctx context.Context, query ledger0.ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Log], error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListLogs", ctx, query) ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Log]) @@ -309,7 +309,7 @@ func (mr *LedgerControllerMockRecorder) ListLogs(ctx, query any) *gomock.Call { } // ListTransactions mocks base method. -func (m *LedgerController) ListTransactions(ctx context.Context, query ledger0.ListTransactionsQuery) (*bunpaginate.Cursor[ledger.Transaction], error) { +func (m *LedgerController) ListTransactions(ctx context.Context, query ledger0.ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Transaction], error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListTransactions", ctx, query) ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Transaction]) diff --git a/internal/api/v1/utils.go b/internal/api/v1/utils.go index f94d5cb07..c6e2d3242 100644 --- a/internal/api/v1/utils.go +++ b/internal/api/v1/utils.go @@ -6,66 +6,11 @@ import ( ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" - "github.com/formancehq/go-libs/v2/time" - "github.com/formancehq/go-libs/v2/bun/bunpaginate" - "github.com/formancehq/go-libs/v2/collectionutils" "github.com/formancehq/go-libs/v2/pointer" - "github.com/formancehq/go-libs/v2/query" ) -func getPITFilter(r *http.Request) (*ledgercontroller.PITFilter, error) { - pitString := r.URL.Query().Get("pit") - if pitString == "" { - return &ledgercontroller.PITFilter{}, nil - } - pit, err := time.ParseTime(pitString) - if err != nil { - return nil, err - } - return &ledgercontroller.PITFilter{ - PIT: &pit, - }, nil -} - -func getPITFilterWithVolumes(r *http.Request) (*ledgercontroller.PITFilterWithVolumes, error) { - pit, err := getPITFilter(r) - if err != nil { - return nil, err - } - return &ledgercontroller.PITFilterWithVolumes{ - PITFilter: *pit, - ExpandVolumes: collectionutils.Contains(r.URL.Query()["expand"], "volumes"), - ExpandEffectiveVolumes: collectionutils.Contains(r.URL.Query()["expand"], "effectiveVolumes"), - }, nil -} - -func getQueryBuilder(r *http.Request) (query.Builder, error) { - return query.ParseJSON(r.URL.Query().Get("query")) -} - -func getPaginatedQueryOptionsOfPITFilterWithVolumes(r *http.Request) (*ledgercontroller.PaginatedQueryOptions[ledgercontroller.PITFilterWithVolumes], error) { - qb, err := getQueryBuilder(r) - if err != nil { - return nil, err - } - - pitFilter, err := getPITFilterWithVolumes(r) - if err != nil { - return nil, err - } - - pageSize, err := bunpaginate.GetPageSize(r, bunpaginate.WithMaxPageSize(MaxPageSize), bunpaginate.WithDefaultPageSize(DefaultPageSize)) - if err != nil { - return nil, err - } - - return pointer.For(ledgercontroller.NewPaginatedQueryOptions(*pitFilter). - WithQueryBuilder(qb). - WithPageSize(pageSize)), nil -} - func getCommandParameters[INPUT any](r *http.Request, input INPUT) ledgercontroller.Parameters[INPUT] { dryRunAsString := r.URL.Query().Get("preview") dryRun := strings.ToUpper(dryRunAsString) == "YES" || strings.ToUpper(dryRunAsString) == "TRUE" || dryRunAsString == "1" @@ -78,3 +23,57 @@ func getCommandParameters[INPUT any](r *http.Request, input INPUT) ledgercontrol Input: input, } } + +func getOffsetPaginatedQuery[v any](r *http.Request, modifiers ...func(*v) error) (*ledgercontroller.OffsetPaginatedQuery[v], error) { + return bunpaginate.Extract[ledgercontroller.OffsetPaginatedQuery[v]](r, func() (*ledgercontroller.OffsetPaginatedQuery[v], error) { + rq, err := getResourceQuery[v](r, modifiers...) + if err != nil { + return nil, err + } + + pageSize, err := bunpaginate.GetPageSize(r, bunpaginate.WithMaxPageSize(MaxPageSize), bunpaginate.WithDefaultPageSize(DefaultPageSize)) + if err != nil { + return nil, err + } + + return &ledgercontroller.OffsetPaginatedQuery[v]{ + PageSize: pageSize, + Options: *rq, + }, nil + }) +} + +func getColumnPaginatedQuery[v any](r *http.Request, column string, order bunpaginate.Order, modifiers ...func(*v) error) (*ledgercontroller.ColumnPaginatedQuery[v], error) { + return bunpaginate.Extract[ledgercontroller.ColumnPaginatedQuery[v]](r, func() (*ledgercontroller.ColumnPaginatedQuery[v], error) { + rq, err := getResourceQuery[v](r, modifiers...) + if err != nil { + return nil, err + } + + pageSize, err := bunpaginate.GetPageSize(r, bunpaginate.WithMaxPageSize(MaxPageSize), bunpaginate.WithDefaultPageSize(DefaultPageSize)) + if err != nil { + return nil, err + } + + return &ledgercontroller.ColumnPaginatedQuery[v]{ + PageSize: pageSize, + Column: column, + Order: pointer.For(order), + Options: *rq, + }, nil + }) +} + +func getResourceQuery[v any](r *http.Request, modifiers ...func(*v) error) (*ledgercontroller.ResourceQuery[v], error) { + var options v + for _, modifier := range modifiers { + if err := modifier(&options); err != nil { + return nil, err + } + } + + return &ledgercontroller.ResourceQuery[v]{ + Expand: r.URL.Query()["expand"], + Opts: options, + }, nil +} diff --git a/internal/api/v2/common.go b/internal/api/v2/common.go index b223f1fee..6d7c94185 100644 --- a/internal/api/v2/common.go +++ b/internal/api/v2/common.go @@ -1,16 +1,12 @@ package v2 import ( + . "github.com/formancehq/go-libs/v2/collectionutils" + ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" "io" "net/http" - "slices" - "strconv" "strings" - "github.com/formancehq/go-libs/v2/api" - - ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" - "github.com/formancehq/go-libs/v2/bun/bunpaginate" "github.com/formancehq/go-libs/v2/time" @@ -18,106 +14,27 @@ import ( "github.com/formancehq/go-libs/v2/query" ) -func getPITOOTFilter(r *http.Request) (*ledgercontroller.PITFilter, error) { - pitString := r.URL.Query().Get("endTime") - ootString := r.URL.Query().Get("startTime") - - var ( - pit *time.Time - oot *time.Time - ) - - if pitString != "" { - var err error - _pit, err := time.ParseTime(pitString) - if err != nil { - return nil, err - } - - pit = &_pit - } - - if ootString != "" { - var err error - _oot, err := time.ParseTime(ootString) - if err != nil { - return nil, err - } - - oot = &_oot - } - - return &ledgercontroller.PITFilter{ - PIT: pit, - OOT: oot, - }, nil -} - -func getPITFilter(r *http.Request) (*ledgercontroller.PITFilter, error) { - pitString := r.URL.Query().Get("pit") - - var pit *time.Time - if pitString != "" { - var err error - _pit, err := time.ParseTime(pitString) - if err != nil { - return nil, err - } +func getDate(r *http.Request, key string) (*time.Time, error) { + dateString := r.URL.Query().Get(key) - pit = &_pit + if dateString == "" { + return nil, nil } - return &ledgercontroller.PITFilter{ - PIT: pit, - }, nil -} - -func getPITFilterWithVolumes(r *http.Request) (*ledgercontroller.PITFilterWithVolumes, error) { - pit, err := getPITFilter(r) + date, err := time.ParseTime(dateString) if err != nil { return nil, err } - return &ledgercontroller.PITFilterWithVolumes{ - PITFilter: *pit, - ExpandVolumes: hasExpandVolumes(r), - ExpandEffectiveVolumes: hasExpandEffectiveVolumes(r), - }, nil -} -func hasExpandVolumes(r *http.Request) bool { - parts := strings.Split(r.URL.Query().Get("expand"), ",") - return slices.Contains(parts, "volumes") + return &date, nil } -func hasExpandEffectiveVolumes(r *http.Request) bool { - parts := strings.Split(r.URL.Query().Get("expand"), ",") - return slices.Contains(parts, "effectiveVolumes") +func getPIT(r *http.Request) (*time.Time, error) { + return getDate(r, "pit") } -func getFiltersForVolumes(r *http.Request) (*ledgercontroller.FiltersForVolumes, error) { - pit, err := getPITOOTFilter(r) - if err != nil { - return nil, err - } - - useInsertionDate := api.QueryParamBool(r, "insertionDate") - groupLvl := 0 - - groupLvlStr := r.URL.Query().Get("groupBy") - if groupLvlStr != "" { - groupLvlInt, err := strconv.Atoi(groupLvlStr) - if err != nil { - return nil, err - } - if groupLvlInt > 0 { - groupLvl = groupLvlInt - } - } - return &ledgercontroller.FiltersForVolumes{ - PITFilter: *pit, - UseInsertionDate: useInsertionDate, - GroupLvl: groupLvl, - }, nil +func getOOT(r *http.Request) (*time.Time, error) { + return getDate(r, "oot") } func getQueryBuilder(r *http.Request) (query.Builder, error) { @@ -136,44 +53,80 @@ func getQueryBuilder(r *http.Request) (query.Builder, error) { return nil, nil } -func getPaginatedQueryOptionsOfPITFilterWithVolumes(r *http.Request) (*ledgercontroller.PaginatedQueryOptions[ledgercontroller.PITFilterWithVolumes], error) { - qb, err := getQueryBuilder(r) - if err != nil { - return nil, err - } +func getExpand(r *http.Request) []string { + return Flatten( + Map(r.URL.Query()["expand"], func(from string) []string { + return strings.Split(from, ",") + }), + ) +} - pitFilter, err := getPITFilterWithVolumes(r) - if err != nil { - return nil, err - } +func getOffsetPaginatedQuery[v any](r *http.Request, modifiers ...func(*v) error) (*ledgercontroller.OffsetPaginatedQuery[v], error) { + return bunpaginate.Extract[ledgercontroller.OffsetPaginatedQuery[v]](r, func() (*ledgercontroller.OffsetPaginatedQuery[v], error) { + rq, err := getResourceQuery[v](r, modifiers...) + if err != nil { + return nil, err + } - pageSize, err := bunpaginate.GetPageSize(r) - if err != nil { - return nil, err - } + pageSize, err := bunpaginate.GetPageSize(r, bunpaginate.WithMaxPageSize(MaxPageSize), bunpaginate.WithDefaultPageSize(DefaultPageSize)) + if err != nil { + return nil, err + } - return pointer.For(ledgercontroller.NewPaginatedQueryOptions(*pitFilter). - WithQueryBuilder(qb). - WithPageSize(pageSize)), nil + return &ledgercontroller.OffsetPaginatedQuery[v]{ + PageSize: pageSize, + Options: *rq, + }, nil + }) } -func getPaginatedQueryOptionsOfFiltersForVolumes(r *http.Request) (*ledgercontroller.PaginatedQueryOptions[ledgercontroller.FiltersForVolumes], error) { - qb, err := getQueryBuilder(r) +func getColumnPaginatedQuery[v any](r *http.Request, defaultPaginationColumn string, order bunpaginate.Order, modifiers ...func(*v) error) (*ledgercontroller.ColumnPaginatedQuery[v], error) { + return bunpaginate.Extract[ledgercontroller.ColumnPaginatedQuery[v]](r, func() (*ledgercontroller.ColumnPaginatedQuery[v], error) { + rq, err := getResourceQuery[v](r, modifiers...) + if err != nil { + return nil, err + } + + pageSize, err := bunpaginate.GetPageSize(r, bunpaginate.WithMaxPageSize(MaxPageSize), bunpaginate.WithDefaultPageSize(DefaultPageSize)) + if err != nil { + return nil, err + } + + return &ledgercontroller.ColumnPaginatedQuery[v]{ + PageSize: pageSize, + Column: defaultPaginationColumn, + Order: pointer.For(order), + Options: *rq, + }, nil + }) +} + +func getResourceQuery[v any](r *http.Request, modifiers ...func(*v) error) (*ledgercontroller.ResourceQuery[v], error) { + pit, err := getPIT(r) if err != nil { return nil, err } - - filtersForVolumes, err := getFiltersForVolumes(r) + oot, err := getOOT(r) if err != nil { return nil, err } - - pageSize, err := bunpaginate.GetPageSize(r) + builder, err := getQueryBuilder(r) if err != nil { return nil, err } - return pointer.For(ledgercontroller.NewPaginatedQueryOptions(*filtersForVolumes). - WithPageSize(pageSize). - WithQueryBuilder(qb)), nil + var options v + for _, modifier := range modifiers { + if err := modifier(&options); err != nil { + return nil, err + } + } + + return &ledgercontroller.ResourceQuery[v]{ + PIT: pit, + OOT: oot, + Builder: builder, + Expand: getExpand(r), + Opts: options, + }, nil } diff --git a/internal/api/v2/controllers_accounts_count.go b/internal/api/v2/controllers_accounts_count.go index 61d26cd61..c8346ac08 100644 --- a/internal/api/v2/controllers_accounts_count.go +++ b/internal/api/v2/controllers_accounts_count.go @@ -13,13 +13,13 @@ import ( func countAccounts(w http.ResponseWriter, r *http.Request) { l := common.LedgerFromContext(r.Context()) - options, err := getPaginatedQueryOptionsOfPITFilterWithVolumes(r) + rq, err := getResourceQuery[any](r) if err != nil { api.BadRequest(w, common.ErrValidation, err) return } - count, err := l.CountAccounts(r.Context(), ledgercontroller.NewListAccountsQuery(*options)) + count, err := l.CountAccounts(r.Context(), *rq) if err != nil { switch { case errors.Is(err, ledgercontroller.ErrInvalidQuery{}) || errors.Is(err, ledgercontroller.ErrMissingFeature{}): diff --git a/internal/api/v2/controllers_accounts_count_test.go b/internal/api/v2/controllers_accounts_count_test.go index 8e04ac775..151f66451 100644 --- a/internal/api/v2/controllers_accounts_count_test.go +++ b/internal/api/v2/controllers_accounts_count_test.go @@ -26,7 +26,7 @@ func TestAccountsCount(t *testing.T) { name string queryParams url.Values body string - expectQuery ledgercontroller.PaginatedQueryOptions[ledgercontroller.PITFilterWithVolumes] + expectQuery ledgercontroller.ResourceQuery[any] expectStatusCode int expectedErrorCode string returnErr error @@ -37,82 +37,51 @@ func TestAccountsCount(t *testing.T) { testCases := []testCase{ { name: "nominal", - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, - }, - }). - WithPageSize(DefaultPageSize), + expectQuery: ledgercontroller.ResourceQuery[any]{ + PIT: &before, + Expand: make([]string, 0), + }, expectBackendCall: true, }, { name: "using metadata", body: `{"$match": { "metadata[roles]": "admin" }}`, expectBackendCall: true, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, - }, - }). - WithQueryBuilder(query.Match("metadata[roles]", "admin")). - WithPageSize(DefaultPageSize), + expectQuery: ledgercontroller.ResourceQuery[any]{ + PIT: &before, + Builder: query.Match("metadata[roles]", "admin"), + Expand: make([]string, 0), + }, }, { name: "using address", body: `{"$match": { "address": "foo" }}`, expectBackendCall: true, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, - }, - }). - WithQueryBuilder(query.Match("address", "foo")). - WithPageSize(DefaultPageSize), - }, - { - name: "invalid page size", - queryParams: url.Values{ - "pageSize": []string{"nan"}, + expectQuery: ledgercontroller.ResourceQuery[any]{ + PIT: &before, + Builder: query.Match("address", "foo"), + Expand: make([]string, 0), }, - expectStatusCode: http.StatusBadRequest, - expectedErrorCode: common.ErrValidation, - }, - { - name: "page size over maximum", - expectBackendCall: true, - queryParams: url.Values{ - "pageSize": []string{"1000000"}, - }, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, - }, - }). - WithPageSize(MaxPageSize), }, { name: "using balance filter", body: `{"$lt": { "balance[USD/2]": 100 }}`, expectBackendCall: true, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, - }, - }). - WithQueryBuilder(query.Lt("balance[USD/2]", float64(100))). - WithPageSize(DefaultPageSize), + expectQuery: ledgercontroller.ResourceQuery[any]{ + PIT: &before, + Builder: query.Lt("balance[USD/2]", float64(100)), + Expand: make([]string, 0), + }, }, { name: "using exists filter", body: `{"$exists": { "metadata": "foo" }}`, expectBackendCall: true, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, - }, - }). - WithQueryBuilder(query.Exists("metadata", "foo")). - WithPageSize(DefaultPageSize), + expectQuery: ledgercontroller.ResourceQuery[any]{ + PIT: &before, + Builder: query.Exists("metadata", "foo"), + Expand: make([]string, 0), + }, }, { name: "using invalid query payload", @@ -126,12 +95,10 @@ func TestAccountsCount(t *testing.T) { expectedErrorCode: common.ErrValidation, expectBackendCall: true, returnErr: ledgercontroller.ErrInvalidQuery{}, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, - }, - }). - WithPageSize(DefaultPageSize), + expectQuery: ledgercontroller.ResourceQuery[any]{ + PIT: &before, + Expand: make([]string, 0), + }, }, { name: "with missing feature", @@ -139,12 +106,10 @@ func TestAccountsCount(t *testing.T) { expectedErrorCode: common.ErrValidation, expectBackendCall: true, returnErr: ledgercontroller.ErrMissingFeature{}, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, - }, - }). - WithPageSize(DefaultPageSize), + expectQuery: ledgercontroller.ResourceQuery[any]{ + PIT: &before, + Expand: make([]string, 0), + }, }, { name: "with unexpected error", @@ -152,12 +117,10 @@ func TestAccountsCount(t *testing.T) { expectedErrorCode: api.ErrorInternal, expectBackendCall: true, returnErr: errors.New("undefined error"), - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, - }, - }). - WithPageSize(DefaultPageSize), + expectQuery: ledgercontroller.ResourceQuery[any]{ + PIT: &before, + Expand: make([]string, 0), + }, }, } for _, testCase := range testCases { @@ -171,7 +134,7 @@ func TestAccountsCount(t *testing.T) { systemController, ledgerController := newTestingSystemController(t, true) if testCase.expectBackendCall { ledgerController.EXPECT(). - CountAccounts(gomock.Any(), ledgercontroller.NewListAccountsQuery(testCase.expectQuery)). + CountAccounts(gomock.Any(), testCase.expectQuery). Return(10, testCase.returnErr) } diff --git a/internal/api/v2/controllers_accounts_list.go b/internal/api/v2/controllers_accounts_list.go index 88b3322a0..0c1e171fd 100644 --- a/internal/api/v2/controllers_accounts_list.go +++ b/internal/api/v2/controllers_accounts_list.go @@ -5,8 +5,6 @@ import ( "errors" "github.com/formancehq/go-libs/v2/api" - "github.com/formancehq/go-libs/v2/bun/bunpaginate" - "github.com/formancehq/go-libs/v2/pointer" "github.com/formancehq/ledger/internal/api/common" ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" ) @@ -14,13 +12,7 @@ import ( func listAccounts(w http.ResponseWriter, r *http.Request) { l := common.LedgerFromContext(r.Context()) - query, err := bunpaginate.Extract[ledgercontroller.ListAccountsQuery](r, func() (*ledgercontroller.ListAccountsQuery, error) { - options, err := getPaginatedQueryOptionsOfPITFilterWithVolumes(r) - if err != nil { - return nil, err - } - return pointer.For(ledgercontroller.NewListAccountsQuery(*options)), nil - }) + query, err := getOffsetPaginatedQuery[any](r) if err != nil { api.BadRequest(w, common.ErrValidation, err) return diff --git a/internal/api/v2/controllers_accounts_list_test.go b/internal/api/v2/controllers_accounts_list_test.go index b2c81a63b..3dff1b1de 100644 --- a/internal/api/v2/controllers_accounts_list_test.go +++ b/internal/api/v2/controllers_accounts_list_test.go @@ -29,7 +29,7 @@ func TestAccountsList(t *testing.T) { name string queryParams url.Values body string - expectQuery ledgercontroller.PaginatedQueryOptions[ledgercontroller.PITFilterWithVolumes] + expectQuery ledgercontroller.OffsetPaginatedQuery[any] expectStatusCode int expectedErrorCode string expectBackendCall bool @@ -40,45 +40,54 @@ func TestAccountsList(t *testing.T) { testCases := []testCase{ { name: "nominal", - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, + expectQuery: ledgercontroller.OffsetPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Options: ledgercontroller.ResourceQuery[any]{ + PIT: &before, + Expand: make([]string, 0), }, - }). - WithPageSize(DefaultPageSize), + }, expectBackendCall: true, }, { name: "using metadata", body: `{"$match": { "metadata[roles]": "admin" }}`, expectBackendCall: true, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, + expectQuery: ledgercontroller.OffsetPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Options: ledgercontroller.ResourceQuery[any]{ + PIT: &before, + Builder: query.Match("metadata[roles]", "admin"), + Expand: make([]string, 0), }, - }). - WithQueryBuilder(query.Match("metadata[roles]", "admin")). - WithPageSize(DefaultPageSize), + }, }, { name: "using address", body: `{"$match": { "address": "foo" }}`, expectBackendCall: true, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, + expectQuery: ledgercontroller.OffsetPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Options: ledgercontroller.ResourceQuery[any]{ + PIT: &before, + Builder: query.Match("address", "foo"), + Expand: make([]string, 0), }, - }). - WithQueryBuilder(query.Match("address", "foo")). - WithPageSize(DefaultPageSize), + }, }, { name: "using empty cursor", expectBackendCall: true, queryParams: url.Values{ - "cursor": []string{bunpaginate.EncodeCursor(ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{})))}, + "cursor": []string{bunpaginate.EncodeCursor(ledgercontroller.OffsetPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Options: ledgercontroller.ResourceQuery[any]{}, + })}, + }, + expectQuery: ledgercontroller.OffsetPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Options: ledgercontroller.ResourceQuery[any]{}, }, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}), }, { name: "using invalid cursor", @@ -102,36 +111,39 @@ func TestAccountsList(t *testing.T) { queryParams: url.Values{ "pageSize": []string{"1000000"}, }, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, + expectQuery: ledgercontroller.OffsetPaginatedQuery[any]{ + PageSize: MaxPageSize, + Options: ledgercontroller.ResourceQuery[any]{ + PIT: &before, + Expand: make([]string, 0), }, - }). - WithPageSize(MaxPageSize), + }, }, { name: "using balance filter", expectBackendCall: true, body: `{"$lt": { "balance[USD/2]": 100 }}`, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, + expectQuery: ledgercontroller.OffsetPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Options: ledgercontroller.ResourceQuery[any]{ + PIT: &before, + Builder: query.Lt("balance[USD/2]", float64(100)), + Expand: make([]string, 0), }, - }). - WithQueryBuilder(query.Lt("balance[USD/2]", float64(100))). - WithPageSize(DefaultPageSize), + }, }, { name: "using exists filter", expectBackendCall: true, body: `{"$exists": { "metadata": "foo" }}`, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, + expectQuery: ledgercontroller.OffsetPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Options: ledgercontroller.ResourceQuery[any]{ + PIT: &before, + Builder: query.Exists("metadata", "foo"), + Expand: make([]string, 0), }, - }). - WithQueryBuilder(query.Exists("metadata", "foo")). - WithPageSize(DefaultPageSize), + }, }, { name: "using invalid query payload", @@ -145,12 +157,13 @@ func TestAccountsList(t *testing.T) { expectedErrorCode: common.ErrValidation, expectBackendCall: true, returnErr: ledgercontroller.ErrInvalidQuery{}, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, + expectQuery: ledgercontroller.OffsetPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Options: ledgercontroller.ResourceQuery[any]{ + PIT: &before, + Expand: make([]string, 0), }, - }). - WithPageSize(DefaultPageSize), + }, }, { name: "with missing feature", @@ -158,12 +171,13 @@ func TestAccountsList(t *testing.T) { expectedErrorCode: common.ErrValidation, expectBackendCall: true, returnErr: ledgercontroller.ErrMissingFeature{}, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, + expectQuery: ledgercontroller.OffsetPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Options: ledgercontroller.ResourceQuery[any]{ + PIT: &before, + Expand: make([]string, 0), }, - }). - WithPageSize(DefaultPageSize), + }, }, { name: "with unexpected error", @@ -171,12 +185,13 @@ func TestAccountsList(t *testing.T) { expectedErrorCode: api.ErrorInternal, expectBackendCall: true, returnErr: errors.New("undefined error"), - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, + expectQuery: ledgercontroller.OffsetPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Options: ledgercontroller.ResourceQuery[any]{ + PIT: &before, + Expand: make([]string, 0), }, - }). - WithPageSize(DefaultPageSize), + }, }, } for _, testCase := range testCases { @@ -199,7 +214,7 @@ func TestAccountsList(t *testing.T) { systemController, ledgerController := newTestingSystemController(t, true) if tc.expectBackendCall { ledgerController.EXPECT(). - ListAccounts(gomock.Any(), ledgercontroller.NewListAccountsQuery(tc.expectQuery)). + ListAccounts(gomock.Any(), tc.expectQuery). Return(&expectedCursor, tc.returnErr) } diff --git a/internal/api/v2/controllers_accounts_read.go b/internal/api/v2/controllers_accounts_read.go index cb9f21672..e063138ad 100644 --- a/internal/api/v2/controllers_accounts_read.go +++ b/internal/api/v2/controllers_accounts_read.go @@ -1,6 +1,7 @@ package v2 import ( + "github.com/formancehq/go-libs/v2/query" "net/http" "net/url" @@ -20,21 +21,17 @@ func readAccount(w http.ResponseWriter, r *http.Request) { return } - query := ledgercontroller.NewGetAccountQuery(param) - if hasExpandVolumes(r) { - query = query.WithExpandVolumes() - } - if hasExpandEffectiveVolumes(r) { - query = query.WithExpandEffectiveVolumes() - } - pitFilter, err := getPITFilter(r) + pit, err := getPIT(r) if err != nil { api.BadRequest(w, common.ErrValidation, err) return } - query.PITFilter = *pitFilter - acc, err := l.GetAccount(r.Context(), query) + acc, err := l.GetAccount(r.Context(), ledgercontroller.ResourceQuery[any]{ + PIT: pit, + Builder: query.Match("address", param), + Expand: r.URL.Query()["expand"], + }) if err != nil { switch { case postgres.IsNotFoundError(err): diff --git a/internal/api/v2/controllers_accounts_read_test.go b/internal/api/v2/controllers_accounts_read_test.go index 8ab40ccc2..259d550fd 100644 --- a/internal/api/v2/controllers_accounts_read_test.go +++ b/internal/api/v2/controllers_accounts_read_test.go @@ -2,6 +2,7 @@ package v2 import ( "bytes" + "github.com/formancehq/go-libs/v2/query" "github.com/formancehq/ledger/internal/api/common" "net/http" "net/http/httptest" @@ -25,7 +26,7 @@ func TestAccountsRead(t *testing.T) { name string queryParams url.Values body string - expectQuery ledgercontroller.GetAccountQuery + expectQuery ledgercontroller.ResourceQuery[any] expectStatusCode int expectedErrorCode string expectBackendCall bool @@ -38,13 +39,20 @@ func TestAccountsRead(t *testing.T) { { name: "nominal", account: "foo", - expectQuery: ledgercontroller.NewGetAccountQuery("foo").WithPIT(before), + expectQuery: ledgercontroller.ResourceQuery[any]{ + PIT: &before, + Builder: query.Match("address", "foo"), + }, expectBackendCall: true, }, { name: "with expand volumes", account: "foo", - expectQuery: ledgercontroller.NewGetAccountQuery("foo").WithPIT(before).WithExpandVolumes(), + expectQuery: ledgercontroller.ResourceQuery[any]{ + PIT: &before, + Builder: query.Match("address", "foo"), + Expand: []string{"volumes"}, + }, expectBackendCall: true, queryParams: url.Values{ "expand": {"volumes"}, @@ -53,7 +61,11 @@ func TestAccountsRead(t *testing.T) { { name: "with expand effective volumes", account: "foo", - expectQuery: ledgercontroller.NewGetAccountQuery("foo").WithPIT(before).WithExpandEffectiveVolumes(), + expectQuery: ledgercontroller.ResourceQuery[any]{ + PIT: &before, + Builder: query.Match("address", "foo"), + Expand: []string{"effectiveVolumes"}, + }, expectBackendCall: true, queryParams: url.Values{ "expand": {"effectiveVolumes"}, diff --git a/internal/api/v2/controllers_balances.go b/internal/api/v2/controllers_balances.go index a63646f38..4d094352b 100644 --- a/internal/api/v2/controllers_balances.go +++ b/internal/api/v2/controllers_balances.go @@ -12,21 +12,17 @@ import ( func readBalancesAggregated(w http.ResponseWriter, r *http.Request) { - pitFilter, err := getPITFilter(r) - if err != nil { - api.BadRequest(w, common.ErrValidation, err) - return - } + rq, err := getResourceQuery[ledgercontroller.GetAggregatedVolumesOptions](r, func(options *ledgercontroller.GetAggregatedVolumesOptions) error { + options.UseInsertionDate = api.QueryParamBool(r, "use_insertion_date") || api.QueryParamBool(r, "useInsertionDate") - queryBuilder, err := getQueryBuilder(r) + return nil + }) if err != nil { api.BadRequest(w, common.ErrValidation, err) return } - balances, err := common.LedgerFromContext(r.Context()). - GetAggregatedBalances(r.Context(), ledgercontroller.NewGetAggregatedBalancesQuery( - *pitFilter, queryBuilder, api.QueryParamBool(r, "use_insertion_date") || api.QueryParamBool(r, "useInsertionDate"))) + balances, err := common.LedgerFromContext(r.Context()).GetAggregatedBalances(r.Context(), *rq) if err != nil { switch { case errors.Is(err, ledgercontroller.ErrInvalidQuery{}) || errors.Is(err, ledgercontroller.ErrMissingFeature{}): diff --git a/internal/api/v2/controllers_balances_test.go b/internal/api/v2/controllers_balances_test.go index 5d1de3a8b..1428bbd77 100644 --- a/internal/api/v2/controllers_balances_test.go +++ b/internal/api/v2/controllers_balances_test.go @@ -28,7 +28,7 @@ func TestBalancesAggregates(t *testing.T) { name string queryParams url.Values body string - expectQuery ledgercontroller.GetAggregatedBalanceQuery + expectQuery ledgercontroller.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions] } now := time.Now() @@ -36,30 +36,30 @@ func TestBalancesAggregates(t *testing.T) { testCases := []testCase{ { name: "nominal", - expectQuery: ledgercontroller.GetAggregatedBalanceQuery{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &now, - }, + expectQuery: ledgercontroller.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{ + Opts: ledgercontroller.GetAggregatedVolumesOptions{}, + PIT: &now, + Expand: make([]string, 0), }, }, { name: "using address", body: `{"$match": {"address": "foo"}}`, - expectQuery: ledgercontroller.GetAggregatedBalanceQuery{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &now, - }, - QueryBuilder: query.Match("address", "foo"), + expectQuery: ledgercontroller.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{ + Opts: ledgercontroller.GetAggregatedVolumesOptions{}, + PIT: &now, + Builder: query.Match("address", "foo"), + Expand: make([]string, 0), }, }, { name: "using exists metadata filter", body: `{"$exists": {"metadata": "foo"}}`, - expectQuery: ledgercontroller.GetAggregatedBalanceQuery{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &now, - }, - QueryBuilder: query.Exists("metadata", "foo"), + expectQuery: ledgercontroller.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{ + Opts: ledgercontroller.GetAggregatedVolumesOptions{}, + PIT: &now, + Builder: query.Exists("metadata", "foo"), + Expand: make([]string, 0), }, }, { @@ -67,10 +67,10 @@ func TestBalancesAggregates(t *testing.T) { queryParams: url.Values{ "pit": []string{now.Format(time.RFC3339Nano)}, }, - expectQuery: ledgercontroller.GetAggregatedBalanceQuery{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &now, - }, + expectQuery: ledgercontroller.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{ + Opts: ledgercontroller.GetAggregatedVolumesOptions{}, + PIT: &now, + Expand: make([]string, 0), }, }, { @@ -79,11 +79,12 @@ func TestBalancesAggregates(t *testing.T) { "pit": []string{now.Format(time.RFC3339Nano)}, "useInsertionDate": []string{"true"}, }, - expectQuery: ledgercontroller.GetAggregatedBalanceQuery{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &now, + expectQuery: ledgercontroller.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{ + Opts: ledgercontroller.GetAggregatedVolumesOptions{ + UseInsertionDate: true, }, - UseInsertionDate: true, + PIT: &now, + Expand: make([]string, 0), }, }, } diff --git a/internal/api/v2/controllers_logs_list.go b/internal/api/v2/controllers_logs_list.go index 231c0f278..56c82236a 100644 --- a/internal/api/v2/controllers_logs_list.go +++ b/internal/api/v2/controllers_logs_list.go @@ -1,7 +1,6 @@ package v2 import ( - "fmt" "net/http" "errors" @@ -15,36 +14,13 @@ import ( func listLogs(w http.ResponseWriter, r *http.Request) { l := common.LedgerFromContext(r.Context()) - query := ledgercontroller.GetLogsQuery{} - - if r.URL.Query().Get(QueryKeyCursor) != "" { - err := bunpaginate.UnmarshalCursor(r.URL.Query().Get(QueryKeyCursor), &query) - if err != nil { - api.BadRequest(w, common.ErrValidation, fmt.Errorf("invalid '%s' query param", QueryKeyCursor)) - return - } - } else { - var err error - - pageSize, err := bunpaginate.GetPageSize(r) - if err != nil { - api.BadRequest(w, common.ErrValidation, err) - return - } - - qb, err := getQueryBuilder(r) - if err != nil { - api.BadRequest(w, common.ErrValidation, err) - return - } - - query = ledgercontroller.NewListLogsQuery(ledgercontroller.PaginatedQueryOptions[any]{ - QueryBuilder: qb, - PageSize: pageSize, - }) + rq, err := getColumnPaginatedQuery[any](r, "id", bunpaginate.OrderDesc) + if err != nil { + api.BadRequest(w, common.ErrValidation, err) + return } - cursor, err := l.ListLogs(r.Context(), query) + cursor, err := l.ListLogs(r.Context(), *rq) if err != nil { switch { case errors.Is(err, ledgercontroller.ErrInvalidQuery{}): diff --git a/internal/api/v2/controllers_logs_list_test.go b/internal/api/v2/controllers_logs_list_test.go index c06db4189..2e931f7f3 100644 --- a/internal/api/v2/controllers_logs_list_test.go +++ b/internal/api/v2/controllers_logs_list_test.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "github.com/formancehq/go-libs/v2/pointer" "github.com/formancehq/ledger/internal/api/common" "net/http" "net/http/httptest" @@ -30,7 +31,7 @@ func TestGetLogs(t *testing.T) { name string queryParams url.Values body string - expectQuery ledgercontroller.PaginatedQueryOptions[any] + expectQuery ledgercontroller.ColumnPaginatedQuery[any] expectStatusCode int expectedErrorCode string expectBackendCall bool @@ -40,29 +41,59 @@ func TestGetLogs(t *testing.T) { now := time.Now() testCases := []testCase{ { - name: "nominal", - expectQuery: ledgercontroller.NewPaginatedQueryOptions[any](nil), + name: "nominal", + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Column: "id", + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Options: ledgercontroller.ResourceQuery[any]{ + Expand: make([]string, 0), + }, + }, expectBackendCall: true, }, { - name: "using start time", - body: fmt.Sprintf(`{"$gte": {"date": "%s"}}`, now.Format(time.DateFormat)), - expectQuery: ledgercontroller.NewPaginatedQueryOptions[any](nil).WithQueryBuilder(query.Gte("date", now.Format(time.DateFormat))), + name: "using start time", + body: fmt.Sprintf(`{"$gte": {"date": "%s"}}`, now.Format(time.DateFormat)), + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Column: "id", + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Gte("date", now.Format(time.DateFormat)), + Expand: make([]string, 0), + }, + }, expectBackendCall: true, }, { name: "using end time", body: fmt.Sprintf(`{"$lt": {"date": "%s"}}`, now.Format(time.DateFormat)), - expectQuery: ledgercontroller.NewPaginatedQueryOptions[any](nil). - WithQueryBuilder(query.Lt("date", now.Format(time.DateFormat))), + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Column: "id", + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Lt("date", now.Format(time.DateFormat)), + Expand: make([]string, 0), + }, + }, expectBackendCall: true, }, { name: "using empty cursor", queryParams: url.Values{ - "cursor": []string{bunpaginate.EncodeCursor(ledgercontroller.NewListLogsQuery(ledgercontroller.NewPaginatedQueryOptions[any](nil)))}, + "cursor": []string{bunpaginate.EncodeCursor(ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Column: "id", + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + })}, + }, + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Column: "id", + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), }, - expectQuery: ledgercontroller.NewPaginatedQueryOptions[any](nil), expectBackendCall: true, }, { @@ -88,17 +119,31 @@ func TestGetLogs(t *testing.T) { expectedErrorCode: common.ErrValidation, }, { - name: "with invalid query", - expectStatusCode: http.StatusBadRequest, - expectQuery: ledgercontroller.NewPaginatedQueryOptions[any](nil), + name: "with invalid query", + expectStatusCode: http.StatusBadRequest, + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Column: "id", + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Options: ledgercontroller.ResourceQuery[any]{ + Expand: make([]string, 0), + }, + }, expectedErrorCode: common.ErrValidation, expectBackendCall: true, returnErr: ledgercontroller.ErrInvalidQuery{}, }, { - name: "with unexpected error", - expectStatusCode: http.StatusInternalServerError, - expectQuery: ledgercontroller.NewPaginatedQueryOptions[any](nil), + name: "with unexpected error", + expectStatusCode: http.StatusInternalServerError, + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Column: "id", + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Options: ledgercontroller.ResourceQuery[any]{ + Expand: make([]string, 0), + }, + }, expectedErrorCode: api.ErrorInternal, expectBackendCall: true, returnErr: errors.New("unexpected error"), @@ -125,7 +170,7 @@ func TestGetLogs(t *testing.T) { systemController, ledgerController := newTestingSystemController(t, true) if testCase.expectBackendCall { ledgerController.EXPECT(). - ListLogs(gomock.Any(), ledgercontroller.NewListLogsQuery(testCase.expectQuery)). + ListLogs(gomock.Any(), testCase.expectQuery). Return(&expectedCursor, testCase.returnErr) } diff --git a/internal/api/v2/controllers_transactions_count.go b/internal/api/v2/controllers_transactions_count.go index 3388d07fc..75f50bc37 100644 --- a/internal/api/v2/controllers_transactions_count.go +++ b/internal/api/v2/controllers_transactions_count.go @@ -12,14 +12,13 @@ import ( func countTransactions(w http.ResponseWriter, r *http.Request) { - options, err := getPaginatedQueryOptionsOfPITFilterWithVolumes(r) + rq, err := getResourceQuery[any](r) if err != nil { api.BadRequest(w, common.ErrValidation, err) return } - count, err := common.LedgerFromContext(r.Context()). - CountTransactions(r.Context(), ledgercontroller.NewListTransactionsQuery(*options)) + count, err := common.LedgerFromContext(r.Context()).CountTransactions(r.Context(), *rq) if err != nil { switch { case errors.Is(err, ledgercontroller.ErrInvalidQuery{}) || errors.Is(err, ledgercontroller.ErrMissingFeature{}): diff --git a/internal/api/v2/controllers_transactions_count_test.go b/internal/api/v2/controllers_transactions_count_test.go index a7a53a4a0..7cd83cd7e 100644 --- a/internal/api/v2/controllers_transactions_count_test.go +++ b/internal/api/v2/controllers_transactions_count_test.go @@ -29,7 +29,7 @@ func TestTransactionsCount(t *testing.T) { name string queryParams url.Values body string - expectQuery ledgercontroller.PaginatedQueryOptions[ledgercontroller.PITFilterWithVolumes] + expectQuery ledgercontroller.ResourceQuery[any] expectStatusCode int expectedErrorCode string expectBackendCall bool @@ -40,97 +40,88 @@ func TestTransactionsCount(t *testing.T) { testCases := []testCase{ { name: "nominal", - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, - }, - }), + expectQuery: ledgercontroller.ResourceQuery[any]{ + PIT: &before, + Expand: make([]string, 0), + }, expectBackendCall: true, }, { name: "using metadata", body: `{"$match": {"metadata[roles]": "admin"}}`, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, - }, - }). - WithQueryBuilder(query.Match("metadata[roles]", "admin")), + expectQuery: ledgercontroller.ResourceQuery[any]{ + PIT: &before, + Builder: query.Match("metadata[roles]", "admin"), + Expand: make([]string, 0), + }, expectBackendCall: true, }, { name: "using startTime", body: fmt.Sprintf(`{"$gte": {"date": "%s"}}`, now.Format(time.DateFormat)), - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, - }, - }). - WithQueryBuilder(query.Gte("date", now.Format(time.DateFormat))), + expectQuery: ledgercontroller.ResourceQuery[any]{ + PIT: &before, + Builder: query.Gte("date", now.Format(time.DateFormat)), + Expand: make([]string, 0), + }, expectBackendCall: true, }, { name: "using endTime", body: fmt.Sprintf(`{"$gte": {"date": "%s"}}`, now.Format(time.DateFormat)), - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, - }, - }). - WithQueryBuilder(query.Gte("date", now.Format(time.DateFormat))), + expectQuery: ledgercontroller.ResourceQuery[any]{ + PIT: &before, + Builder: query.Gte("date", now.Format(time.DateFormat)), + Expand: make([]string, 0), + }, expectBackendCall: true, }, { name: "using account", body: `{"$match": {"account": "xxx"}}`, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, - }, - }). - WithQueryBuilder(query.Match("account", "xxx")), + expectQuery: ledgercontroller.ResourceQuery[any]{ + PIT: &before, + Builder: query.Match("account", "xxx"), + Expand: make([]string, 0), + }, expectBackendCall: true, }, { name: "using reference", body: `{"$match": {"reference": "xxx"}}`, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, - }, - }). - WithQueryBuilder(query.Match("reference", "xxx")), + expectQuery: ledgercontroller.ResourceQuery[any]{ + PIT: &before, + Builder: query.Match("reference", "xxx"), + Expand: make([]string, 0), + }, expectBackendCall: true, }, { name: "using destination", body: `{"$match": {"destination": "xxx"}}`, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, - }, - }). - WithQueryBuilder(query.Match("destination", "xxx")), + expectQuery: ledgercontroller.ResourceQuery[any]{ + PIT: &before, + Builder: query.Match("destination", "xxx"), + Expand: make([]string, 0), + }, expectBackendCall: true, }, { name: "using source", body: `{"$match": {"source": "xxx"}}`, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, - }, - }). - WithQueryBuilder(query.Match("source", "xxx")), + expectQuery: ledgercontroller.ResourceQuery[any]{ + PIT: &before, + Builder: query.Match("source", "xxx"), + Expand: make([]string, 0), + }, expectBackendCall: true, }, { name: "error from backend", - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, - }, - }), + expectQuery: ledgercontroller.ResourceQuery[any]{ + PIT: &before, + Expand: make([]string, 0), + }, expectStatusCode: http.StatusInternalServerError, expectedErrorCode: api.ErrorInternal, expectBackendCall: true, @@ -142,11 +133,10 @@ func TestTransactionsCount(t *testing.T) { expectedErrorCode: common.ErrValidation, expectBackendCall: true, returnErr: ledgercontroller.ErrInvalidQuery{}, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, - }, - }), + expectQuery: ledgercontroller.ResourceQuery[any]{ + PIT: &before, + Expand: make([]string, 0), + }, }, { name: "with missing feature", @@ -154,11 +144,10 @@ func TestTransactionsCount(t *testing.T) { expectedErrorCode: common.ErrValidation, expectBackendCall: true, returnErr: ledgercontroller.ErrMissingFeature{}, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, - }, - }), + expectQuery: ledgercontroller.ResourceQuery[any]{ + PIT: &before, + Expand: make([]string, 0), + }, }, } for _, tc := range testCases { @@ -171,7 +160,7 @@ func TestTransactionsCount(t *testing.T) { systemController, ledgerController := newTestingSystemController(t, true) if tc.expectBackendCall { ledgerController.EXPECT(). - CountTransactions(gomock.Any(), ledgercontroller.NewListTransactionsQuery(tc.expectQuery)). + CountTransactions(gomock.Any(), tc.expectQuery). Return(10, tc.returnErr) } diff --git a/internal/api/v2/controllers_transactions_list.go b/internal/api/v2/controllers_transactions_list.go index b64839dac..2705a514e 100644 --- a/internal/api/v2/controllers_transactions_list.go +++ b/internal/api/v2/controllers_transactions_list.go @@ -6,7 +6,6 @@ import ( "errors" "github.com/formancehq/go-libs/v2/api" "github.com/formancehq/go-libs/v2/bun/bunpaginate" - "github.com/formancehq/go-libs/v2/pointer" "github.com/formancehq/ledger/internal/api/common" ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" ) @@ -14,28 +13,18 @@ import ( func listTransactions(w http.ResponseWriter, r *http.Request) { l := common.LedgerFromContext(r.Context()) - query, err := bunpaginate.Extract[ledgercontroller.ListTransactionsQuery](r, func() (*ledgercontroller.ListTransactionsQuery, error) { - options, err := getPaginatedQueryOptionsOfPITFilterWithVolumes(r) - if err != nil { - return nil, err - } - q := ledgercontroller.NewListTransactionsQuery(*options) - - if r.URL.Query().Get("order") == "effective" { - q.Column = "timestamp" - } - if r.URL.Query().Get("reverse") == "true" { - q.Order = bunpaginate.OrderAsc - } + paginationColumn := "id" + if r.URL.Query().Get("order") == "effective" { + paginationColumn = "timestamp" + } - return pointer.For(q), nil - }) + rq, err := getColumnPaginatedQuery[any](r, paginationColumn, bunpaginate.OrderDesc) if err != nil { api.BadRequest(w, common.ErrValidation, err) return } - cursor, err := l.ListTransactions(r.Context(), *query) + cursor, err := l.ListTransactions(r.Context(), *rq) if err != nil { switch { case errors.Is(err, ledgercontroller.ErrInvalidQuery{}) || errors.Is(err, ledgercontroller.ErrMissingFeature{}): diff --git a/internal/api/v2/controllers_transactions_list_test.go b/internal/api/v2/controllers_transactions_list_test.go index 8ff17eff3..c64489bb7 100644 --- a/internal/api/v2/controllers_transactions_list_test.go +++ b/internal/api/v2/controllers_transactions_list_test.go @@ -3,6 +3,7 @@ package v2 import ( "bytes" "fmt" + "github.com/formancehq/go-libs/v2/pointer" "github.com/formancehq/ledger/internal/api/common" "math/big" "net/http" @@ -29,7 +30,7 @@ func TestTransactionsList(t *testing.T) { name string queryParams url.Values body string - expectQuery ledgercontroller.ListTransactionsQuery + expectQuery ledgercontroller.ColumnPaginatedQuery[any] expectStatusCode int expectedErrorCode string } @@ -38,90 +39,120 @@ func TestTransactionsList(t *testing.T) { testCases := []testCase{ { name: "nominal", - expectQuery: ledgercontroller.NewListTransactionsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &now, + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Column: "id", + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Options: ledgercontroller.ResourceQuery[any]{ + PIT: &now, + Expand: make([]string, 0), }, - })), + }, }, { name: "using metadata", body: `{"$match": {"metadata[roles]": "admin"}}`, - expectQuery: ledgercontroller.NewListTransactionsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &now, + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Column: "id", + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Options: ledgercontroller.ResourceQuery[any]{ + PIT: &now, + Builder: query.Match("metadata[roles]", "admin"), + Expand: make([]string, 0), }, - }). - WithQueryBuilder(query.Match("metadata[roles]", "admin"))), + }, }, { name: "using startTime", body: fmt.Sprintf(`{"$gte": {"start_time": "%s"}}`, now.Format(time.DateFormat)), - expectQuery: ledgercontroller.NewListTransactionsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &now, + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Column: "id", + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Options: ledgercontroller.ResourceQuery[any]{ + PIT: &now, + Builder: query.Gte("start_time", now.Format(time.DateFormat)), + Expand: make([]string, 0), }, - }). - WithQueryBuilder(query.Gte("start_time", now.Format(time.DateFormat)))), + }, }, { name: "using endTime", body: fmt.Sprintf(`{"$lte": {"end_time": "%s"}}`, now.Format(time.DateFormat)), - expectQuery: ledgercontroller.NewListTransactionsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &now, + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Column: "id", + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Options: ledgercontroller.ResourceQuery[any]{ + PIT: &now, + Builder: query.Lte("end_time", now.Format(time.DateFormat)), + Expand: make([]string, 0), }, - }). - WithQueryBuilder(query.Lte("end_time", now.Format(time.DateFormat)))), + }, }, { name: "using account", body: `{"$match": {"account": "xxx"}}`, - expectQuery: ledgercontroller.NewListTransactionsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &now, + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Column: "id", + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Options: ledgercontroller.ResourceQuery[any]{ + PIT: &now, + Builder: query.Match("account", "xxx"), + Expand: make([]string, 0), }, - }). - WithQueryBuilder(query.Match("account", "xxx"))), + }, }, { name: "using reference", body: `{"$match": {"reference": "xxx"}}`, - expectQuery: ledgercontroller.NewListTransactionsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &now, + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Column: "id", + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Options: ledgercontroller.ResourceQuery[any]{ + PIT: &now, + Builder: query.Match("reference", "xxx"), + Expand: make([]string, 0), }, - }). - WithQueryBuilder(query.Match("reference", "xxx"))), + }, }, { name: "using destination", body: `{"$match": {"destination": "xxx"}}`, - expectQuery: ledgercontroller.NewListTransactionsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &now, + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Column: "id", + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Options: ledgercontroller.ResourceQuery[any]{ + PIT: &now, + Expand: make([]string, 0), + Builder: query.Match("destination", "xxx"), }, - }). - WithQueryBuilder(query.Match("destination", "xxx"))), + }, }, { name: "using source", body: `{"$match": {"source": "xxx"}}`, - expectQuery: ledgercontroller.NewListTransactionsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &now, + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Column: "id", + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Options: ledgercontroller.ResourceQuery[any]{ + PIT: &now, + Builder: query.Match("source", "xxx"), + Expand: make([]string, 0), }, - }). - WithQueryBuilder(query.Match("source", "xxx"))), + }, }, { name: "using empty cursor", queryParams: url.Values{ - "cursor": []string{bunpaginate.EncodeCursor(ledgercontroller.NewListTransactionsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{})))}, + "cursor": []string{bunpaginate.EncodeCursor(ledgercontroller.ColumnPaginatedQuery[any]{})}, }, - expectQuery: ledgercontroller.NewListTransactionsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{}, - })), + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{}, }, { name: "using invalid cursor", @@ -144,39 +175,65 @@ func TestTransactionsList(t *testing.T) { queryParams: url.Values{ "pageSize": []string{"1000000"}, }, - expectQuery: ledgercontroller.NewListTransactionsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &now, + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: MaxPageSize, + Column: "id", + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Options: ledgercontroller.ResourceQuery[any]{ + PIT: &now, + Expand: make([]string, 0), }, - }). - WithPageSize(MaxPageSize)), + }, }, { name: "using cursor", queryParams: url.Values{ - "cursor": []string{"eyJwYWdlU2l6ZSI6MTUsImJvdHRvbSI6bnVsbCwiY29sdW1uIjoiaWQiLCJwYWdpbmF0aW9uSUQiOm51bGwsIm9yZGVyIjoxLCJmaWx0ZXJzIjp7InFiIjp7fSwicGFnZVNpemUiOjE1LCJvcHRpb25zIjp7InBpdCI6bnVsbCwidm9sdW1lcyI6ZmFsc2UsImVmZmVjdGl2ZVZvbHVtZXMiOmZhbHNlfX0sInJldmVyc2UiOmZhbHNlfQ"}, + "cursor": []string{func() string { + return bunpaginate.EncodeCursor(ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Column: "id", + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Options: ledgercontroller.ResourceQuery[any]{ + PIT: &now, + }, + }) + }()}, + }, + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Column: "id", + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Options: ledgercontroller.ResourceQuery[any]{ + PIT: &now, + }, }, - expectQuery: ledgercontroller.NewListTransactionsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{})), }, { name: "using $exists metadata filter", body: `{"$exists": {"metadata": "foo"}}`, - expectQuery: ledgercontroller.NewListTransactionsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &now, + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Column: "id", + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Options: ledgercontroller.ResourceQuery[any]{ + PIT: &now, + Builder: query.Exists("metadata", "foo"), + Expand: make([]string, 0), }, - }). - WithQueryBuilder(query.Exists("metadata", "foo"))), + }, }, { name: "paginate using effective order", queryParams: map[string][]string{"order": {"effective"}}, - expectQuery: ledgercontroller.NewListTransactionsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &now, + expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: DefaultPageSize, + Column: "timestamp", + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Options: ledgercontroller.ResourceQuery[any]{ + PIT: &now, + Expand: make([]string, 0), }, - })). - WithColumn("timestamp"), + }, }, } for _, testCase := range testCases { @@ -223,7 +280,6 @@ func TestTransactionsList(t *testing.T) { err := api.ErrorResponse{} api.Decode(t, rec.Body, &err) require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } }) } diff --git a/internal/api/v2/controllers_transactions_read.go b/internal/api/v2/controllers_transactions_read.go index ef00f5e00..b22142cd6 100644 --- a/internal/api/v2/controllers_transactions_read.go +++ b/internal/api/v2/controllers_transactions_read.go @@ -1,6 +1,7 @@ package v2 import ( + "github.com/formancehq/go-libs/v2/query" "net/http" "strconv" @@ -20,22 +21,17 @@ func readTransaction(w http.ResponseWriter, r *http.Request) { return } - query := ledgercontroller.NewGetTransactionQuery(int(txId)) - if hasExpandVolumes(r) { - query = query.WithExpandVolumes() - } - if hasExpandEffectiveVolumes(r) { - query = query.WithExpandEffectiveVolumes() - } - - pitFilter, err := getPITFilter(r) + pit, err := getPIT(r) if err != nil { api.BadRequest(w, common.ErrValidation, err) return } - query.PITFilter = *pitFilter - tx, err := l.GetTransaction(r.Context(), query) + tx, err := l.GetTransaction(r.Context(), ledgercontroller.ResourceQuery[any]{ + PIT: pit, + Builder: query.Match("id", int(txId)), + Expand: r.URL.Query()["expand"], + }) if err != nil { switch { case postgres.IsNotFoundError(err): diff --git a/internal/api/v2/controllers_transactions_read_test.go b/internal/api/v2/controllers_transactions_read_test.go index 8eba1283f..4033ca7d9 100644 --- a/internal/api/v2/controllers_transactions_read_test.go +++ b/internal/api/v2/controllers_transactions_read_test.go @@ -1,6 +1,7 @@ package v2 import ( + "github.com/formancehq/go-libs/v2/query" "math/big" "net/http" "net/http/httptest" @@ -25,12 +26,15 @@ func TestTransactionsRead(t *testing.T) { ledger.NewPosting("world", "bank", "USD", big.NewInt(100)), ) - query := ledgercontroller.NewGetTransactionQuery(0) - query.PIT = &now + q := ledgercontroller.ResourceQuery[any]{ + PIT: &now, + Builder: query.Match("id", tx.ID), + } + q.PIT = &now systemController, ledgerController := newTestingSystemController(t, true) ledgerController.EXPECT(). - GetTransaction(gomock.Any(), query). + GetTransaction(gomock.Any(), q). Return(&tx, nil) router := NewRouter(systemController, auth.NewNoAuth(), os.Getenv("DEBUG") == "true") diff --git a/internal/api/v2/controllers_volumes.go b/internal/api/v2/controllers_volumes.go index caef27739..8c94765cb 100644 --- a/internal/api/v2/controllers_volumes.go +++ b/internal/api/v2/controllers_volumes.go @@ -2,40 +2,55 @@ package v2 import ( "net/http" + "strconv" "errors" ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" "github.com/formancehq/go-libs/v2/api" "github.com/formancehq/ledger/internal/api/common" - - "github.com/formancehq/go-libs/v2/pointer" - - "github.com/formancehq/go-libs/v2/bun/bunpaginate" ) func readVolumes(w http.ResponseWriter, r *http.Request) { l := common.LedgerFromContext(r.Context()) - query, err := bunpaginate.Extract[ledgercontroller.GetVolumesWithBalancesQuery](r, func() (*ledgercontroller.GetVolumesWithBalancesQuery, error) { - options, err := getPaginatedQueryOptionsOfFiltersForVolumes(r) - if err != nil { - return nil, err + rq, err := getOffsetPaginatedQuery[ledgercontroller.GetVolumesOptions](r, func(opts *ledgercontroller.GetVolumesOptions) error { + groupBy := r.URL.Query().Get("groupBy") + if groupBy != "" { + v, err := strconv.ParseInt(groupBy, 10, 64) + if err != nil { + return err + } + opts.GroupLvl = int(v) } - getVolumesWithBalancesQuery := ledgercontroller.NewGetVolumesWithBalancesQuery(*options) - return pointer.For(getVolumesWithBalancesQuery), nil + opts.UseInsertionDate = api.QueryParamBool(r, "insertionDate") + return nil }) - if err != nil { api.BadRequest(w, common.ErrValidation, err) return } - cursor, err := l.GetVolumesWithBalances(r.Context(), *query) + if r.URL.Query().Get("endTime") != "" { + rq.Options.PIT, err = getDate(r, "endTime") + if err != nil { + api.BadRequest(w, common.ErrValidation, err) + return + } + } + + if r.URL.Query().Get("startTime") != "" { + rq.Options.OOT, err = getDate(r, "startTime") + if err != nil { + api.BadRequest(w, common.ErrValidation, err) + return + } + } + cursor, err := l.GetVolumesWithBalances(r.Context(), *rq) if err != nil { switch { case errors.Is(err, ledgercontroller.ErrInvalidQuery{}) || errors.Is(err, ledgercontroller.ErrMissingFeature{}): diff --git a/internal/api/v2/controllers_volumes_test.go b/internal/api/v2/controllers_volumes_test.go index 7d5b7a183..b570bae9b 100644 --- a/internal/api/v2/controllers_volumes_test.go +++ b/internal/api/v2/controllers_volumes_test.go @@ -31,7 +31,7 @@ func TestGetVolumes(t *testing.T) { name string queryParams url.Values body string - expectQuery ledgercontroller.PaginatedQueryOptions[ledgercontroller.FiltersForVolumes] + expectQuery ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions] expectStatusCode int expectedErrorCode string } @@ -40,36 +40,37 @@ func TestGetVolumes(t *testing.T) { testCases := []testCase{ { name: "basic", - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, + expectQuery: ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + PageSize: DefaultPageSize, + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + PIT: &before, + Expand: make([]string, 0), }, - - UseInsertionDate: false, - }). - WithPageSize(DefaultPageSize), + }, }, { name: "using metadata", body: `{"$match": { "metadata[roles]": "admin" }}`, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, + expectQuery: ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + PageSize: DefaultPageSize, + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + PIT: &before, + Builder: query.Match("metadata[roles]", "admin"), + Expand: make([]string, 0), }, - }). - WithQueryBuilder(query.Match("metadata[roles]", "admin")). - WithPageSize(DefaultPageSize), + }, }, { name: "using account", body: `{"$match": { "account": "foo" }}`, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, + expectQuery: ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + PageSize: DefaultPageSize, + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + PIT: &before, + Builder: query.Match("account", "foo"), + Expand: make([]string, 0), }, - }). - WithQueryBuilder(query.Match("account", "foo")). - WithPageSize(DefaultPageSize), + }, }, { name: "using invalid query payload", @@ -83,31 +84,40 @@ func TestGetVolumes(t *testing.T) { "pit": []string{before.Format(time.RFC3339Nano)}, "groupBy": []string{"3"}, }, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, + expectQuery: ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + PageSize: DefaultPageSize, + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + PIT: &before, + Expand: make([]string, 0), + Opts: ledgercontroller.GetVolumesOptions{ + GroupLvl: 3, + }, }, - GroupLvl: 3, - }).WithPageSize(DefaultPageSize), + }, }, { name: "using Exists metadata filter", body: `{"$exists": { "metadata": "foo" }}`, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, + expectQuery: ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + PageSize: DefaultPageSize, + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + PIT: &before, + Builder: query.Exists("metadata", "foo"), + Expand: make([]string, 0), }, - }).WithPageSize(DefaultPageSize).WithQueryBuilder(query.Exists("metadata", "foo")), + }, }, { name: "using balance filter", body: `{"$gte": { "balance[EUR]": 50 }}`, - expectQuery: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &before, + expectQuery: ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + PageSize: DefaultPageSize, + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + PIT: &before, + Builder: query.Gte("balance[EUR]", float64(50)), + Expand: make([]string, 0), }, - }).WithQueryBuilder(query.Gte("balance[EUR]", float64(50))). - WithPageSize(DefaultPageSize), + }, }, } @@ -136,7 +146,7 @@ func TestGetVolumes(t *testing.T) { systemController, ledgerController := newTestingSystemController(t, true) if testCase.expectStatusCode < 300 && testCase.expectStatusCode >= 200 { ledgerController.EXPECT(). - GetVolumesWithBalances(gomock.Any(), ledgercontroller.NewGetVolumesWithBalancesQuery(testCase.expectQuery)). + GetVolumesWithBalances(gomock.Any(), testCase.expectQuery). Return(&expectedCursor, nil) } diff --git a/internal/api/v2/mocks_ledger_controller_test.go b/internal/api/v2/mocks_ledger_controller_test.go index 26775c2d7..2cbbfee4a 100644 --- a/internal/api/v2/mocks_ledger_controller_test.go +++ b/internal/api/v2/mocks_ledger_controller_test.go @@ -70,7 +70,7 @@ func (mr *LedgerControllerMockRecorder) Commit(ctx any) *gomock.Call { } // CountAccounts mocks base method. -func (m *LedgerController) CountAccounts(ctx context.Context, query ledger0.ListAccountsQuery) (int, error) { +func (m *LedgerController) CountAccounts(ctx context.Context, query ledger0.ResourceQuery[any]) (int, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CountAccounts", ctx, query) ret0, _ := ret[0].(int) @@ -85,7 +85,7 @@ func (mr *LedgerControllerMockRecorder) CountAccounts(ctx, query any) *gomock.Ca } // CountTransactions mocks base method. -func (m *LedgerController) CountTransactions(ctx context.Context, query ledger0.ListTransactionsQuery) (int, error) { +func (m *LedgerController) CountTransactions(ctx context.Context, query ledger0.ResourceQuery[any]) (int, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CountTransactions", ctx, query) ret0, _ := ret[0].(int) @@ -160,7 +160,7 @@ func (mr *LedgerControllerMockRecorder) Export(ctx, w any) *gomock.Call { } // GetAccount mocks base method. -func (m *LedgerController) GetAccount(ctx context.Context, query ledger0.GetAccountQuery) (*ledger.Account, error) { +func (m *LedgerController) GetAccount(ctx context.Context, query ledger0.ResourceQuery[any]) (*ledger.Account, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetAccount", ctx, query) ret0, _ := ret[0].(*ledger.Account) @@ -175,7 +175,7 @@ func (mr *LedgerControllerMockRecorder) GetAccount(ctx, query any) *gomock.Call } // GetAggregatedBalances mocks base method. -func (m *LedgerController) GetAggregatedBalances(ctx context.Context, q ledger0.GetAggregatedBalanceQuery) (ledger.BalancesByAssets, error) { +func (m *LedgerController) GetAggregatedBalances(ctx context.Context, q ledger0.ResourceQuery[ledger0.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetAggregatedBalances", ctx, q) ret0, _ := ret[0].(ledger.BalancesByAssets) @@ -220,7 +220,7 @@ func (mr *LedgerControllerMockRecorder) GetStats(ctx any) *gomock.Call { } // GetTransaction mocks base method. -func (m *LedgerController) GetTransaction(ctx context.Context, query ledger0.GetTransactionQuery) (*ledger.Transaction, error) { +func (m *LedgerController) GetTransaction(ctx context.Context, query ledger0.ResourceQuery[any]) (*ledger.Transaction, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetTransaction", ctx, query) ret0, _ := ret[0].(*ledger.Transaction) @@ -235,7 +235,7 @@ func (mr *LedgerControllerMockRecorder) GetTransaction(ctx, query any) *gomock.C } // GetVolumesWithBalances mocks base method. -func (m *LedgerController) GetVolumesWithBalances(ctx context.Context, q ledger0.GetVolumesWithBalancesQuery) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { +func (m *LedgerController) GetVolumesWithBalances(ctx context.Context, q ledger0.OffsetPaginatedQuery[ledger0.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetVolumesWithBalances", ctx, q) ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount]) @@ -279,7 +279,7 @@ func (mr *LedgerControllerMockRecorder) IsDatabaseUpToDate(ctx any) *gomock.Call } // ListAccounts mocks base method. -func (m *LedgerController) ListAccounts(ctx context.Context, query ledger0.ListAccountsQuery) (*bunpaginate.Cursor[ledger.Account], error) { +func (m *LedgerController) ListAccounts(ctx context.Context, query ledger0.OffsetPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Account], error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListAccounts", ctx, query) ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Account]) @@ -294,7 +294,7 @@ func (mr *LedgerControllerMockRecorder) ListAccounts(ctx, query any) *gomock.Cal } // ListLogs mocks base method. -func (m *LedgerController) ListLogs(ctx context.Context, query ledger0.GetLogsQuery) (*bunpaginate.Cursor[ledger.Log], error) { +func (m *LedgerController) ListLogs(ctx context.Context, query ledger0.ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Log], error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListLogs", ctx, query) ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Log]) @@ -309,7 +309,7 @@ func (mr *LedgerControllerMockRecorder) ListLogs(ctx, query any) *gomock.Call { } // ListTransactions mocks base method. -func (m *LedgerController) ListTransactions(ctx context.Context, query ledger0.ListTransactionsQuery) (*bunpaginate.Cursor[ledger.Transaction], error) { +func (m *LedgerController) ListTransactions(ctx context.Context, query ledger0.ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Transaction], error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListTransactions", ctx, query) ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Transaction]) diff --git a/internal/controller/ledger/controller.go b/internal/controller/ledger/controller.go index eee5771a8..a1cd360ed 100644 --- a/internal/controller/ledger/controller.go +++ b/internal/controller/ledger/controller.go @@ -24,15 +24,15 @@ type Controller interface { GetMigrationsInfo(ctx context.Context) ([]migrations.Info, error) GetStats(ctx context.Context) (Stats, error) - GetAccount(ctx context.Context, query GetAccountQuery) (*ledger.Account, error) - ListAccounts(ctx context.Context, query ListAccountsQuery) (*bunpaginate.Cursor[ledger.Account], error) - CountAccounts(ctx context.Context, query ListAccountsQuery) (int, error) - ListLogs(ctx context.Context, query GetLogsQuery) (*bunpaginate.Cursor[ledger.Log], error) - CountTransactions(ctx context.Context, query ListTransactionsQuery) (int, error) - ListTransactions(ctx context.Context, query ListTransactionsQuery) (*bunpaginate.Cursor[ledger.Transaction], error) - GetTransaction(ctx context.Context, query GetTransactionQuery) (*ledger.Transaction, error) - GetVolumesWithBalances(ctx context.Context, q GetVolumesWithBalancesQuery) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) - GetAggregatedBalances(ctx context.Context, q GetAggregatedBalanceQuery) (ledger.BalancesByAssets, error) + GetAccount(ctx context.Context, query ResourceQuery[any]) (*ledger.Account, error) + ListAccounts(ctx context.Context, query OffsetPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Account], error) + CountAccounts(ctx context.Context, query ResourceQuery[any]) (int, error) + ListLogs(ctx context.Context, query ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Log], error) + CountTransactions(ctx context.Context, query ResourceQuery[any]) (int, error) + ListTransactions(ctx context.Context, query ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Transaction], error) + GetTransaction(ctx context.Context, query ResourceQuery[any]) (*ledger.Transaction, error) + GetVolumesWithBalances(ctx context.Context, q OffsetPaginatedQuery[GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) + GetAggregatedBalances(ctx context.Context, q ResourceQuery[GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) // CreateTransaction accept a numscript script and returns a transaction // It can return following errors: diff --git a/internal/controller/ledger/controller_default.go b/internal/controller/ledger/controller_default.go index a8a3f26ef..eac5a7f9e 100644 --- a/internal/controller/ledger/controller_default.go +++ b/internal/controller/ledger/controller_default.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "github.com/formancehq/go-libs/v2/pointer" "github.com/formancehq/go-libs/v2/time" "github.com/formancehq/ledger/pkg/features" "math/big" @@ -106,40 +107,52 @@ func NewDefaultController( return ret } +func (ctrl *DefaultController) IsDatabaseUpToDate(ctx context.Context) (bool, error) { + return ctrl.store.IsUpToDate(ctx) +} + func (ctrl *DefaultController) GetMigrationsInfo(ctx context.Context) ([]migrations.Info, error) { return ctrl.store.GetMigrationsInfo(ctx) } -func (ctrl *DefaultController) ListTransactions(ctx context.Context, q ListTransactionsQuery) (*bunpaginate.Cursor[ledger.Transaction], error) { - return ctrl.store.ListTransactions(ctx, q) +func (ctrl *DefaultController) ListTransactions(ctx context.Context, q ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Transaction], error) { + return ctrl.store.Transactions().Paginate(ctx, q) } -func (ctrl *DefaultController) CountTransactions(ctx context.Context, q ListTransactionsQuery) (int, error) { - return ctrl.store.CountTransactions(ctx, q) +func (ctrl *DefaultController) CountTransactions(ctx context.Context, q ResourceQuery[any]) (int, error) { + return ctrl.store.Transactions().Count(ctx, q) } -func (ctrl *DefaultController) GetTransaction(ctx context.Context, query GetTransactionQuery) (*ledger.Transaction, error) { - return ctrl.store.GetTransaction(ctx, query) +func (ctrl *DefaultController) GetTransaction(ctx context.Context, q ResourceQuery[any]) (*ledger.Transaction, error) { + return ctrl.store.Transactions().GetOne(ctx, q) } -func (ctrl *DefaultController) CountAccounts(ctx context.Context, a ListAccountsQuery) (int, error) { - return ctrl.store.CountAccounts(ctx, a) +func (ctrl *DefaultController) CountAccounts(ctx context.Context, q ResourceQuery[any]) (int, error) { + return ctrl.store.Accounts().Count(ctx, q) } -func (ctrl *DefaultController) ListAccounts(ctx context.Context, a ListAccountsQuery) (*bunpaginate.Cursor[ledger.Account], error) { - return ctrl.store.ListAccounts(ctx, a) +func (ctrl *DefaultController) ListAccounts(ctx context.Context, q OffsetPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Account], error) { + return ctrl.store.Accounts().Paginate(ctx, q) } -func (ctrl *DefaultController) GetAccount(ctx context.Context, q GetAccountQuery) (*ledger.Account, error) { - return ctrl.store.GetAccount(ctx, q) +func (ctrl *DefaultController) GetAccount(ctx context.Context, q ResourceQuery[any]) (*ledger.Account, error) { + return ctrl.store.Accounts().GetOne(ctx, q) +} + +func (ctrl *DefaultController) GetAggregatedBalances(ctx context.Context, q ResourceQuery[GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) { + ret, err := ctrl.store.AggregatedBalances().GetOne(ctx, q) + if err != nil { + return nil, err + } + return ret.Aggregated.Balances(), nil } -func (ctrl *DefaultController) GetAggregatedBalances(ctx context.Context, q GetAggregatedBalanceQuery) (ledger.BalancesByAssets, error) { - return ctrl.store.GetAggregatedBalances(ctx, q) +func (ctrl *DefaultController) ListLogs(ctx context.Context, q ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Log], error) { + return ctrl.store.Logs().Paginate(ctx, q) } -func (ctrl *DefaultController) ListLogs(ctx context.Context, q GetLogsQuery) (*bunpaginate.Cursor[ledger.Log], error) { - return ctrl.store.ListLogs(ctx, q) +func (ctrl *DefaultController) GetVolumesWithBalances(ctx context.Context, q OffsetPaginatedQuery[GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { + return ctrl.store.Volumes().Paginate(ctx, q) } func (ctrl *DefaultController) Import(ctx context.Context, stream chan ledger.Log) error { @@ -174,9 +187,9 @@ func (ctrl *DefaultController) importLogs(ctx context.Context, store Store, stre } // We can import only if the ledger is empty. - logs, err := store.ListLogs(ctx, NewListLogsQuery(PaginatedQueryOptions[any]{ + logs, err := store.Logs().Paginate(ctx, ColumnPaginatedQuery[any]{ PageSize: 1, - })) + }) if err != nil { return fmt.Errorf("error listing logs: %w", err) } @@ -276,10 +289,12 @@ func (ctrl *DefaultController) importLog(ctx context.Context, store Store, log l func (ctrl *DefaultController) Export(ctx context.Context, w ExportWriter) error { return bunpaginate.Iterate( ctx, - NewListLogsQuery(NewPaginatedQueryOptions[any](nil).WithPageSize(100)). - WithOrder(bunpaginate.OrderAsc), - func(ctx context.Context, q GetLogsQuery) (*bunpaginate.Cursor[ledger.Log], error) { - return ctrl.store.ListLogs(ctx, q) + ColumnPaginatedQuery[any]{ + PageSize: 100, + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderAsc)), + }, + func(ctx context.Context, q ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Log], error) { + return ctrl.store.Logs().Paginate(ctx, q) }, func(cursor *bunpaginate.Cursor[ledger.Log]) error { for _, data := range cursor.Data { @@ -292,14 +307,6 @@ func (ctrl *DefaultController) Export(ctx context.Context, w ExportWriter) error ) } -func (ctrl *DefaultController) IsDatabaseUpToDate(ctx context.Context) (bool, error) { - return ctrl.store.IsUpToDate(ctx) -} - -func (ctrl *DefaultController) GetVolumesWithBalances(ctx context.Context, q GetVolumesWithBalancesQuery) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { - return ctrl.store.GetVolumesWithBalances(ctx, q) -} - func (ctrl *DefaultController) createTransaction(ctx context.Context, store Store, parameters Parameters[RunScript]) (*ledger.CreatedTransaction, error) { logger := logging.FromContext(ctx).WithField("req", uuid.NewString()[:8]) diff --git a/internal/controller/ledger/controller_default_test.go b/internal/controller/ledger/controller_default_test.go index d590b19c8..775414c8d 100644 --- a/internal/controller/ledger/controller_default_test.go +++ b/internal/controller/ledger/controller_default_test.go @@ -2,6 +2,7 @@ package ledger import ( "context" + "github.com/formancehq/go-libs/v2/query" "math/big" "testing" @@ -199,15 +200,24 @@ func TestListTransactions(t *testing.T) { store := NewMockStore(ctrl) parser := NewMockNumscriptParser(ctrl) ctx := logging.TestingContext() + transactions := NewMockPaginatedResource[ledger.Transaction, any, ColumnPaginatedQuery[any]](ctrl) cursor := &bunpaginate.Cursor[ledger.Transaction]{} - query := NewListTransactionsQuery(NewPaginatedQueryOptions[PITFilterWithVolumes](PITFilterWithVolumes{})) - store.EXPECT(). - ListTransactions(gomock.Any(), query). + store.EXPECT().Transactions().Return(transactions) + transactions.EXPECT(). + Paginate(gomock.Any(), ColumnPaginatedQuery[any]{ + PageSize: bunpaginate.QueryDefaultPageSize, + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Column: "id", + }). Return(cursor, nil) l := NewDefaultController(ledger.Ledger{}, store, parser) - ret, err := l.ListTransactions(ctx, query) + ret, err := l.ListTransactions(ctx, ColumnPaginatedQuery[any]{ + PageSize: bunpaginate.QueryDefaultPageSize, + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Column: "id", + }) require.NoError(t, err) require.Equal(t, cursor, ret) } @@ -219,12 +229,13 @@ func TestCountAccounts(t *testing.T) { store := NewMockStore(ctrl) parser := NewMockNumscriptParser(ctrl) ctx := logging.TestingContext() + accounts := NewMockPaginatedResource[ledger.Account, any, OffsetPaginatedQuery[any]](ctrl) - query := NewListAccountsQuery(NewPaginatedQueryOptions[PITFilterWithVolumes](PITFilterWithVolumes{})) - store.EXPECT().CountAccounts(gomock.Any(), query).Return(1, nil) + store.EXPECT().Accounts().Return(accounts) + accounts.EXPECT().Count(gomock.Any(), ResourceQuery[any]{}).Return(1, nil) l := NewDefaultController(ledger.Ledger{}, store, parser) - count, err := l.CountAccounts(ctx, query) + count, err := l.CountAccounts(ctx, ResourceQuery[any]{}) require.NoError(t, err) require.Equal(t, 1, count) } @@ -236,15 +247,18 @@ func TestGetTransaction(t *testing.T) { store := NewMockStore(ctrl) parser := NewMockNumscriptParser(ctrl) ctx := logging.TestingContext() + transactions := NewMockPaginatedResource[ledger.Transaction, any, ColumnPaginatedQuery[any]](ctrl) tx := ledger.Transaction{} - query := NewGetTransactionQuery(0) - store.EXPECT(). - GetTransaction(gomock.Any(), query). - Return(&tx, nil) + store.EXPECT().Transactions().Return(transactions) + transactions.EXPECT().GetOne(gomock.Any(), ResourceQuery[any]{ + Builder: query.Match("id", 1), + }).Return(&tx, nil) l := NewDefaultController(ledger.Ledger{}, store, parser) - ret, err := l.GetTransaction(ctx, query) + ret, err := l.GetTransaction(ctx, ResourceQuery[any]{ + Builder: query.Match("id", 1), + }) require.NoError(t, err) require.Equal(t, tx, *ret) } @@ -256,15 +270,18 @@ func TestGetAccount(t *testing.T) { store := NewMockStore(ctrl) parser := NewMockNumscriptParser(ctrl) ctx := logging.TestingContext() + accounts := NewMockPaginatedResource[ledger.Account, any, OffsetPaginatedQuery[any]](ctrl) account := ledger.Account{} - query := NewGetAccountQuery("world") - store.EXPECT(). - GetAccount(gomock.Any(), query). - Return(&account, nil) + store.EXPECT().Accounts().Return(accounts) + accounts.EXPECT().GetOne(gomock.Any(), ResourceQuery[any]{ + Builder: query.Match("address", "world"), + }).Return(&account, nil) l := NewDefaultController(ledger.Ledger{}, store, parser) - ret, err := l.GetAccount(ctx, query) + ret, err := l.GetAccount(ctx, ResourceQuery[any]{ + Builder: query.Match("address", "world"), + }) require.NoError(t, err) require.Equal(t, account, *ret) } @@ -276,12 +293,13 @@ func TestCountTransactions(t *testing.T) { store := NewMockStore(ctrl) parser := NewMockNumscriptParser(ctrl) ctx := logging.TestingContext() + transactions := NewMockPaginatedResource[ledger.Transaction, any, ColumnPaginatedQuery[any]](ctrl) - query := NewListTransactionsQuery(NewPaginatedQueryOptions[PITFilterWithVolumes](PITFilterWithVolumes{})) - store.EXPECT().CountTransactions(gomock.Any(), query).Return(1, nil) + store.EXPECT().Transactions().Return(transactions) + transactions.EXPECT().Count(gomock.Any(), ResourceQuery[any]{}).Return(1, nil) l := NewDefaultController(ledger.Ledger{}, store, parser) - count, err := l.CountTransactions(ctx, query) + count, err := l.CountTransactions(ctx, ResourceQuery[any]{}) require.NoError(t, err) require.Equal(t, 1, count) } @@ -293,15 +311,20 @@ func TestListAccounts(t *testing.T) { store := NewMockStore(ctrl) parser := NewMockNumscriptParser(ctrl) ctx := logging.TestingContext() + accounts := NewMockPaginatedResource[ledger.Account, any, OffsetPaginatedQuery[any]](ctrl) cursor := &bunpaginate.Cursor[ledger.Account]{} - query := NewListAccountsQuery(NewPaginatedQueryOptions[PITFilterWithVolumes](PITFilterWithVolumes{})) - store.EXPECT(). - ListAccounts(gomock.Any(), query). - Return(cursor, nil) + store.EXPECT().Accounts().Return(accounts) + accounts.EXPECT().Paginate(gomock.Any(), OffsetPaginatedQuery[any]{ + PageSize: bunpaginate.QueryDefaultPageSize, + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderAsc)), + }).Return(cursor, nil) l := NewDefaultController(ledger.Ledger{}, store, parser) - ret, err := l.ListAccounts(ctx, query) + ret, err := l.ListAccounts(ctx, OffsetPaginatedQuery[any]{ + PageSize: bunpaginate.QueryDefaultPageSize, + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderAsc)), + }) require.NoError(t, err) require.Equal(t, cursor, ret) } @@ -313,17 +336,16 @@ func TestGetAggregatedBalances(t *testing.T) { store := NewMockStore(ctrl) parser := NewMockNumscriptParser(ctrl) ctx := logging.TestingContext() + aggregatedBalances := NewMockResource[ledger.AggregatedVolumes, GetAggregatedVolumesOptions](ctrl) - balancesByAssets := ledger.BalancesByAssets{} - query := NewGetAggregatedBalancesQuery(PITFilter{}, nil, false) - store.EXPECT(). - GetAggregatedBalances(gomock.Any(), query). - Return(balancesByAssets, nil) + store.EXPECT().AggregatedBalances().Return(aggregatedBalances) + aggregatedBalances.EXPECT().GetOne(gomock.Any(), ResourceQuery[GetAggregatedVolumesOptions]{}). + Return(&ledger.AggregatedVolumes{}, nil) l := NewDefaultController(ledger.Ledger{}, store, parser) - ret, err := l.GetAggregatedBalances(ctx, query) + ret, err := l.GetAggregatedBalances(ctx, ResourceQuery[GetAggregatedVolumesOptions]{}) require.NoError(t, err) - require.Equal(t, balancesByAssets, ret) + require.Equal(t, ledger.BalancesByAssets{}, ret) } func TestListLogs(t *testing.T) { @@ -333,15 +355,22 @@ func TestListLogs(t *testing.T) { store := NewMockStore(ctrl) parser := NewMockNumscriptParser(ctrl) ctx := logging.TestingContext() + logs := NewMockPaginatedResource[ledger.Log, any, ColumnPaginatedQuery[any]](ctrl) cursor := &bunpaginate.Cursor[ledger.Log]{} - query := NewListLogsQuery(NewPaginatedQueryOptions[any](nil)) - store.EXPECT(). - ListLogs(gomock.Any(), query). - Return(cursor, nil) + store.EXPECT().Logs().Return(logs) + logs.EXPECT().Paginate(gomock.Any(), ColumnPaginatedQuery[any]{ + PageSize: bunpaginate.QueryDefaultPageSize, + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Column: "id", + }).Return(cursor, nil) l := NewDefaultController(ledger.Ledger{}, store, parser) - ret, err := l.ListLogs(ctx, query) + ret, err := l.ListLogs(ctx, ColumnPaginatedQuery[any]{ + PageSize: bunpaginate.QueryDefaultPageSize, + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), + Column: "id", + }) require.NoError(t, err) require.Equal(t, cursor, ret) } @@ -353,15 +382,20 @@ func TestGetVolumesWithBalances(t *testing.T) { store := NewMockStore(ctrl) parser := NewMockNumscriptParser(ctrl) ctx := logging.TestingContext() + volumes := NewMockPaginatedResource[ledger.VolumesWithBalanceByAssetByAccount, GetVolumesOptions, OffsetPaginatedQuery[GetVolumesOptions]](ctrl) balancesByAssets := &bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount]{} - query := NewGetVolumesWithBalancesQuery(NewPaginatedQueryOptions[FiltersForVolumes](FiltersForVolumes{})) - store.EXPECT(). - GetVolumesWithBalances(gomock.Any(), query). - Return(balancesByAssets, nil) + store.EXPECT().Volumes().Return(volumes) + volumes.EXPECT().Paginate(gomock.Any(), OffsetPaginatedQuery[GetVolumesOptions]{ + PageSize: bunpaginate.QueryDefaultPageSize, + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderAsc)), + }).Return(balancesByAssets, nil) l := NewDefaultController(ledger.Ledger{}, store, parser) - ret, err := l.GetVolumesWithBalances(ctx, query) + ret, err := l.GetVolumesWithBalances(ctx, OffsetPaginatedQuery[GetVolumesOptions]{ + PageSize: bunpaginate.QueryDefaultPageSize, + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderAsc)), + }) require.NoError(t, err) require.Equal(t, balancesByAssets, ret) } diff --git a/internal/controller/ledger/controller_generated_test.go b/internal/controller/ledger/controller_generated_test.go index 3090e4922..7e6601231 100644 --- a/internal/controller/ledger/controller_generated_test.go +++ b/internal/controller/ledger/controller_generated_test.go @@ -69,7 +69,7 @@ func (mr *MockControllerMockRecorder) Commit(ctx any) *gomock.Call { } // CountAccounts mocks base method. -func (m *MockController) CountAccounts(ctx context.Context, query ListAccountsQuery) (int, error) { +func (m *MockController) CountAccounts(ctx context.Context, query ResourceQuery[any]) (int, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CountAccounts", ctx, query) ret0, _ := ret[0].(int) @@ -84,7 +84,7 @@ func (mr *MockControllerMockRecorder) CountAccounts(ctx, query any) *gomock.Call } // CountTransactions mocks base method. -func (m *MockController) CountTransactions(ctx context.Context, query ListTransactionsQuery) (int, error) { +func (m *MockController) CountTransactions(ctx context.Context, query ResourceQuery[any]) (int, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CountTransactions", ctx, query) ret0, _ := ret[0].(int) @@ -159,7 +159,7 @@ func (mr *MockControllerMockRecorder) Export(ctx, w any) *gomock.Call { } // GetAccount mocks base method. -func (m *MockController) GetAccount(ctx context.Context, query GetAccountQuery) (*ledger.Account, error) { +func (m *MockController) GetAccount(ctx context.Context, query ResourceQuery[any]) (*ledger.Account, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetAccount", ctx, query) ret0, _ := ret[0].(*ledger.Account) @@ -174,7 +174,7 @@ func (mr *MockControllerMockRecorder) GetAccount(ctx, query any) *gomock.Call { } // GetAggregatedBalances mocks base method. -func (m *MockController) GetAggregatedBalances(ctx context.Context, q GetAggregatedBalanceQuery) (ledger.BalancesByAssets, error) { +func (m *MockController) GetAggregatedBalances(ctx context.Context, q ResourceQuery[GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetAggregatedBalances", ctx, q) ret0, _ := ret[0].(ledger.BalancesByAssets) @@ -219,7 +219,7 @@ func (mr *MockControllerMockRecorder) GetStats(ctx any) *gomock.Call { } // GetTransaction mocks base method. -func (m *MockController) GetTransaction(ctx context.Context, query GetTransactionQuery) (*ledger.Transaction, error) { +func (m *MockController) GetTransaction(ctx context.Context, query ResourceQuery[any]) (*ledger.Transaction, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetTransaction", ctx, query) ret0, _ := ret[0].(*ledger.Transaction) @@ -234,7 +234,7 @@ func (mr *MockControllerMockRecorder) GetTransaction(ctx, query any) *gomock.Cal } // GetVolumesWithBalances mocks base method. -func (m *MockController) GetVolumesWithBalances(ctx context.Context, q GetVolumesWithBalancesQuery) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { +func (m *MockController) GetVolumesWithBalances(ctx context.Context, q OffsetPaginatedQuery[GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetVolumesWithBalances", ctx, q) ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount]) @@ -278,7 +278,7 @@ func (mr *MockControllerMockRecorder) IsDatabaseUpToDate(ctx any) *gomock.Call { } // ListAccounts mocks base method. -func (m *MockController) ListAccounts(ctx context.Context, query ListAccountsQuery) (*bunpaginate.Cursor[ledger.Account], error) { +func (m *MockController) ListAccounts(ctx context.Context, query OffsetPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Account], error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListAccounts", ctx, query) ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Account]) @@ -293,7 +293,7 @@ func (mr *MockControllerMockRecorder) ListAccounts(ctx, query any) *gomock.Call } // ListLogs mocks base method. -func (m *MockController) ListLogs(ctx context.Context, query GetLogsQuery) (*bunpaginate.Cursor[ledger.Log], error) { +func (m *MockController) ListLogs(ctx context.Context, query ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Log], error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListLogs", ctx, query) ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Log]) @@ -308,7 +308,7 @@ func (mr *MockControllerMockRecorder) ListLogs(ctx, query any) *gomock.Call { } // ListTransactions mocks base method. -func (m *MockController) ListTransactions(ctx context.Context, query ListTransactionsQuery) (*bunpaginate.Cursor[ledger.Transaction], error) { +func (m *MockController) ListTransactions(ctx context.Context, query ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Transaction], error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListTransactions", ctx, query) ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Transaction]) diff --git a/internal/controller/ledger/controller_with_traces.go b/internal/controller/ledger/controller_with_traces.go index b95bb13f0..9bf0aebc8 100644 --- a/internal/controller/ledger/controller_with_traces.go +++ b/internal/controller/ledger/controller_with_traces.go @@ -195,7 +195,7 @@ func (c *ControllerWithTraces) GetMigrationsInfo(ctx context.Context) ([]migrati ) } -func (c *ControllerWithTraces) ListTransactions(ctx context.Context, q ListTransactionsQuery) (*bunpaginate.Cursor[ledger.Transaction], error) { +func (c *ControllerWithTraces) ListTransactions(ctx context.Context, q ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Transaction], error) { return tracing.TraceWithMetric( ctx, "ListTransactions", @@ -207,7 +207,7 @@ func (c *ControllerWithTraces) ListTransactions(ctx context.Context, q ListTrans ) } -func (c *ControllerWithTraces) CountTransactions(ctx context.Context, q ListTransactionsQuery) (int, error) { +func (c *ControllerWithTraces) CountTransactions(ctx context.Context, q ResourceQuery[any]) (int, error) { return tracing.TraceWithMetric( ctx, "CountTransactions", @@ -219,7 +219,7 @@ func (c *ControllerWithTraces) CountTransactions(ctx context.Context, q ListTran ) } -func (c *ControllerWithTraces) GetTransaction(ctx context.Context, query GetTransactionQuery) (*ledger.Transaction, error) { +func (c *ControllerWithTraces) GetTransaction(ctx context.Context, query ResourceQuery[any]) (*ledger.Transaction, error) { return tracing.TraceWithMetric( ctx, "GetTransaction", @@ -231,7 +231,7 @@ func (c *ControllerWithTraces) GetTransaction(ctx context.Context, query GetTran ) } -func (c *ControllerWithTraces) CountAccounts(ctx context.Context, a ListAccountsQuery) (int, error) { +func (c *ControllerWithTraces) CountAccounts(ctx context.Context, a ResourceQuery[any]) (int, error) { return tracing.TraceWithMetric( ctx, "CountAccounts", @@ -243,7 +243,7 @@ func (c *ControllerWithTraces) CountAccounts(ctx context.Context, a ListAccounts ) } -func (c *ControllerWithTraces) ListAccounts(ctx context.Context, a ListAccountsQuery) (*bunpaginate.Cursor[ledger.Account], error) { +func (c *ControllerWithTraces) ListAccounts(ctx context.Context, a OffsetPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Account], error) { return tracing.TraceWithMetric( ctx, "ListAccounts", @@ -255,7 +255,7 @@ func (c *ControllerWithTraces) ListAccounts(ctx context.Context, a ListAccountsQ ) } -func (c *ControllerWithTraces) GetAccount(ctx context.Context, q GetAccountQuery) (*ledger.Account, error) { +func (c *ControllerWithTraces) GetAccount(ctx context.Context, q ResourceQuery[any]) (*ledger.Account, error) { return tracing.TraceWithMetric( ctx, "GetAccount", @@ -267,7 +267,7 @@ func (c *ControllerWithTraces) GetAccount(ctx context.Context, q GetAccountQuery ) } -func (c *ControllerWithTraces) GetAggregatedBalances(ctx context.Context, q GetAggregatedBalanceQuery) (ledger.BalancesByAssets, error) { +func (c *ControllerWithTraces) GetAggregatedBalances(ctx context.Context, q ResourceQuery[GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) { return tracing.TraceWithMetric( ctx, "GetAggregatedBalances", @@ -279,7 +279,7 @@ func (c *ControllerWithTraces) GetAggregatedBalances(ctx context.Context, q GetA ) } -func (c *ControllerWithTraces) ListLogs(ctx context.Context, q GetLogsQuery) (*bunpaginate.Cursor[ledger.Log], error) { +func (c *ControllerWithTraces) ListLogs(ctx context.Context, q ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Log], error) { return tracing.TraceWithMetric( ctx, "ListLogs", @@ -327,7 +327,7 @@ func (c *ControllerWithTraces) IsDatabaseUpToDate(ctx context.Context) (bool, er ) } -func (c *ControllerWithTraces) GetVolumesWithBalances(ctx context.Context, q GetVolumesWithBalancesQuery) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { +func (c *ControllerWithTraces) GetVolumesWithBalances(ctx context.Context, q OffsetPaginatedQuery[GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { return tracing.TraceWithMetric( ctx, "GetVolumesWithBalances", diff --git a/internal/controller/ledger/stats.go b/internal/controller/ledger/stats.go index 46d9c92c4..24a6a8e84 100644 --- a/internal/controller/ledger/stats.go +++ b/internal/controller/ledger/stats.go @@ -13,12 +13,12 @@ type Stats struct { func (ctrl *DefaultController) GetStats(ctx context.Context) (Stats, error) { var stats Stats - transactions, err := ctrl.store.CountTransactions(ctx, NewListTransactionsQuery(NewPaginatedQueryOptions(PITFilterWithVolumes{}))) + transactions, err := ctrl.store.Transactions().Count(ctx, ResourceQuery[any]{}) if err != nil { return stats, fmt.Errorf("counting transactions: %w", err) } - accounts, err := ctrl.store.CountAccounts(ctx, NewListAccountsQuery(NewPaginatedQueryOptions(PITFilterWithVolumes{}))) + accounts, err := ctrl.store.Accounts().Count(ctx, ResourceQuery[any]{}) if err != nil { return stats, fmt.Errorf("counting accounts: %w", err) } diff --git a/internal/controller/ledger/stats_test.go b/internal/controller/ledger/stats_test.go index e314c6d02..eae3b9801 100644 --- a/internal/controller/ledger/stats_test.go +++ b/internal/controller/ledger/stats_test.go @@ -15,14 +15,13 @@ func TestStats(t *testing.T) { ctrl := gomock.NewController(t) store := NewMockStore(ctrl) parser := NewMockNumscriptParser(ctrl) + transactions := NewMockPaginatedResource[ledger.Transaction, any, ColumnPaginatedQuery[any]](ctrl) + accounts := NewMockPaginatedResource[ledger.Account, any, OffsetPaginatedQuery[any]](ctrl) - store.EXPECT(). - CountTransactions(gomock.Any(), NewListTransactionsQuery(NewPaginatedQueryOptions(PITFilterWithVolumes{}))). - Return(10, nil) - - store.EXPECT(). - CountAccounts(gomock.Any(), NewListAccountsQuery(NewPaginatedQueryOptions(PITFilterWithVolumes{}))). - Return(10, nil) + store.EXPECT().Transactions().Return(transactions) + transactions.EXPECT().Count(ctx, ResourceQuery[any]{}).Return(10, nil) + store.EXPECT().Accounts().Return(accounts) + accounts.EXPECT().Count(ctx, ResourceQuery[any]{}).Return(10, nil) ledgerController := NewDefaultController( ledger.MustNewWithDefault("foo"), diff --git a/internal/controller/ledger/store.go b/internal/controller/ledger/store.go index 9a4579419..3b1375b92 100644 --- a/internal/controller/ledger/store.go +++ b/internal/controller/ledger/store.go @@ -11,7 +11,6 @@ import ( "github.com/formancehq/go-libs/v2/bun/bunpaginate" "github.com/formancehq/go-libs/v2/metadata" - "github.com/formancehq/go-libs/v2/pointer" "github.com/formancehq/go-libs/v2/query" "github.com/formancehq/go-libs/v2/time" ledger "github.com/formancehq/ledger/internal" @@ -53,136 +52,16 @@ type Store interface { LockLedger(ctx context.Context) error GetDB() bun.IDB - ListLogs(ctx context.Context, q GetLogsQuery) (*bunpaginate.Cursor[ledger.Log], error) ReadLogWithIdempotencyKey(ctx context.Context, ik string) (*ledger.Log, error) - ListTransactions(ctx context.Context, q ListTransactionsQuery) (*bunpaginate.Cursor[ledger.Transaction], error) - CountTransactions(ctx context.Context, q ListTransactionsQuery) (int, error) - GetTransaction(ctx context.Context, query GetTransactionQuery) (*ledger.Transaction, error) - CountAccounts(ctx context.Context, a ListAccountsQuery) (int, error) - ListAccounts(ctx context.Context, a ListAccountsQuery) (*bunpaginate.Cursor[ledger.Account], error) - GetAccount(ctx context.Context, q GetAccountQuery) (*ledger.Account, error) - GetAggregatedBalances(ctx context.Context, q GetAggregatedBalanceQuery) (ledger.BalancesByAssets, error) - GetVolumesWithBalances(ctx context.Context, q GetVolumesWithBalancesQuery) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) IsUpToDate(ctx context.Context) (bool, error) GetMigrationsInfo(ctx context.Context) ([]migrations.Info, error) -} - -type ListTransactionsQuery bunpaginate.ColumnPaginatedQuery[PaginatedQueryOptions[PITFilterWithVolumes]] - -func (q ListTransactionsQuery) WithColumn(column string) ListTransactionsQuery { - ret := pointer.For((bunpaginate.ColumnPaginatedQuery[PaginatedQueryOptions[PITFilterWithVolumes]])(q)) - ret = ret.WithColumn(column) - - return ListTransactionsQuery(*ret) -} - -func NewListTransactionsQuery(options PaginatedQueryOptions[PITFilterWithVolumes]) ListTransactionsQuery { - return ListTransactionsQuery{ - PageSize: options.PageSize, - Column: "id", - Order: bunpaginate.OrderDesc, - Options: options, - } -} - -type GetTransactionQuery struct { - PITFilterWithVolumes - ID int -} - -func (q GetTransactionQuery) WithExpandVolumes() GetTransactionQuery { - q.ExpandVolumes = true - - return q -} - -func (q GetTransactionQuery) WithExpandEffectiveVolumes() GetTransactionQuery { - q.ExpandEffectiveVolumes = true - - return q -} - -func NewGetTransactionQuery(id int) GetTransactionQuery { - return GetTransactionQuery{ - PITFilterWithVolumes: PITFilterWithVolumes{}, - ID: id, - } -} - -type ListAccountsQuery bunpaginate.OffsetPaginatedQuery[PaginatedQueryOptions[PITFilterWithVolumes]] - -func (q ListAccountsQuery) WithExpandVolumes() ListAccountsQuery { - q.Options.Options.ExpandVolumes = true - - return q -} - -func (q ListAccountsQuery) WithExpandEffectiveVolumes() ListAccountsQuery { - q.Options.Options.ExpandEffectiveVolumes = true - - return q -} - -func NewListAccountsQuery(opts PaginatedQueryOptions[PITFilterWithVolumes]) ListAccountsQuery { - return ListAccountsQuery{ - PageSize: opts.PageSize, - Order: bunpaginate.OrderAsc, - Options: opts, - } -} - -type GetAccountQuery struct { - PITFilterWithVolumes - Addr string -} - -func (q GetAccountQuery) WithPIT(pit time.Time) GetAccountQuery { - q.PIT = &pit - - return q -} - -func (q GetAccountQuery) WithExpandVolumes() GetAccountQuery { - q.ExpandVolumes = true - - return q -} - -func (q GetAccountQuery) WithExpandEffectiveVolumes() GetAccountQuery { - q.ExpandEffectiveVolumes = true - - return q -} - -func NewGetAccountQuery(addr string) GetAccountQuery { - return GetAccountQuery{ - Addr: addr, - } -} - -type GetAggregatedBalanceQuery struct { - PITFilter - QueryBuilder query.Builder - UseInsertionDate bool -} -func NewGetAggregatedBalancesQuery(filter PITFilter, qb query.Builder, useInsertionDate bool) GetAggregatedBalanceQuery { - return GetAggregatedBalanceQuery{ - PITFilter: filter, - QueryBuilder: qb, - UseInsertionDate: useInsertionDate, - } -} - -type GetVolumesWithBalancesQuery bunpaginate.OffsetPaginatedQuery[PaginatedQueryOptions[FiltersForVolumes]] - -func NewGetVolumesWithBalancesQuery(opts PaginatedQueryOptions[FiltersForVolumes]) GetVolumesWithBalancesQuery { - return GetVolumesWithBalancesQuery{ - PageSize: opts.PageSize, - Order: bunpaginate.OrderAsc, - Options: opts, - } + Accounts() PaginatedResource[ledger.Account, any, OffsetPaginatedQuery[any]] + Logs() PaginatedResource[ledger.Log, any, ColumnPaginatedQuery[any]] + Transactions() PaginatedResource[ledger.Transaction, any, ColumnPaginatedQuery[any]] + AggregatedBalances() Resource[ledger.AggregatedVolumes, GetAggregatedVolumesOptions] + Volumes() PaginatedResource[ledger.VolumesWithBalanceByAssetByAccount, GetVolumesOptions, OffsetPaginatedQuery[GetVolumesOptions]] } type PaginatedQueryOptions[T any] struct { @@ -237,45 +116,14 @@ func NewPaginatedQueryOptions[T any](options T) PaginatedQueryOptions[T] { } } -type PITFilter struct { - PIT *time.Time `json:"pit"` - OOT *time.Time `json:"oot"` -} - -type PITFilterWithVolumes struct { - PITFilter - ExpandVolumes bool `json:"volumes"` - ExpandEffectiveVolumes bool `json:"effectiveVolumes"` -} - -type FiltersForVolumes struct { - PITFilter - UseInsertionDate bool - GroupLvl int -} - -type GetLogsQuery bunpaginate.ColumnPaginatedQuery[PaginatedQueryOptions[any]] - -func (q GetLogsQuery) WithOrder(order bunpaginate.Order) GetLogsQuery { - q.Order = order - return q -} - -func NewListLogsQuery(options PaginatedQueryOptions[any]) GetLogsQuery { - return GetLogsQuery{ - PageSize: options.PageSize, - Column: "id", - Order: bunpaginate.OrderDesc, - Options: options, - } -} - type vmStoreAdapter struct { Store } func (v *vmStoreAdapter) GetAccount(ctx context.Context, address string) (*ledger.Account, error) { - account, err := v.Store.GetAccount(ctx, NewGetAccountQuery(address)) + account, err := v.Store.Accounts().GetOne(ctx, ResourceQuery[any]{ + Builder: query.Match("address", address), + }) if err != nil { return nil, err } @@ -298,6 +146,75 @@ func NewListLedgersQuery(pageSize uint64) ListLedgersQuery { } } +type ResourceQuery[Opts any] struct { + PIT *time.Time `json:"pit"` + OOT *time.Time `json:"oot"` + Builder query.Builder `json:"qb"` + Expand []string `json:"expand,omitempty"` + Opts Opts `json:"opts"` +} + +func (rq ResourceQuery[Opts]) UsePIT() bool { + return rq.PIT != nil && !rq.PIT.IsZero() +} + +func (rq ResourceQuery[Opts]) UseOOT() bool { + return rq.OOT != nil && !rq.OOT.IsZero() +} + +func (rq *ResourceQuery[Opts]) UnmarshalJSON(data []byte) error { + type rawResourceQuery ResourceQuery[Opts] + type aux struct { + rawResourceQuery + Builder json.RawMessage `json:"qb"` + } + x := aux{} + if err := json.Unmarshal(data, &x); err != nil { + return err + } + + var err error + *rq = ResourceQuery[Opts](x.rawResourceQuery) + rq.Builder, err = query.ParseJSON(string(x.Builder)) + + return err +} + +//go:generate mockgen -write_source_comment=false -write_package_comment=false -source store.go -destination store_generated_test.go -package ledger . Resource +type Resource[ResourceType, OptionsType any] interface { + GetOne(ctx context.Context, query ResourceQuery[OptionsType]) (*ResourceType, error) + Count(ctx context.Context, query ResourceQuery[OptionsType]) (int, error) +} + +type ( + OffsetPaginatedQuery[OptionsType any] struct { + Column string `json:"column"` + Offset uint64 `json:"offset"` + Order *bunpaginate.Order `json:"order"` + PageSize uint64 `json:"pageSize"` + Options ResourceQuery[OptionsType] `json:"filters"` + } + ColumnPaginatedQuery[OptionsType any] struct { + PageSize uint64 `json:"pageSize"` + Bottom *big.Int `json:"bottom"` + Column string `json:"column"` + PaginationID *big.Int `json:"paginationID"` + // todo: backport in go-libs + Order *bunpaginate.Order `json:"order"` + Options ResourceQuery[OptionsType] `json:"filters"` + Reverse bool `json:"reverse"` + } + PaginatedQuery[OptionsType any] interface { + OffsetPaginatedQuery[OptionsType] | ColumnPaginatedQuery[OptionsType] + } +) + +//go:generate mockgen -write_source_comment=false -write_package_comment=false -source store.go -destination store_generated_test.go -package ledger . PaginatedResource +type PaginatedResource[ResourceType, OptionsType any, PaginationQueryType PaginatedQuery[OptionsType]] interface { + Resource[ResourceType, OptionsType] + Paginate(ctx context.Context, paginationOptions PaginationQueryType) (*bunpaginate.Cursor[ResourceType], error) +} + // numscript rewrite implementation var _ numscript.Store = (*numscriptRewriteAdapter)(nil) @@ -326,8 +243,8 @@ func (s *numscriptRewriteAdapter) GetAccountsMetadata(ctx context.Context, q num // we ignore the needed metadata values and just return all of them for address := range q { - v, err := s.Store.GetAccount(ctx, GetAccountQuery{ - Addr: address, + v, err := s.Store.Accounts().GetOne(ctx, ResourceQuery[any]{ + Builder: query.Match("address", address), }) if err != nil { return nil, err @@ -337,3 +254,12 @@ func (s *numscriptRewriteAdapter) GetAccountsMetadata(ctx context.Context, q num return m, nil } + +type GetAggregatedVolumesOptions struct { + UseInsertionDate bool `json:"useInsertionDate"` +} + +type GetVolumesOptions struct { + UseInsertionDate bool `json:"useInsertionDate"` + GroupLvl int `json:"groupLvl"` +} diff --git a/internal/controller/ledger/store_generated_test.go b/internal/controller/ledger/store_generated_test.go index 0244559a7..7a677d58e 100644 --- a/internal/controller/ledger/store_generated_test.go +++ b/internal/controller/ledger/store_generated_test.go @@ -2,7 +2,7 @@ // // Generated by this command: // -// mockgen -write_source_comment=false -write_package_comment=false -source store.go -destination store_generated_test.go -package ledger . Store +// mockgen -write_source_comment=false -write_package_comment=false -source store.go -destination store_generated_test.go -package ledger . PaginatedResource package ledger import ( @@ -42,6 +42,34 @@ func (m *MockStore) EXPECT() *MockStoreMockRecorder { return m.recorder } +// Accounts mocks base method. +func (m *MockStore) Accounts() PaginatedResource[ledger.Account, any, OffsetPaginatedQuery[any]] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Accounts") + ret0, _ := ret[0].(PaginatedResource[ledger.Account, any, OffsetPaginatedQuery[any]]) + return ret0 +} + +// Accounts indicates an expected call of Accounts. +func (mr *MockStoreMockRecorder) Accounts() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Accounts", reflect.TypeOf((*MockStore)(nil).Accounts)) +} + +// AggregatedBalances mocks base method. +func (m *MockStore) AggregatedBalances() Resource[ledger.AggregatedVolumes, GetAggregatedVolumesOptions] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AggregatedBalances") + ret0, _ := ret[0].(Resource[ledger.AggregatedVolumes, GetAggregatedVolumesOptions]) + return ret0 +} + +// AggregatedBalances indicates an expected call of AggregatedBalances. +func (mr *MockStoreMockRecorder) AggregatedBalances() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AggregatedBalances", reflect.TypeOf((*MockStore)(nil).AggregatedBalances)) +} + // BeginTX mocks base method. func (m *MockStore) BeginTX(ctx context.Context, options *sql.TxOptions) (Store, error) { m.ctrl.T.Helper() @@ -85,36 +113,6 @@ func (mr *MockStoreMockRecorder) CommitTransaction(ctx, transaction any) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CommitTransaction", reflect.TypeOf((*MockStore)(nil).CommitTransaction), ctx, transaction) } -// CountAccounts mocks base method. -func (m *MockStore) CountAccounts(ctx context.Context, a ListAccountsQuery) (int, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CountAccounts", ctx, a) - ret0, _ := ret[0].(int) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CountAccounts indicates an expected call of CountAccounts. -func (mr *MockStoreMockRecorder) CountAccounts(ctx, a any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountAccounts", reflect.TypeOf((*MockStore)(nil).CountAccounts), ctx, a) -} - -// CountTransactions mocks base method. -func (m *MockStore) CountTransactions(ctx context.Context, q ListTransactionsQuery) (int, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CountTransactions", ctx, q) - ret0, _ := ret[0].(int) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CountTransactions indicates an expected call of CountTransactions. -func (mr *MockStoreMockRecorder) CountTransactions(ctx, q any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountTransactions", reflect.TypeOf((*MockStore)(nil).CountTransactions), ctx, q) -} - // DeleteAccountMetadata mocks base method. func (m *MockStore) DeleteAccountMetadata(ctx context.Context, address, key string) error { m.ctrl.T.Helper() @@ -145,36 +143,6 @@ func (mr *MockStoreMockRecorder) DeleteTransactionMetadata(ctx, transactionID, k return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTransactionMetadata", reflect.TypeOf((*MockStore)(nil).DeleteTransactionMetadata), ctx, transactionID, key) } -// GetAccount mocks base method. -func (m *MockStore) GetAccount(ctx context.Context, q GetAccountQuery) (*ledger.Account, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAccount", ctx, q) - ret0, _ := ret[0].(*ledger.Account) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetAccount indicates an expected call of GetAccount. -func (mr *MockStoreMockRecorder) GetAccount(ctx, q any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccount", reflect.TypeOf((*MockStore)(nil).GetAccount), ctx, q) -} - -// GetAggregatedBalances mocks base method. -func (m *MockStore) GetAggregatedBalances(ctx context.Context, q GetAggregatedBalanceQuery) (ledger.BalancesByAssets, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAggregatedBalances", ctx, q) - ret0, _ := ret[0].(ledger.BalancesByAssets) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetAggregatedBalances indicates an expected call of GetAggregatedBalances. -func (mr *MockStoreMockRecorder) GetAggregatedBalances(ctx, q any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAggregatedBalances", reflect.TypeOf((*MockStore)(nil).GetAggregatedBalances), ctx, q) -} - // GetBalances mocks base method. func (m *MockStore) GetBalances(ctx context.Context, query BalanceQuery) (Balances, error) { m.ctrl.T.Helper() @@ -219,36 +187,6 @@ func (mr *MockStoreMockRecorder) GetMigrationsInfo(ctx any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMigrationsInfo", reflect.TypeOf((*MockStore)(nil).GetMigrationsInfo), ctx) } -// GetTransaction mocks base method. -func (m *MockStore) GetTransaction(ctx context.Context, query GetTransactionQuery) (*ledger.Transaction, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTransaction", ctx, query) - ret0, _ := ret[0].(*ledger.Transaction) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetTransaction indicates an expected call of GetTransaction. -func (mr *MockStoreMockRecorder) GetTransaction(ctx, query any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTransaction", reflect.TypeOf((*MockStore)(nil).GetTransaction), ctx, query) -} - -// GetVolumesWithBalances mocks base method. -func (m *MockStore) GetVolumesWithBalances(ctx context.Context, q GetVolumesWithBalancesQuery) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetVolumesWithBalances", ctx, q) - ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount]) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetVolumesWithBalances indicates an expected call of GetVolumesWithBalances. -func (mr *MockStoreMockRecorder) GetVolumesWithBalances(ctx, q any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVolumesWithBalances", reflect.TypeOf((*MockStore)(nil).GetVolumesWithBalances), ctx, q) -} - // InsertLog mocks base method. func (m *MockStore) InsertLog(ctx context.Context, log *ledger.Log) error { m.ctrl.T.Helper() @@ -278,51 +216,6 @@ func (mr *MockStoreMockRecorder) IsUpToDate(ctx any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsUpToDate", reflect.TypeOf((*MockStore)(nil).IsUpToDate), ctx) } -// ListAccounts mocks base method. -func (m *MockStore) ListAccounts(ctx context.Context, a ListAccountsQuery) (*bunpaginate.Cursor[ledger.Account], error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAccounts", ctx, a) - ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Account]) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListAccounts indicates an expected call of ListAccounts. -func (mr *MockStoreMockRecorder) ListAccounts(ctx, a any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAccounts", reflect.TypeOf((*MockStore)(nil).ListAccounts), ctx, a) -} - -// ListLogs mocks base method. -func (m *MockStore) ListLogs(ctx context.Context, q GetLogsQuery) (*bunpaginate.Cursor[ledger.Log], error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListLogs", ctx, q) - ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Log]) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListLogs indicates an expected call of ListLogs. -func (mr *MockStoreMockRecorder) ListLogs(ctx, q any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLogs", reflect.TypeOf((*MockStore)(nil).ListLogs), ctx, q) -} - -// ListTransactions mocks base method. -func (m *MockStore) ListTransactions(ctx context.Context, q ListTransactionsQuery) (*bunpaginate.Cursor[ledger.Transaction], error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListTransactions", ctx, q) - ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Transaction]) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListTransactions indicates an expected call of ListTransactions. -func (mr *MockStoreMockRecorder) ListTransactions(ctx, q any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTransactions", reflect.TypeOf((*MockStore)(nil).ListTransactions), ctx, q) -} - // LockLedger mocks base method. func (m *MockStore) LockLedger(ctx context.Context) error { m.ctrl.T.Helper() @@ -337,6 +230,20 @@ func (mr *MockStoreMockRecorder) LockLedger(ctx any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LockLedger", reflect.TypeOf((*MockStore)(nil).LockLedger), ctx) } +// Logs mocks base method. +func (m *MockStore) Logs() PaginatedResource[ledger.Log, any, ColumnPaginatedQuery[any]] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Logs") + ret0, _ := ret[0].(PaginatedResource[ledger.Log, any, ColumnPaginatedQuery[any]]) + return ret0 +} + +// Logs indicates an expected call of Logs. +func (mr *MockStoreMockRecorder) Logs() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logs", reflect.TypeOf((*MockStore)(nil).Logs)) +} + // ReadLogWithIdempotencyKey mocks base method. func (m *MockStore) ReadLogWithIdempotencyKey(ctx context.Context, ik string) (*ledger.Log, error) { m.ctrl.T.Helper() @@ -382,6 +289,20 @@ func (mr *MockStoreMockRecorder) Rollback() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rollback", reflect.TypeOf((*MockStore)(nil).Rollback)) } +// Transactions mocks base method. +func (m *MockStore) Transactions() PaginatedResource[ledger.Transaction, any, ColumnPaginatedQuery[any]] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Transactions") + ret0, _ := ret[0].(PaginatedResource[ledger.Transaction, any, ColumnPaginatedQuery[any]]) + return ret0 +} + +// Transactions indicates an expected call of Transactions. +func (mr *MockStoreMockRecorder) Transactions() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Transactions", reflect.TypeOf((*MockStore)(nil).Transactions)) +} + // UpdateAccountsMetadata mocks base method. func (m_2 *MockStore) UpdateAccountsMetadata(ctx context.Context, m map[string]metadata.Metadata) error { m_2.ctrl.T.Helper() @@ -430,3 +351,138 @@ func (mr *MockStoreMockRecorder) UpsertAccounts(ctx any, accounts ...any) *gomoc varargs := append([]any{ctx}, accounts...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertAccounts", reflect.TypeOf((*MockStore)(nil).UpsertAccounts), varargs...) } + +// Volumes mocks base method. +func (m *MockStore) Volumes() PaginatedResource[ledger.VolumesWithBalanceByAssetByAccount, GetVolumesOptions, OffsetPaginatedQuery[GetVolumesOptions]] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Volumes") + ret0, _ := ret[0].(PaginatedResource[ledger.VolumesWithBalanceByAssetByAccount, GetVolumesOptions, OffsetPaginatedQuery[GetVolumesOptions]]) + return ret0 +} + +// Volumes indicates an expected call of Volumes. +func (mr *MockStoreMockRecorder) Volumes() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Volumes", reflect.TypeOf((*MockStore)(nil).Volumes)) +} + +// MockResource is a mock of Resource interface. +type MockResource[ResourceType any, OptionsType any] struct { + ctrl *gomock.Controller + recorder *MockResourceMockRecorder[ResourceType, OptionsType] +} + +// MockResourceMockRecorder is the mock recorder for MockResource. +type MockResourceMockRecorder[ResourceType any, OptionsType any] struct { + mock *MockResource[ResourceType, OptionsType] +} + +// NewMockResource creates a new mock instance. +func NewMockResource[ResourceType any, OptionsType any](ctrl *gomock.Controller) *MockResource[ResourceType, OptionsType] { + mock := &MockResource[ResourceType, OptionsType]{ctrl: ctrl} + mock.recorder = &MockResourceMockRecorder[ResourceType, OptionsType]{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockResource[ResourceType, OptionsType]) EXPECT() *MockResourceMockRecorder[ResourceType, OptionsType] { + return m.recorder +} + +// Count mocks base method. +func (m *MockResource[ResourceType, OptionsType]) Count(ctx context.Context, query ResourceQuery[OptionsType]) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Count", ctx, query) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Count indicates an expected call of Count. +func (mr *MockResourceMockRecorder[ResourceType, OptionsType]) Count(ctx, query any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Count", reflect.TypeOf((*MockResource[ResourceType, OptionsType])(nil).Count), ctx, query) +} + +// GetOne mocks base method. +func (m *MockResource[ResourceType, OptionsType]) GetOne(ctx context.Context, query ResourceQuery[OptionsType]) (*ResourceType, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetOne", ctx, query) + ret0, _ := ret[0].(*ResourceType) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetOne indicates an expected call of GetOne. +func (mr *MockResourceMockRecorder[ResourceType, OptionsType]) GetOne(ctx, query any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOne", reflect.TypeOf((*MockResource[ResourceType, OptionsType])(nil).GetOne), ctx, query) +} + +// MockPaginatedResource is a mock of PaginatedResource interface. +type MockPaginatedResource[ResourceType any, OptionsType any, PaginationQueryType PaginatedQuery[OptionsType]] struct { + ctrl *gomock.Controller + recorder *MockPaginatedResourceMockRecorder[ResourceType, OptionsType, PaginationQueryType] +} + +// MockPaginatedResourceMockRecorder is the mock recorder for MockPaginatedResource. +type MockPaginatedResourceMockRecorder[ResourceType any, OptionsType any, PaginationQueryType PaginatedQuery[OptionsType]] struct { + mock *MockPaginatedResource[ResourceType, OptionsType, PaginationQueryType] +} + +// NewMockPaginatedResource creates a new mock instance. +func NewMockPaginatedResource[ResourceType any, OptionsType any, PaginationQueryType PaginatedQuery[OptionsType]](ctrl *gomock.Controller) *MockPaginatedResource[ResourceType, OptionsType, PaginationQueryType] { + mock := &MockPaginatedResource[ResourceType, OptionsType, PaginationQueryType]{ctrl: ctrl} + mock.recorder = &MockPaginatedResourceMockRecorder[ResourceType, OptionsType, PaginationQueryType]{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPaginatedResource[ResourceType, OptionsType, PaginationQueryType]) EXPECT() *MockPaginatedResourceMockRecorder[ResourceType, OptionsType, PaginationQueryType] { + return m.recorder +} + +// Count mocks base method. +func (m *MockPaginatedResource[ResourceType, OptionsType, PaginationQueryType]) Count(ctx context.Context, query ResourceQuery[OptionsType]) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Count", ctx, query) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Count indicates an expected call of Count. +func (mr *MockPaginatedResourceMockRecorder[ResourceType, OptionsType, PaginationQueryType]) Count(ctx, query any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Count", reflect.TypeOf((*MockPaginatedResource[ResourceType, OptionsType, PaginationQueryType])(nil).Count), ctx, query) +} + +// GetOne mocks base method. +func (m *MockPaginatedResource[ResourceType, OptionsType, PaginationQueryType]) GetOne(ctx context.Context, query ResourceQuery[OptionsType]) (*ResourceType, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetOne", ctx, query) + ret0, _ := ret[0].(*ResourceType) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetOne indicates an expected call of GetOne. +func (mr *MockPaginatedResourceMockRecorder[ResourceType, OptionsType, PaginationQueryType]) GetOne(ctx, query any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOne", reflect.TypeOf((*MockPaginatedResource[ResourceType, OptionsType, PaginationQueryType])(nil).GetOne), ctx, query) +} + +// Paginate mocks base method. +func (m *MockPaginatedResource[ResourceType, OptionsType, PaginationQueryType]) Paginate(ctx context.Context, paginationOptions PaginationQueryType) (*bunpaginate.Cursor[ResourceType], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Paginate", ctx, paginationOptions) + ret0, _ := ret[0].(*bunpaginate.Cursor[ResourceType]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Paginate indicates an expected call of Paginate. +func (mr *MockPaginatedResourceMockRecorder[ResourceType, OptionsType, PaginationQueryType]) Paginate(ctx, paginationOptions any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Paginate", reflect.TypeOf((*MockPaginatedResource[ResourceType, OptionsType, PaginationQueryType])(nil).Paginate), ctx, paginationOptions) +} diff --git a/internal/storage/ledger/accounts.go b/internal/storage/ledger/accounts.go index 6fa8545bd..5e28be4ef 100644 --- a/internal/storage/ledger/accounts.go +++ b/internal/storage/ledger/accounts.go @@ -3,320 +3,27 @@ package ledger import ( "context" "fmt" - . "github.com/formancehq/go-libs/v2/bun/bunpaginate" . "github.com/formancehq/go-libs/v2/collectionutils" - "github.com/formancehq/ledger/pkg/features" + "github.com/formancehq/ledger/internal/tracing" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "regexp" - "github.com/formancehq/ledger/internal/tracing" - "github.com/formancehq/go-libs/v2/metadata" "github.com/formancehq/go-libs/v2/platform/postgres" - "github.com/formancehq/go-libs/v2/time" - ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" - - "github.com/formancehq/go-libs/v2/query" ledger "github.com/formancehq/ledger/internal" - "github.com/uptrace/bun" ) var ( balanceRegex = regexp.MustCompile(`balance\[(.*)]`) ) -func convertOperatorToSQL(operator string) string { - switch operator { - case "$match": - return "=" - case "$lt": - return "<" - case "$gt": - return ">" - case "$lte": - return "<=" - case "$gte": - return ">=" - } - panic("unreachable") -} - -func (s *Store) selectBalance(date *time.Time) (*bun.SelectQuery, error) { - - if date != nil && !date.IsZero() { - selectDistinctMovesBySeq, err := s.SelectDistinctMovesBySeq(date) - if err != nil { - return nil, err - } - sortedMoves := selectDistinctMovesBySeq. - ColumnExpr("(post_commit_volumes).inputs - (post_commit_volumes).outputs as balance") - - return s.db.NewSelect(). - ModelTableExpr("(?) moves", sortedMoves). - Where("ledger = ?", s.ledger.Name). - ColumnExpr("accounts_address, asset, balance"), nil - } - - return s.db.NewSelect(). - ModelTableExpr(s.GetPrefixedRelationName("accounts_volumes")). - Where("ledger = ?", s.ledger.Name). - ColumnExpr("input - output as balance"), nil -} - -func (s *Store) selectDistinctAccountMetadataHistories(date *time.Time) *bun.SelectQuery { - ret := s.db.NewSelect(). - DistinctOn("accounts_address"). - ModelTableExpr(s.GetPrefixedRelationName("accounts_metadata")). - Where("ledger = ?", s.ledger.Name). - Column("accounts_address", "metadata"). - Order("accounts_address", "revision desc") - - if date != nil && !date.IsZero() { - ret = ret.Where("date <= ?", date) - } - - return ret -} - -func (s *Store) selectAccounts(date *time.Time, expandVolumes, expandEffectiveVolumes bool, qb query.Builder) (*bun.SelectQuery, error) { - - ret := s.db.NewSelect() - - needVolumes := expandVolumes - if qb != nil { - // Analyze filters to check for errors and find potentially additional table to load - if err := qb.Walk(func(operator, key string, value any) error { - switch { - // Balances requires pvc, force load in this case - case balanceRegex.MatchString(key): - needVolumes = true - case key == "address": - return s.validateAddressFilter(operator, value) - case key == "metadata": - if operator != "$exists" { - return ledgercontroller.NewErrInvalidQuery("'metadata' key filter can only be used with $exists") - } - case metadataRegex.MatchString(key): - if operator != "$match" { - return ledgercontroller.NewErrInvalidQuery("'metadata' key filter can only be used with $match") - } - case key == "first_usage" || key == "balance": - default: - return ledgercontroller.NewErrInvalidQuery("unknown key '%s' when building query", key) - } - - return nil - }); err != nil { - return nil, fmt.Errorf("failed to check filters: %w", err) - } - } - - // Build the query - ret = ret. - ModelTableExpr(s.GetPrefixedRelationName("accounts")). - Column("accounts.address", "accounts.first_usage"). - Where("ledger = ?", s.ledger.Name). - Order("accounts.address") - - if date != nil && !date.IsZero() { - ret = ret.Where("accounts.first_usage <= ?", date) - } - - if s.ledger.HasFeature(features.FeatureAccountMetadataHistory, "SYNC") && date != nil && !date.IsZero() { - ret = ret. - Join( - `left join (?) accounts_metadata on accounts_metadata.accounts_address = accounts.address`, - s.selectDistinctAccountMetadataHistories(date), - ). - ColumnExpr("coalesce(accounts_metadata.metadata, '{}'::jsonb) as metadata") - } else { - ret = ret.ColumnExpr("accounts.metadata") - } - - if s.ledger.HasFeature(features.FeatureMovesHistory, "ON") && needVolumes { - selectAccountWithAggregatedVolumes, err := s.selectAccountWithAggregatedVolumes(date, true, "volumes") - if err != nil { - return nil, err - } - ret = ret.Join( - `left join (?) volumes on volumes.accounts_address = accounts.address`, - selectAccountWithAggregatedVolumes, - ).Column("volumes.*") - } - - if s.ledger.HasFeature(features.FeatureMovesHistoryPostCommitEffectiveVolumes, "SYNC") && expandEffectiveVolumes { - selectAccountWithAggregatedVolumes, err := s.selectAccountWithAggregatedVolumes(date, false, "effective_volumes") - if err != nil { - return nil, err - } - ret = ret.Join( - `left join (?) effective_volumes on effective_volumes.accounts_address = accounts.address`, - selectAccountWithAggregatedVolumes, - ).Column("effective_volumes.*") - } - - if qb != nil { - // Convert filters to where clause - where, args, err := qb.Build(query.ContextFn(func(key, operator string, value any) (string, []any, error) { - switch { - case key == "address": - return filterAccountAddress(value.(string), "accounts.address"), nil, nil - case key == "first_usage": - return fmt.Sprintf("first_usage %s ?", convertOperatorToSQL(operator)), []any{value}, nil - case balanceRegex.Match([]byte(key)): - match := balanceRegex.FindAllStringSubmatch(key, 2) - asset := match[0][1] - - selectBalance, err := s.selectBalance(date) - if err != nil { - return "", nil, err - } - - return s.db.NewSelect(). - TableExpr( - "(?) balance", - selectBalance. - Where("asset = ? and accounts_address = accounts.address", asset), - ). - ColumnExpr(fmt.Sprintf("balance %s ?", convertOperatorToSQL(operator)), value). - String(), nil, nil - - case key == "balance": - selectBalance, err := s.selectBalance(date) - if err != nil { - return "", nil, err - } - - return s.db.NewSelect(). - TableExpr( - "(?) balance", - selectBalance. - Where("accounts_address = accounts.address"), - ). - ColumnExpr(fmt.Sprintf("balance %s ?", convertOperatorToSQL(operator)), value). - String(), nil, nil - - case key == "metadata": - if s.ledger.HasFeature(features.FeatureAccountMetadataHistory, "SYNC") && date != nil && !date.IsZero() { - key = "accounts_metadata.metadata" - } - - return key + " -> ? is not null", []any{value}, nil - - case metadataRegex.Match([]byte(key)): - match := metadataRegex.FindAllStringSubmatch(key, 3) - if s.ledger.HasFeature(features.FeatureAccountMetadataHistory, "SYNC") && date != nil && !date.IsZero() { - key = "accounts_metadata.metadata" - } else { - key = "metadata" - } - - return key + " @> ?", []any{map[string]any{ - match[0][1]: value, - }}, nil - } - - panic("unreachable") - })) - if err != nil { - return nil, fmt.Errorf("evaluating filters: %w", err) - } - if len(args) > 0 { - ret = ret.Where(where, args...) - } else { - ret = ret.Where(where) - } - } - - return ret, nil -} - -func (s *Store) ListAccounts(ctx context.Context, q ledgercontroller.ListAccountsQuery) (*Cursor[ledger.Account], error) { - selectAccounts, err := s.selectAccounts( - q.Options.Options.PIT, - q.Options.Options.ExpandVolumes, - q.Options.Options.ExpandEffectiveVolumes, - q.Options.QueryBuilder, - ) - if err != nil { - return nil, err - } - return tracing.TraceWithMetric( - ctx, - "ListAccounts", - s.tracer, - s.listAccountsHistogram, - func(ctx context.Context) (*Cursor[ledger.Account], error) { - ret, err := UsingOffset[ledgercontroller.PaginatedQueryOptions[ledgercontroller.PITFilterWithVolumes], ledger.Account]( - ctx, - selectAccounts, - OffsetPaginatedQuery[ledgercontroller.PaginatedQueryOptions[ledgercontroller.PITFilterWithVolumes]](q), - ) - - if err != nil { - return nil, err - } - - return ret, nil - }, - ) -} - -func (s *Store) GetAccount(ctx context.Context, q ledgercontroller.GetAccountQuery) (*ledger.Account, error) { - return tracing.TraceWithMetric( - ctx, - "GetAccount", - s.tracer, - s.getAccountHistogram, - func(ctx context.Context) (*ledger.Account, error) { - ret := &ledger.Account{} - selectAccounts, err := s.selectAccounts(q.PIT, q.ExpandVolumes, q.ExpandEffectiveVolumes, nil) - if err != nil { - return nil, err - } - if err := selectAccounts. - Model(ret). - Where("accounts.address = ?", q.Addr). - Limit(1). - Scan(ctx); err != nil { - return nil, postgres.ResolveError(err) - } - - return ret, nil - }, - ) -} - -func (s *Store) CountAccounts(ctx context.Context, q ledgercontroller.ListAccountsQuery) (int, error) { - return tracing.TraceWithMetric( - ctx, - "CountAccounts", - s.tracer, - s.countAccountsHistogram, - func(ctx context.Context) (int, error) { - selectAccounts, err := s.selectAccounts( - q.Options.Options.PIT, - q.Options.Options.ExpandVolumes, - q.Options.Options.ExpandEffectiveVolumes, - q.Options.QueryBuilder, - ) - if err != nil { - return 0, err - } - return s.db.NewSelect(). - TableExpr("(?) data", selectAccounts). - Count(ctx) - }, - ) -} - -func (s *Store) UpdateAccountsMetadata(ctx context.Context, m map[string]metadata.Metadata) error { +func (store *Store) UpdateAccountsMetadata(ctx context.Context, m map[string]metadata.Metadata) error { _, err := tracing.TraceWithMetric( ctx, "UpdateAccountsMetadata", - s.tracer, - s.updateAccountsMetadataHistogram, + store.tracer, + store.updateAccountsMetadataHistogram, tracing.NoResult(func(ctx context.Context) error { span := trace.SpanFromContext(ctx) @@ -330,7 +37,7 @@ func (s *Store) UpdateAccountsMetadata(ctx context.Context, m map[string]metadat accounts := make([]AccountWithLedger, 0) for account, accountMetadata := range m { accounts = append(accounts, AccountWithLedger{ - Ledger: s.ledger.Name, + Ledger: store.ledger.Name, Account: ledger.Account{ Address: account, Metadata: accountMetadata, @@ -338,11 +45,12 @@ func (s *Store) UpdateAccountsMetadata(ctx context.Context, m map[string]metadat }) } - ret, err := s.db.NewInsert(). + ret, err := store.db.NewInsert(). Model(&accounts). - ModelTableExpr(s.GetPrefixedRelationName("accounts")). + ModelTableExpr(store.GetPrefixedRelationName("accounts")). On("CONFLICT (ledger, address) DO UPDATE"). Set("metadata = excluded.metadata || accounts.metadata"). + Set("updated_at = excluded.updated_at"). Where("not accounts.metadata @> excluded.metadata"). Exec(ctx) @@ -363,18 +71,18 @@ func (s *Store) UpdateAccountsMetadata(ctx context.Context, m map[string]metadat return err } -func (s *Store) DeleteAccountMetadata(ctx context.Context, account, key string) error { +func (store *Store) DeleteAccountMetadata(ctx context.Context, account, key string) error { _, err := tracing.TraceWithMetric( ctx, "DeleteAccountMetadata", - s.tracer, - s.deleteAccountMetadataHistogram, + store.tracer, + store.deleteAccountMetadataHistogram, tracing.NoResult(func(ctx context.Context) error { - _, err := s.db.NewUpdate(). - ModelTableExpr(s.GetPrefixedRelationName("accounts")). + _, err := store.db.NewUpdate(). + ModelTableExpr(store.GetPrefixedRelationName("accounts")). Set("metadata = metadata - ?", key). Where("address = ?", account). - Where("ledger = ?", s.ledger.Name). + Where("ledger = ?", store.ledger.Name). Exec(ctx) return postgres.ResolveError(err) }), @@ -382,24 +90,24 @@ func (s *Store) DeleteAccountMetadata(ctx context.Context, account, key string) return err } -func (s *Store) UpsertAccounts(ctx context.Context, accounts ...*ledger.Account) error { +func (store *Store) UpsertAccounts(ctx context.Context, accounts ...*ledger.Account) error { return tracing.SkipResult(tracing.TraceWithMetric( ctx, "UpsertAccounts", - s.tracer, - s.upsertAccountsHistogram, + store.tracer, + store.upsertAccountsHistogram, tracing.NoResult(func(ctx context.Context) error { span := trace.SpanFromContext(ctx) span.SetAttributes(attribute.StringSlice("accounts", Map(accounts, (*ledger.Account).GetAddress))) - ret, err := s.db.NewInsert(). + ret, err := store.db.NewInsert(). Model(&accounts). - ModelTableExpr(s.GetPrefixedRelationName("accounts")). + ModelTableExpr(store.GetPrefixedRelationName("accounts")). On("conflict (ledger, address) do update"). Set("first_usage = case when excluded.first_usage < accounts.first_usage then excluded.first_usage else accounts.first_usage end"). Set("metadata = accounts.metadata || excluded.metadata"). Set("updated_at = excluded.updated_at"). - Value("ledger", "?", s.ledger.Name). + Value("ledger", "?", store.ledger.Name). Returning("*"). Where("(excluded.first_usage < accounts.first_usage) or not accounts.metadata @> excluded.metadata"). Exec(ctx) diff --git a/internal/storage/ledger/accounts_test.go b/internal/storage/ledger/accounts_test.go index ef0c2f6d1..4d20e16d6 100644 --- a/internal/storage/ledger/accounts_test.go +++ b/internal/storage/ledger/accounts_test.go @@ -6,6 +6,7 @@ import ( "context" "math/big" "testing" + libtime "time" "errors" "github.com/formancehq/go-libs/v2/pointer" @@ -71,27 +72,29 @@ func TestAccountsList(t *testing.T) { t.Run("list all", func(t *testing.T) { t.Parallel() - accounts, err := store.ListAccounts(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}))) + accounts, err := store.Accounts().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[any]{}) require.NoError(t, err) require.Len(t, accounts.Data, 7) }) t.Run("list using metadata", func(t *testing.T) { t.Parallel() - accounts, err := store.ListAccounts(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Match("metadata[category]", "1")), - )) + accounts, err := store.Accounts().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[any]{ + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("metadata[category]", "1"), + }, + }) require.NoError(t, err) require.Len(t, accounts.Data, 1) }) t.Run("list before date", func(t *testing.T) { t.Parallel() - accounts, err := store.ListAccounts(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ + accounts, err := store.Accounts().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[any]{ + Options: ledgercontroller.ResourceQuery[any]{ PIT: &now, }, - }))) + }) require.NoError(t, err) require.Len(t, accounts.Data, 2) }) @@ -99,9 +102,12 @@ func TestAccountsList(t *testing.T) { t.Run("list with volumes", func(t *testing.T) { t.Parallel() - accounts, err := store.ListAccounts(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - ExpandVolumes: true, - }).WithQueryBuilder(query.Match("address", "account:1")))) + accounts, err := store.Accounts().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[any]{ + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("address", "account:1"), + Expand: []string{"volumes"}, + }, + }) require.NoError(t, err) require.Len(t, accounts.Data, 1) require.Equal(t, ledger.VolumesByAssets{ @@ -112,12 +118,13 @@ func TestAccountsList(t *testing.T) { t.Run("list with volumes using PIT", func(t *testing.T) { t.Parallel() - accounts, err := store.ListAccounts(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &now, + accounts, err := store.Accounts().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[any]{ + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("address", "account:1"), + PIT: &now, + Expand: []string{"volumes"}, }, - ExpandVolumes: true, - }).WithQueryBuilder(query.Match("address", "account:1")))) + }) require.NoError(t, err) require.Len(t, accounts.Data, 1) require.Equal(t, ledger.VolumesByAssets{ @@ -128,9 +135,12 @@ func TestAccountsList(t *testing.T) { t.Run("list with effective volumes", func(t *testing.T) { t.Parallel() - accounts, err := store.ListAccounts(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - ExpandEffectiveVolumes: true, - }).WithQueryBuilder(query.Match("address", "account:1")))) + accounts, err := store.Accounts().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[any]{ + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("address", "account:1"), + Expand: []string{"effectiveVolumes"}, + }, + }) require.NoError(t, err) require.Len(t, accounts.Data, 1) require.Equal(t, ledger.VolumesByAssets{ @@ -140,12 +150,13 @@ func TestAccountsList(t *testing.T) { t.Run("list with effective volumes using PIT", func(t *testing.T) { t.Parallel() - accounts, err := store.ListAccounts(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &now, + accounts, err := store.Accounts().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[any]{ + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("address", "account:1"), + PIT: &now, + Expand: []string{"effectiveVolumes"}, }, - ExpandEffectiveVolumes: true, - }).WithQueryBuilder(query.Match("address", "account:1")))) + }) require.NoError(t, err) require.Len(t, accounts.Data, 1) require.Equal(t, ledger.VolumesByAssets{ @@ -155,36 +166,42 @@ func TestAccountsList(t *testing.T) { t.Run("list using filter on address", func(t *testing.T) { t.Parallel() - accounts, err := store.ListAccounts(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Match("address", "account:")), - )) + accounts, err := store.Accounts().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[any]{ + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("address", "account:"), + }, + }) require.NoError(t, err) require.Len(t, accounts.Data, 3) }) t.Run("list using filter on multiple address", func(t *testing.T) { t.Parallel() - accounts, err := store.ListAccounts(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder( - query.Or( + accounts, err := store.Accounts().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[any]{ + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Or( query.Match("address", "account:1"), query.Match("address", "orders:"), ), - ), - )) + }, + }) require.NoError(t, err) require.Len(t, accounts.Data, 3) }) t.Run("list using filter on balances", func(t *testing.T) { t.Parallel() - accounts, err := store.ListAccounts(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Lt("balance[USD]", 0)), - )) + accounts, err := store.Accounts().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[any]{ + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Lt("balance[USD]", 0), + }, + }) require.NoError(t, err) require.Len(t, accounts.Data, 1) // world - accounts, err = store.ListAccounts(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Gt("balance[USD]", 0)), - )) + accounts, err = store.Accounts().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[any]{ + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Gt("balance[USD]", 0), + }, + }) require.NoError(t, err) require.Len(t, accounts.Data, 2) require.Equal(t, "account:1", accounts.Data[0].Address) @@ -192,49 +209,53 @@ func TestAccountsList(t *testing.T) { }) t.Run("list using filter on balances[USD] and PIT", func(t *testing.T) { t.Parallel() - accounts, err := store.ListAccounts(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &now, + accounts, err := store.Accounts().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[any]{ + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Lt("balance[USD]", 0), + PIT: &now, }, - }). - WithQueryBuilder(query.Lt("balance[USD]", 0)), - )) + }) require.NoError(t, err) require.Len(t, accounts.Data, 1) // world }) t.Run("list using filter on balances and PIT", func(t *testing.T) { t.Parallel() - accounts, err := store.ListAccounts(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &now, + accounts, err := store.Accounts().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[any]{ + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Lt("balance", 0), + PIT: &now, }, - }). - WithQueryBuilder(query.Lt("balance", 0)), - )) + }) require.NoError(t, err) require.Len(t, accounts.Data, 1) // world }) t.Run("list using filter on exists metadata", func(t *testing.T) { t.Parallel() - accounts, err := store.ListAccounts(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Exists("metadata", "foo")), - )) + accounts, err := store.Accounts().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[any]{ + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Exists("metadata", "foo"), + }, + }) require.NoError(t, err) require.Len(t, accounts.Data, 2) - accounts, err = store.ListAccounts(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Exists("metadata", "category")), - )) + accounts, err = store.Accounts().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[any]{ + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Exists("metadata", "category"), + }, + }) require.NoError(t, err) require.Len(t, accounts.Data, 3) }) t.Run("list using filter invalid field", func(t *testing.T) { t.Parallel() - _, err := store.ListAccounts(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Lt("invalid", 0)), - )) + _, err := store.Accounts().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[any]{ + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Lt("invalid", 0), + }, + }) require.Error(t, err) require.True(t, errors.Is(err, ledgercontroller.ErrInvalidQuery{})) }) @@ -242,9 +263,11 @@ func TestAccountsList(t *testing.T) { t.Run("filter on first_usage", func(t *testing.T) { t.Parallel() - ret, err := store.ListAccounts(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Lt("first_usage", now)), - )) + ret, err := store.Accounts().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[any]{ + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Lte("first_usage", now), + }, + }) require.NoError(t, err) require.Len(t, ret.Data, 2) }) @@ -263,7 +286,9 @@ func TestAccountsUpdateMetadata(t *testing.T) { "bank": m, })) - account, err := store.GetAccount(context.Background(), ledgercontroller.NewGetAccountQuery("bank")) + account, err := store.Accounts().GetOne(context.Background(), ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("address", "bank"), + }) require.NoError(t, err, "account retrieval should not fail") require.Equal(t, "bank", account.Address, "account address should match") @@ -277,59 +302,79 @@ func TestAccountsGet(t *testing.T) { now := time.Now() ctx := logging.TestingContext() - err := store.CommitTransaction(ctx, pointer.For(ledger.NewTransaction().WithPostings( + tx1 := pointer.For(ledger.NewTransaction().WithPostings( ledger.NewPosting("world", "multi", "USD/2", big.NewInt(100)), - ).WithTimestamp(now))) + ).WithTimestamp(now)) + err := store.CommitTransaction(ctx, tx1) require.NoError(t, err) + // sleep for at least the time precision to ensure the next transaction is inserted with a different timestamp + libtime.Sleep(time.DatePrecision) + require.NoError(t, store.UpdateAccountsMetadata(ctx, map[string]metadata.Metadata{ "multi": { "category": "gold", }, })) - err = store.CommitTransaction(ctx, pointer.For(ledger.NewTransaction().WithPostings( + tx2 := pointer.For(ledger.NewTransaction().WithPostings( ledger.NewPosting("world", "multi", "USD/2", big.NewInt(0)), - ).WithTimestamp(now.Add(-time.Minute)))) + ).WithTimestamp(now.Add(-time.Minute))) + err = store.CommitTransaction(ctx, tx2) require.NoError(t, err) t.Run("find account", func(t *testing.T) { t.Parallel() - account, err := store.GetAccount(ctx, ledgercontroller.NewGetAccountQuery("multi")) + account, err := store.Accounts().GetOne(ctx, ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("address", "multi"), + }) require.NoError(t, err) require.Equal(t, ledger.Account{ Address: "multi", Metadata: metadata.Metadata{ "category": "gold", }, - FirstUsage: now.Add(-time.Minute), + FirstUsage: now.Add(-time.Minute), + InsertionDate: tx1.InsertedAt, + UpdatedAt: tx2.InsertedAt, }, *account) - account, err = store.GetAccount(ctx, ledgercontroller.NewGetAccountQuery("world")) + account, err = store.Accounts().GetOne(ctx, ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("address", "world"), + }) require.NoError(t, err) require.Equal(t, ledger.Account{ - Address: "world", - Metadata: metadata.Metadata{}, - FirstUsage: now.Add(-time.Minute), + Address: "world", + Metadata: metadata.Metadata{}, + FirstUsage: now.Add(-time.Minute), + InsertionDate: tx1.InsertedAt, + UpdatedAt: tx2.InsertedAt, }, *account) }) t.Run("find account in past", func(t *testing.T) { t.Parallel() - account, err := store.GetAccount(ctx, ledgercontroller.NewGetAccountQuery("multi").WithPIT(now.Add(-30*time.Second))) + account, err := store.Accounts().GetOne(ctx, ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("address", "multi"), + PIT: pointer.For(now.Add(-30 * time.Second)), + }) require.NoError(t, err) require.Equal(t, ledger.Account{ - Address: "multi", - Metadata: metadata.Metadata{}, - FirstUsage: now.Add(-time.Minute), + Address: "multi", + Metadata: metadata.Metadata{}, + FirstUsage: now.Add(-time.Minute), + InsertionDate: tx1.InsertedAt, + UpdatedAt: tx2.InsertedAt, }, *account) }) t.Run("find account with volumes", func(t *testing.T) { t.Parallel() - account, err := store.GetAccount(ctx, ledgercontroller.NewGetAccountQuery("multi"). - WithExpandVolumes()) + account, err := store.Accounts().GetOne(ctx, ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("address", "multi"), + Expand: []string{"volumes"}, + }) require.NoError(t, err) require.Equal(t, ledger.Account{ Address: "multi", @@ -340,13 +385,17 @@ func TestAccountsGet(t *testing.T) { Volumes: ledger.VolumesByAssets{ "USD/2": ledger.NewVolumesInt64(100, 0), }, + InsertionDate: tx1.InsertedAt, + UpdatedAt: tx2.InsertedAt, }, *account) }) t.Run("find account with effective volumes", func(t *testing.T) { t.Parallel() - account, err := store.GetAccount(ctx, ledgercontroller.NewGetAccountQuery("multi"). - WithExpandEffectiveVolumes()) + account, err := store.Accounts().GetOne(ctx, ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("address", "multi"), + Expand: []string{"effectiveVolumes"}, + }) require.NoError(t, err) require.Equal(t, ledger.Account{ Address: "multi", @@ -357,25 +406,34 @@ func TestAccountsGet(t *testing.T) { EffectiveVolumes: ledger.VolumesByAssets{ "USD/2": ledger.NewVolumesInt64(100, 0), }, + InsertionDate: tx1.InsertedAt, + UpdatedAt: tx2.InsertedAt, }, *account) }) t.Run("find account using pit", func(t *testing.T) { t.Parallel() - account, err := store.GetAccount(ctx, ledgercontroller.NewGetAccountQuery("multi").WithPIT(now)) + account, err := store.Accounts().GetOne(ctx, ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("address", "multi"), + PIT: pointer.For(now), + }) require.NoError(t, err) require.Equal(t, ledger.Account{ - Address: "multi", - Metadata: metadata.Metadata{}, - FirstUsage: now.Add(-time.Minute), + Address: "multi", + Metadata: metadata.Metadata{}, + FirstUsage: now.Add(-time.Minute), + InsertionDate: tx1.InsertedAt, + UpdatedAt: tx2.InsertedAt, }, *account) }) t.Run("not existent account", func(t *testing.T) { t.Parallel() - _, err := store.GetAccount(ctx, ledgercontroller.NewGetAccountQuery("account_not_existing")) + _, err := store.Accounts().GetOne(ctx, ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("address", "account_not_existing"), + }) require.Error(t, err) }) } @@ -391,7 +449,7 @@ func TestAccountsCount(t *testing.T) { ))) require.NoError(t, err) - countAccounts, err := store.CountAccounts(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}))) + countAccounts, err := store.Accounts().Count(ctx, ledgercontroller.ResourceQuery[any]{}) require.NoError(t, err) require.EqualValues(t, 2, countAccounts) // world + central_bank } diff --git a/internal/storage/ledger/balances.go b/internal/storage/ledger/balances.go index adbb07950..d702f7135 100644 --- a/internal/storage/ledger/balances.go +++ b/internal/storage/ledger/balances.go @@ -2,8 +2,6 @@ package ledger import ( "context" - "fmt" - "github.com/formancehq/ledger/pkg/features" "math/big" "strings" @@ -11,207 +9,23 @@ import ( "github.com/formancehq/ledger/internal/tracing" - "github.com/formancehq/go-libs/v2/query" - "github.com/formancehq/go-libs/v2/time" ledger "github.com/formancehq/ledger/internal" ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" - "github.com/uptrace/bun" ) -func (s *Store) selectAccountWithAssetAndVolumes(date *time.Time, useInsertionDate bool, builder query.Builder) (*bun.SelectQuery, error) { - - var ( - needMetadata bool - needAddressSegment bool - ) - - if builder != nil { - if err := builder.Walk(func(operator string, key string, value any) error { - switch { - case key == "address": - if err := s.validateAddressFilter(operator, value); err != nil { - return err - } - if !needAddressSegment { - // Cast is safe, the type has been validated by validatedAddressFilter - needAddressSegment = isSegmentedAddress(value.(string)) - } - - case key == "metadata": - needMetadata = true - if operator != "$exists" { - return ledgercontroller.NewErrInvalidQuery("'metadata' key filter can only be used with $exists") - } - case metadataRegex.Match([]byte(key)): - needMetadata = true - if operator != "$match" { - return ledgercontroller.NewErrInvalidQuery("'account' column can only be used with $match") - } - default: - return ledgercontroller.NewErrInvalidQuery("unknown key '%s' when building query", key) - } - return nil - }); err != nil { - return nil, err - } - } - - if needAddressSegment && !s.ledger.HasFeature(features.FeatureIndexAddressSegments, "ON") { - return nil, ledgercontroller.NewErrMissingFeature(features.FeatureIndexAddressSegments) - } - - var selectAccountsWithVolumes *bun.SelectQuery - if date != nil && !date.IsZero() { - if useInsertionDate { - if !s.ledger.HasFeature(features.FeatureMovesHistory, "ON") { - return nil, ledgercontroller.NewErrMissingFeature(features.FeatureMovesHistory) - } - selectDistinctMovesBySeq, err := s.SelectDistinctMovesBySeq(date) - if err != nil { - return nil, err - } - selectAccountsWithVolumes = s.db.NewSelect(). - TableExpr("(?) moves", selectDistinctMovesBySeq). - Column("asset", "accounts_address"). - ColumnExpr("post_commit_volumes as volumes") - } else { - if !s.ledger.HasFeature(features.FeatureMovesHistoryPostCommitEffectiveVolumes, "SYNC") { - return nil, ledgercontroller.NewErrMissingFeature(features.FeatureMovesHistoryPostCommitEffectiveVolumes) - } - selectAccountsWithVolumes = s.db.NewSelect(). - TableExpr("(?) moves", s.SelectDistinctMovesByEffectiveDate(date)). - Column("asset", "accounts_address"). - ColumnExpr("moves.post_commit_effective_volumes as volumes") - } - } else { - selectAccountsWithVolumes = s.db.NewSelect(). - ModelTableExpr(s.GetPrefixedRelationName("accounts_volumes")). - Column("asset", "accounts_address"). - ColumnExpr("(input, output)::"+s.GetPrefixedRelationName("volumes")+" as volumes"). - Where("ledger = ?", s.ledger.Name) - } - - selectAccountsWithVolumes = s.db.NewSelect(). - ColumnExpr("*"). - TableExpr("(?) accounts_volumes", selectAccountsWithVolumes) - - needAccount := needAddressSegment - if needMetadata { - if s.ledger.HasFeature(features.FeatureAccountMetadataHistory, "SYNC") && date != nil && !date.IsZero() { - selectAccountsWithVolumes = selectAccountsWithVolumes. - Join( - `left join (?) accounts_metadata on accounts_metadata.accounts_address = accounts_volumes.accounts_address`, - s.selectDistinctAccountMetadataHistories(date), - ) - } else { - needAccount = true - } - } - - if needAccount { - selectAccountsWithVolumes = s.db.NewSelect(). - TableExpr( - "(?) accounts", - selectAccountsWithVolumes. - Join("join "+s.GetPrefixedRelationName("accounts")+" accounts on accounts.address = accounts_volumes.accounts_address and ledger = ?", s.ledger.Name), - ). - ColumnExpr("address, asset, volumes, metadata"). - ColumnExpr("accounts.address_array as accounts_address_array") - } - - finalQuery := s.db.NewSelect(). - TableExpr("(?) accounts", selectAccountsWithVolumes) - - if builder != nil { - where, args, err := builder.Build(query.ContextFn(func(key, _ string, value any) (string, []any, error) { - switch { - case key == "address": - return filterAccountAddress(value.(string), "accounts_address"), nil, nil - case metadataRegex.Match([]byte(key)): - match := metadataRegex.FindAllStringSubmatch(key, 3) - - return "metadata @> ?", []any{map[string]any{ - match[0][1]: value, - }}, nil - - case key == "metadata": - return "metadata -> ? is not null", []any{value}, nil - default: - return "", nil, ledgercontroller.NewErrInvalidQuery("unknown key '%s' when building query", key) - } - })) - if err != nil { - return nil, fmt.Errorf("building where clause: %w", err) - } - finalQuery = finalQuery.Where(where, args...) - } - - return finalQuery, nil -} - -func (s *Store) selectAccountWithAggregatedVolumes(date *time.Time, useInsertionDate bool, alias string) (*bun.SelectQuery, error) { - selectAccountWithAssetAndVolumes, err := s.selectAccountWithAssetAndVolumes(date, useInsertionDate, nil) - if err != nil { - return nil, err - } - return s.db.NewSelect(). - TableExpr("(?) values", selectAccountWithAssetAndVolumes). - Group("accounts_address"). - Column("accounts_address"). - ColumnExpr("public.aggregate_objects(json_build_object(asset, json_build_object('input', (volumes).inputs, 'output', (volumes).outputs))::jsonb) as " + alias), nil -} - -func (s *Store) SelectAggregatedBalances(date *time.Time, useInsertionDate bool, builder query.Builder) (*bun.SelectQuery, error) { - - selectAccountsWithVolumes, err := s.selectAccountWithAssetAndVolumes(date, useInsertionDate, builder) - if err != nil { - return nil, err - } - sumVolumesForAsset := s.db.NewSelect(). - TableExpr("(?) values", selectAccountsWithVolumes). - Group("asset"). - Column("asset"). - ColumnExpr("json_build_object('input', sum(((volumes).inputs)::numeric), 'output', sum(((volumes).outputs)::numeric)) as volumes") - - return s.db.NewSelect(). - TableExpr("(?) values", sumVolumesForAsset). - ColumnExpr("aggregate_objects(json_build_object(asset, volumes)::jsonb) as aggregated"), nil -} - -func (s *Store) GetAggregatedBalances(ctx context.Context, q ledgercontroller.GetAggregatedBalanceQuery) (ledger.BalancesByAssets, error) { - type AggregatedVolumes struct { - Aggregated ledger.VolumesByAssets `bun:"aggregated,type:jsonb"` - } - - selectAggregatedBalances, err := s.SelectAggregatedBalances(q.PIT, q.UseInsertionDate, q.QueryBuilder) - if err != nil { - return nil, err - } - - aggregatedVolumes := AggregatedVolumes{} - if err := s.db.NewSelect(). - ModelTableExpr("(?) aggregated_volumes", selectAggregatedBalances). - Model(&aggregatedVolumes). - Scan(ctx); err != nil { - return nil, err - } - - return aggregatedVolumes.Aggregated.Balances(), nil -} - -func (s *Store) GetBalances(ctx context.Context, query ledgercontroller.BalanceQuery) (ledgercontroller.Balances, error) { +func (store *Store) GetBalances(ctx context.Context, query ledgercontroller.BalanceQuery) (ledgercontroller.Balances, error) { return tracing.TraceWithMetric( ctx, "GetBalances", - s.tracer, - s.getBalancesHistogram, + store.tracer, + store.getBalancesHistogram, func(ctx context.Context) (ledgercontroller.Balances, error) { conditions := make([]string, 0) args := make([]any, 0) for account, assets := range query { for _, asset := range assets { conditions = append(conditions, "ledger = ? and accounts_address = ? and asset = ?") - args = append(args, s.ledger.Name, account, asset) + args = append(args, store.ledger.Name, account, asset) } } @@ -224,7 +38,7 @@ func (s *Store) GetBalances(ctx context.Context, query ledgercontroller.BalanceQ for account, assets := range query { for _, asset := range assets { accountsVolumes = append(accountsVolumes, AccountsVolumesWithLedger{ - Ledger: s.ledger.Name, + Ledger: store.ledger.Name, AccountsVolumes: ledger.AccountsVolumes{ Account: account, Asset: asset, @@ -238,55 +52,55 @@ func (s *Store) GetBalances(ctx context.Context, query ledgercontroller.BalanceQ // Try to insert volumes using last move (to keep compat with previous version) or 0 values. // This way, if the account has a 0 balance at this point, it will be locked as any other accounts. // If the complete sql transaction fails, the account volumes will not be inserted. - selectMoves := s.db.NewSelect(). - ModelTableExpr(s.GetPrefixedRelationName("moves")). + selectMoves := store.db.NewSelect(). + ModelTableExpr(store.GetPrefixedRelationName("moves")). DistinctOn("accounts_address, asset"). Column("accounts_address", "asset"). ColumnExpr("first_value(post_commit_volumes) over (partition by accounts_address, asset order by seq desc) as post_commit_volumes"). ColumnExpr("first_value(ledger) over (partition by accounts_address, asset order by seq desc) as ledger"). Where("("+strings.Join(conditions, ") OR (")+")", args...) - zeroValuesAndMoves := s.db.NewSelect(). + zeroValuesAndMoves := store.db.NewSelect(). TableExpr("(?) data", selectMoves). Column("ledger", "accounts_address", "asset"). ColumnExpr("(post_commit_volumes).inputs as input"). ColumnExpr("(post_commit_volumes).outputs as output"). UnionAll( - s.db.NewSelect(). + store.db.NewSelect(). TableExpr( "(?) data", - s.db.NewSelect().NewValues(&accountsVolumes), + store.db.NewSelect().NewValues(&accountsVolumes), ). Column("*"), ) - zeroValueOrMoves := s.db.NewSelect(). + zeroValueOrMoves := store.db.NewSelect(). TableExpr("(?) data", zeroValuesAndMoves). Column("ledger", "accounts_address", "asset", "input", "output"). DistinctOn("ledger, accounts_address, asset") - insertDefaultValue := s.db.NewInsert(). - TableExpr(s.GetPrefixedRelationName("accounts_volumes")). + insertDefaultValue := store.db.NewInsert(). + TableExpr(store.GetPrefixedRelationName("accounts_volumes")). TableExpr("(" + zeroValueOrMoves.String() + ") data"). On("conflict (ledger, accounts_address, asset) do nothing"). Returning("ledger, accounts_address, asset, input, output") - selectExistingValues := s.db.NewSelect(). - ModelTableExpr(s.GetPrefixedRelationName("accounts_volumes")). + selectExistingValues := store.db.NewSelect(). + ModelTableExpr(store.GetPrefixedRelationName("accounts_volumes")). Column("ledger", "accounts_address", "asset", "input", "output"). Where("("+strings.Join(conditions, ") OR (")+")", args...). For("update"). // notes(gfyrag): Keep order, it ensures consistent locking order and limit deadlocks Order("accounts_address", "asset") - finalQuery := s.db.NewSelect(). + finalQuery := store.db.NewSelect(). With("inserted", insertDefaultValue). With("existing", selectExistingValues). ModelTableExpr( "(?) accounts_volumes", - s.db.NewSelect(). + store.db.NewSelect(). ModelTableExpr("inserted"). - UnionAll(s.db.NewSelect().ModelTableExpr("existing")), + UnionAll(store.db.NewSelect().ModelTableExpr("existing")), ). Model(&accountsVolumes) diff --git a/internal/storage/ledger/balances_test.go b/internal/storage/ledger/balances_test.go index 3507962df..3713294e9 100644 --- a/internal/storage/ledger/balances_test.go +++ b/internal/storage/ledger/balances_test.go @@ -239,106 +239,168 @@ func TestBalancesAggregates(t *testing.T) { t.Run("aggregate on all", func(t *testing.T) { t.Parallel() - cursor, err := store.GetAggregatedBalances(ctx, ledgercontroller.NewGetAggregatedBalancesQuery(ledgercontroller.PITFilter{}, nil, false)) + ret, err := store.AggregatedVolumes().GetOne(ctx, ledgercontroller.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{}) require.NoError(t, err) - RequireEqual(t, ledger.BalancesByAssets{ - "USD": big.NewInt(0), - "EUR": big.NewInt(0), - }, cursor) + RequireEqual(t, ledger.AggregatedVolumes{ + Aggregated: ledger.VolumesByAssets{ + "USD": ledger.Volumes{ + Input: big.NewInt(0).Add( + big.NewInt(0).Mul(bigInt, big.NewInt(2)), + big.NewInt(0).Mul(smallInt, big.NewInt(2)), + ), + Output: big.NewInt(0).Add( + big.NewInt(0).Mul(bigInt, big.NewInt(2)), + big.NewInt(0).Mul(smallInt, big.NewInt(2)), + ), + }, + "EUR": ledger.Volumes{ + Input: smallInt, + Output: smallInt, + }, + }, + }, *ret) }) t.Run("filter on address", func(t *testing.T) { t.Parallel() - ret, err := store.GetAggregatedBalances(ctx, ledgercontroller.NewGetAggregatedBalancesQuery(ledgercontroller.PITFilter{}, - query.Match("address", "users:"), false)) + + ret, err := store.AggregatedVolumes().GetOne(ctx, ledgercontroller.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{ + Builder: query.Match("address", "users:"), + }) require.NoError(t, err) - require.Equal(t, ledger.BalancesByAssets{ - "USD": big.NewInt(0).Add( - big.NewInt(0).Mul(bigInt, big.NewInt(2)), - big.NewInt(0).Mul(smallInt, big.NewInt(2)), - ), - }, ret) + RequireEqual(t, ledger.AggregatedVolumes{ + Aggregated: ledger.VolumesByAssets{ + "USD": ledger.Volumes{ + Input: big.NewInt(0).Add( + big.NewInt(0).Mul(bigInt, big.NewInt(2)), + big.NewInt(0).Mul(smallInt, big.NewInt(2)), + ), + Output: new(big.Int), + }, + }, + }, *ret) }) t.Run("using pit on effective date", func(t *testing.T) { t.Parallel() - ret, err := store.GetAggregatedBalances(ctx, ledgercontroller.NewGetAggregatedBalancesQuery(ledgercontroller.PITFilter{ - PIT: pointer.For(now.Add(-time.Second)), - }, query.Match("address", "users:"), false)) + ret, err := store.AggregatedVolumes().GetOne(ctx, ledgercontroller.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{ + Builder: query.Match("address", "users:"), + PIT: pointer.For(now.Add(-time.Second)), + }) require.NoError(t, err) - require.Equal(t, ledger.BalancesByAssets{ - "USD": big.NewInt(0).Add( - bigInt, - smallInt, - ), - }, ret) + RequireEqual(t, ledger.AggregatedVolumes{ + Aggregated: ledger.VolumesByAssets{ + "USD": ledger.Volumes{ + Input: big.NewInt(0).Add( + bigInt, + smallInt, + ), + Output: new(big.Int), + }, + }, + }, *ret) }) t.Run("using pit on insertion date", func(t *testing.T) { t.Parallel() - ret, err := store.GetAggregatedBalances(ctx, ledgercontroller.NewGetAggregatedBalancesQuery(ledgercontroller.PITFilter{ - PIT: pointer.For(now), - }, query.Match("address", "users:"), true)) + ret, err := store.AggregatedVolumes().GetOne(ctx, ledgercontroller.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{ + Builder: query.Match("address", "users:"), + PIT: pointer.For(now), + Opts: ledgercontroller.GetAggregatedVolumesOptions{ + UseInsertionDate: true, + }, + }) require.NoError(t, err) - require.Equal(t, ledger.BalancesByAssets{ - "USD": big.NewInt(0).Add( - bigInt, - smallInt, - ), - }, ret) + RequireEqual(t, ledger.AggregatedVolumes{ + Aggregated: ledger.VolumesByAssets{ + "USD": ledger.Volumes{ + Input: big.NewInt(0).Add( + bigInt, + smallInt, + ), + Output: new(big.Int), + }, + }, + }, *ret) }) t.Run("using a metadata and pit", func(t *testing.T) { t.Parallel() - ret, err := store.GetAggregatedBalances(ctx, ledgercontroller.NewGetAggregatedBalancesQuery(ledgercontroller.PITFilter{ - PIT: pointer.For(now.Add(time.Minute)), - }, query.Match("metadata[category]", "premium"), false)) + ret, err := store.AggregatedVolumes().GetOne(ctx, ledgercontroller.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{ + PIT: pointer.For(now.Add(time.Minute)), + Builder: query.Match("metadata[category]", "premium"), + }) require.NoError(t, err) - require.Equal(t, ledger.BalancesByAssets{ - "USD": big.NewInt(0).Add( - big.NewInt(0).Mul(bigInt, big.NewInt(2)), - big.NewInt(0), - ), - }, ret) + RequireEqual(t, ledger.AggregatedVolumes{ + Aggregated: ledger.VolumesByAssets{ + "USD": ledger.Volumes{ + Input: big.NewInt(0).Add( + big.NewInt(0).Mul(bigInt, big.NewInt(2)), + big.NewInt(0), + ), + Output: new(big.Int), + }, + }, + }, *ret) }) t.Run("using a metadata without pit", func(t *testing.T) { t.Parallel() - ret, err := store.GetAggregatedBalances(ctx, ledgercontroller.NewGetAggregatedBalancesQuery(ledgercontroller.PITFilter{}, - query.Match("metadata[category]", "premium"), false)) + ret, err := store.AggregatedVolumes().GetOne(ctx, ledgercontroller.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{ + Builder: query.Match("metadata[category]", "premium"), + }) require.NoError(t, err) - require.Equal(t, ledger.BalancesByAssets{ - "USD": big.NewInt(0).Mul(bigInt, big.NewInt(2)), - }, ret) + + RequireEqual(t, ledger.AggregatedVolumes{ + Aggregated: ledger.VolumesByAssets{ + "USD": ledger.Volumes{ + Input: big.NewInt(0).Mul(bigInt, big.NewInt(2)), + Output: new(big.Int), + }, + }, + }, *ret) }) t.Run("when no matching", func(t *testing.T) { t.Parallel() - ret, err := store.GetAggregatedBalances(ctx, ledgercontroller.NewGetAggregatedBalancesQuery(ledgercontroller.PITFilter{}, - query.Match("metadata[category]", "guest"), false)) + ret, err := store.AggregatedVolumes().GetOne(ctx, ledgercontroller.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{ + Builder: query.Match("metadata[category]", "guest"), + }) require.NoError(t, err) - require.Equal(t, ledger.BalancesByAssets{}, ret) + RequireEqual(t, ledger.AggregatedVolumes{ + Aggregated: ledger.VolumesByAssets{}, + }, *ret) }) t.Run("using a filter exist on metadata", func(t *testing.T) { t.Parallel() - ret, err := store.GetAggregatedBalances(ctx, ledgercontroller.NewGetAggregatedBalancesQuery(ledgercontroller.PITFilter{}, query.Exists("metadata", "category"), false)) + ret, err := store.AggregatedVolumes().GetOne(ctx, ledgercontroller.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{ + Builder: query.Exists("metadata", "category"), + }) require.NoError(t, err) - require.Equal(t, ledger.BalancesByAssets{ - "USD": big.NewInt(0).Add( - big.NewInt(0).Mul(bigInt, big.NewInt(2)), - big.NewInt(0).Mul(smallInt, big.NewInt(2)), - ), - }, ret) + RequireEqual(t, ledger.AggregatedVolumes{ + Aggregated: ledger.VolumesByAssets{ + "USD": ledger.Volumes{ + Input: big.NewInt(0).Add( + big.NewInt(0).Mul(bigInt, big.NewInt(2)), + big.NewInt(0).Mul(smallInt, big.NewInt(2)), + ), + Output: new(big.Int), + }, + }, + }, *ret) }) t.Run("using a filter on metadata and on address", func(t *testing.T) { t.Parallel() - ret, err := store.GetAggregatedBalances(ctx, ledgercontroller.NewGetAggregatedBalancesQuery( - ledgercontroller.PITFilter{}, - query.And( + ret, err := store.AggregatedVolumes().GetOne(ctx, ledgercontroller.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{ + Builder: query.And( query.Match("address", "users:"), query.Match("metadata[category]", "premium"), ), - false, - )) + }) require.NoError(t, err) - require.Equal(t, ledger.BalancesByAssets{ - "USD": big.NewInt(0).Mul(bigInt, big.NewInt(2)), - }, ret) + RequireEqual(t, ledger.AggregatedVolumes{ + Aggregated: ledger.VolumesByAssets{ + "USD": ledger.Volumes{ + Input: big.NewInt(0).Mul(bigInt, big.NewInt(2)), + Output: new(big.Int), + }, + }, + }, *ret) }) } diff --git a/internal/storage/ledger/debug.go b/internal/storage/ledger/debug.go index 4cb9c4689..79e79fa64 100644 --- a/internal/storage/ledger/debug.go +++ b/internal/storage/ledger/debug.go @@ -9,28 +9,28 @@ import ( ) //nolint:unused -func (s *Store) DumpTables(ctx context.Context, tables ...string) { +func (store *Store) DumpTables(ctx context.Context, tables ...string) { for _, table := range tables { - s.DumpQuery( + store.DumpQuery( ctx, - s.db.NewSelect(). - ModelTableExpr(s.GetPrefixedRelationName(table)), + store.db.NewSelect(). + ModelTableExpr(store.GetPrefixedRelationName(table)), ) } } //nolint:unused -func (s *Store) DumpQuery(ctx context.Context, query *bun.SelectQuery) { +func (store *Store) DumpQuery(ctx context.Context, query *bun.SelectQuery) { fmt.Println(query) rows, err := query.Rows(ctx) if err != nil { panic(err) } - s.DumpRows(rows) + store.DumpRows(rows) } //nolint:unused -func (s *Store) DumpRows(rows *sql.Rows) { +func (store *Store) DumpRows(rows *sql.Rows) { data, err := xsql.Pretty(rows) if err != nil { panic(err) diff --git a/internal/storage/ledger/errors.go b/internal/storage/ledger/errors.go deleted file mode 100644 index e8a53ec7f..000000000 --- a/internal/storage/ledger/errors.go +++ /dev/null @@ -1,11 +0,0 @@ -package ledger - -import ( - "errors" -) - -var ( - ErrBucketAlreadyExists = errors.New("bucket already exists") - ErrStoreAlreadyExists = errors.New("store already exists") - ErrStoreNotFound = errors.New("store not found") -) diff --git a/internal/storage/ledger/legacy/accounts.go b/internal/storage/ledger/legacy/accounts.go index aa9004697..ccadb0c77 100644 --- a/internal/storage/ledger/legacy/accounts.go +++ b/internal/storage/ledger/legacy/accounts.go @@ -14,7 +14,7 @@ import ( "github.com/uptrace/bun" ) -func (store *Store) buildAccountQuery(q ledgercontroller.PITFilterWithVolumes, query *bun.SelectQuery) *bun.SelectQuery { +func (store *Store) buildAccountQuery(q PITFilterWithVolumes, query *bun.SelectQuery) *bun.SelectQuery { query = query. Column("accounts.address", "accounts.first_usage"). @@ -55,7 +55,7 @@ func (store *Store) buildAccountQuery(q ledgercontroller.PITFilterWithVolumes, q return query } -func (store *Store) accountQueryContext(qb query.Builder, q ledgercontroller.ListAccountsQuery) (string, []any, error) { +func (store *Store) accountQueryContext(qb query.Builder, q ListAccountsQuery) (string, []any, error) { metadataRegex := regexp.MustCompile(`metadata\[(.+)]`) balanceRegex := regexp.MustCompile(`balance\[(.*)]`) @@ -134,7 +134,7 @@ func (store *Store) accountQueryContext(qb query.Builder, q ledgercontroller.Lis })) } -func (store *Store) buildAccountListQuery(selectQuery *bun.SelectQuery, q ledgercontroller.ListAccountsQuery, where string, args []any) *bun.SelectQuery { +func (store *Store) buildAccountListQuery(selectQuery *bun.SelectQuery, q ListAccountsQuery, where string, args []any) *bun.SelectQuery { selectQuery = store.buildAccountQuery(q.Options.Options, selectQuery) if where != "" { @@ -144,7 +144,7 @@ func (store *Store) buildAccountListQuery(selectQuery *bun.SelectQuery, q ledger return selectQuery } -func (store *Store) GetAccountsWithVolumes(ctx context.Context, q ledgercontroller.ListAccountsQuery) (*bunpaginate.Cursor[ledger.Account], error) { +func (store *Store) GetAccountsWithVolumes(ctx context.Context, q ListAccountsQuery) (*bunpaginate.Cursor[ledger.Account], error) { var ( where string args []any @@ -157,15 +157,15 @@ func (store *Store) GetAccountsWithVolumes(ctx context.Context, q ledgercontroll } } - return paginateWithOffset[ledgercontroller.PaginatedQueryOptions[ledgercontroller.PITFilterWithVolumes], ledger.Account](store, ctx, - (*bunpaginate.OffsetPaginatedQuery[ledgercontroller.PaginatedQueryOptions[ledgercontroller.PITFilterWithVolumes]])(&q), + return paginateWithOffset[ledgercontroller.PaginatedQueryOptions[PITFilterWithVolumes], ledger.Account](store, ctx, + (*bunpaginate.OffsetPaginatedQuery[ledgercontroller.PaginatedQueryOptions[PITFilterWithVolumes]])(&q), func(query *bun.SelectQuery) *bun.SelectQuery { return store.buildAccountListQuery(query, q, where, args) }, ) } -func (store *Store) GetAccountWithVolumes(ctx context.Context, q ledgercontroller.GetAccountQuery) (*ledger.Account, error) { +func (store *Store) GetAccountWithVolumes(ctx context.Context, q GetAccountQuery) (*ledger.Account, error) { account, err := fetch[*ledger.Account](store, true, ctx, func(query *bun.SelectQuery) *bun.SelectQuery { query = store.buildAccountQuery(q.PITFilterWithVolumes, query). Where("accounts.address = ?", q.Addr). @@ -179,7 +179,7 @@ func (store *Store) GetAccountWithVolumes(ctx context.Context, q ledgercontrolle return account, nil } -func (store *Store) CountAccounts(ctx context.Context, q ledgercontroller.ListAccountsQuery) (int, error) { +func (store *Store) CountAccounts(ctx context.Context, q ListAccountsQuery) (int, error) { var ( where string args []any diff --git a/internal/storage/ledger/legacy/accounts_test.go b/internal/storage/ledger/legacy/accounts_test.go index 653bcdf7d..62201eaa8 100644 --- a/internal/storage/ledger/legacy/accounts_test.go +++ b/internal/storage/ledger/legacy/accounts_test.go @@ -16,6 +16,7 @@ import ( "github.com/formancehq/go-libs/v2/metadata" "github.com/formancehq/go-libs/v2/query" ledger "github.com/formancehq/ledger/internal" + ledgerstore "github.com/formancehq/ledger/internal/storage/ledger/legacy" "github.com/stretchr/testify/require" ) @@ -67,57 +68,16 @@ func TestGetAccounts(t *testing.T) { WithInsertedAt(now.Add(200*time.Millisecond)))) require.NoError(t, err) - //require.NoError(t, store.InsertLogs(ctx, - // ledger.ChainLogs( - // ledger.NewTransactionLog( - // ledger.NewTransaction(). - // WithPostings(ledger.NewPosting("world", "account:1", "USD", big.NewInt(100))). - // WithDate(now), - // map[string]metadata.Metadata{ - // "account:1": { - // "category": "4", - // }, - // }, - // ).WithDate(now), - // ledger.NewSetMetadataOnAccountLog(time.Now(), "account:1", metadata.Metadata{"category": "1"}).WithDate(now.Add(time.Minute)), - // ledger.NewSetMetadataOnAccountLog(time.Now(), "account:2", metadata.Metadata{"category": "2"}).WithDate(now.Add(2*time.Minute)), - // ledger.NewSetMetadataOnAccountLog(time.Now(), "account:3", metadata.Metadata{"category": "3"}).WithDate(now.Add(3*time.Minute)), - // ledger.NewSetMetadataOnAccountLog(time.Now(), "orders:1", metadata.Metadata{"foo": "bar"}).WithDate(now.Add(3*time.Minute)), - // ledger.NewSetMetadataOnAccountLog(time.Now(), "orders:2", metadata.Metadata{"foo": "bar"}).WithDate(now.Add(3*time.Minute)), - // ledger.NewTransactionLog( - // ledger.NewTransaction(). - // WithPostings(ledger.NewPosting("world", "account:1", "USD", big.NewInt(100))). - // WithIDUint64(1). - // WithDate(now.Add(4*time.Minute)), - // map[string]metadata.Metadata{}, - // ).WithDate(now.Add(100*time.Millisecond)), - // ledger.NewTransactionLog( - // ledger.NewTransaction(). - // WithPostings(ledger.NewPosting("account:1", "bank", "USD", big.NewInt(50))). - // WithDate(now.Add(3*time.Minute)). - // WithIDUint64(2), - // map[string]metadata.Metadata{}, - // ).WithDate(now.Add(200*time.Millisecond)), - // ledger.NewTransactionLog( - // ledger.NewTransaction(). - // WithPostings(ledger.NewPosting("world", "account:1", "USD", big.NewInt(0))). - // WithDate(now.Add(-time.Minute)). - // WithIDUint64(3), - // map[string]metadata.Metadata{}, - // ).WithDate(now.Add(200*time.Millisecond)), - // )..., - //)) - t.Run("list all", func(t *testing.T) { t.Parallel() - accounts, err := store.GetAccountsWithVolumes(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}))) + accounts, err := store.GetAccountsWithVolumes(ctx, ledgerstore.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}))) require.NoError(t, err) require.Len(t, accounts.Data, 7) }) t.Run("list using metadata", func(t *testing.T) { t.Parallel() - accounts, err := store.GetAccountsWithVolumes(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). + accounts, err := store.GetAccountsWithVolumes(ctx, ledgerstore.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). WithQueryBuilder(query.Match("metadata[category]", "1")), )) require.NoError(t, err) @@ -126,8 +86,8 @@ func TestGetAccounts(t *testing.T) { t.Run("list before date", func(t *testing.T) { t.Parallel() - accounts, err := store.GetAccountsWithVolumes(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ + accounts, err := store.GetAccountsWithVolumes(ctx, ledgerstore.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{ + PITFilter: ledgerstore.PITFilter{ PIT: &now, }, }))) @@ -138,7 +98,7 @@ func TestGetAccounts(t *testing.T) { t.Run("list with volumes", func(t *testing.T) { t.Parallel() - accounts, err := store.GetAccountsWithVolumes(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ + accounts, err := store.GetAccountsWithVolumes(ctx, ledgerstore.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{ ExpandVolumes: true, }).WithQueryBuilder(query.Match("address", "account:1")))) require.NoError(t, err) @@ -151,8 +111,8 @@ func TestGetAccounts(t *testing.T) { t.Run("list with volumes using PIT", func(t *testing.T) { t.Parallel() - accounts, err := store.GetAccountsWithVolumes(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ + accounts, err := store.GetAccountsWithVolumes(ctx, ledgerstore.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{ + PITFilter: ledgerstore.PITFilter{ PIT: &now, }, ExpandVolumes: true, @@ -166,7 +126,7 @@ func TestGetAccounts(t *testing.T) { t.Run("list with effective volumes", func(t *testing.T) { t.Parallel() - accounts, err := store.GetAccountsWithVolumes(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ + accounts, err := store.GetAccountsWithVolumes(ctx, ledgerstore.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{ ExpandEffectiveVolumes: true, }).WithQueryBuilder(query.Match("address", "account:1")))) require.NoError(t, err) @@ -178,8 +138,8 @@ func TestGetAccounts(t *testing.T) { t.Run("list with effective volumes using PIT", func(t *testing.T) { t.Parallel() - accounts, err := store.GetAccountsWithVolumes(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ + accounts, err := store.GetAccountsWithVolumes(ctx, ledgerstore.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{ + PITFilter: ledgerstore.PITFilter{ PIT: &now, }, ExpandEffectiveVolumes: true, @@ -193,7 +153,7 @@ func TestGetAccounts(t *testing.T) { t.Run("list using filter on address", func(t *testing.T) { t.Parallel() - accounts, err := store.GetAccountsWithVolumes(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). + accounts, err := store.GetAccountsWithVolumes(ctx, ledgerstore.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). WithQueryBuilder(query.Match("address", "account:")), )) require.NoError(t, err) @@ -201,7 +161,7 @@ func TestGetAccounts(t *testing.T) { }) t.Run("list using filter on multiple address", func(t *testing.T) { t.Parallel() - accounts, err := store.GetAccountsWithVolumes(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). + accounts, err := store.GetAccountsWithVolumes(ctx, ledgerstore.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). WithQueryBuilder( query.Or( query.Match("address", "account:1"), @@ -214,13 +174,13 @@ func TestGetAccounts(t *testing.T) { }) t.Run("list using filter on balances", func(t *testing.T) { t.Parallel() - accounts, err := store.GetAccountsWithVolumes(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). + accounts, err := store.GetAccountsWithVolumes(ctx, ledgerstore.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). WithQueryBuilder(query.Lt("balance[USD]", 0)), )) require.NoError(t, err) require.Len(t, accounts.Data, 1) // world - accounts, err = store.GetAccountsWithVolumes(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). + accounts, err = store.GetAccountsWithVolumes(ctx, ledgerstore.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). WithQueryBuilder(query.Gt("balance[USD]", 0)), )) require.NoError(t, err) @@ -231,13 +191,13 @@ func TestGetAccounts(t *testing.T) { t.Run("list using filter on exists metadata", func(t *testing.T) { t.Parallel() - accounts, err := store.GetAccountsWithVolumes(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). + accounts, err := store.GetAccountsWithVolumes(ctx, ledgerstore.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). WithQueryBuilder(query.Exists("metadata", "foo")), )) require.NoError(t, err) require.Len(t, accounts.Data, 2) - accounts, err = store.GetAccountsWithVolumes(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). + accounts, err = store.GetAccountsWithVolumes(ctx, ledgerstore.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). WithQueryBuilder(query.Exists("metadata", "category")), )) require.NoError(t, err) @@ -246,7 +206,7 @@ func TestGetAccounts(t *testing.T) { t.Run("list using filter invalid field", func(t *testing.T) { t.Parallel() - _, err := store.GetAccountsWithVolumes(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). + _, err := store.GetAccountsWithVolumes(ctx, ledgerstore.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). WithQueryBuilder(query.Lt("invalid", 0)), )) require.Error(t, err) @@ -278,7 +238,7 @@ func TestGetAccount(t *testing.T) { t.Run("find account", func(t *testing.T) { t.Parallel() - account, err := store.GetAccountWithVolumes(ctx, ledgercontroller.NewGetAccountQuery("multi")) + account, err := store.GetAccountWithVolumes(ctx, ledgerstore.NewGetAccountQuery("multi")) require.NoError(t, err) require.Equal(t, ledger.Account{ Address: "multi", @@ -288,7 +248,7 @@ func TestGetAccount(t *testing.T) { FirstUsage: now.Add(-time.Minute), }, *account) - account, err = store.GetAccountWithVolumes(ctx, ledgercontroller.NewGetAccountQuery("world")) + account, err = store.GetAccountWithVolumes(ctx, ledgerstore.NewGetAccountQuery("world")) require.NoError(t, err) require.Equal(t, ledger.Account{ Address: "world", @@ -299,7 +259,7 @@ func TestGetAccount(t *testing.T) { t.Run("find account in past", func(t *testing.T) { t.Parallel() - account, err := store.GetAccountWithVolumes(ctx, ledgercontroller.NewGetAccountQuery("multi").WithPIT(now.Add(-30*time.Second))) + account, err := store.GetAccountWithVolumes(ctx, ledgerstore.NewGetAccountQuery("multi").WithPIT(now.Add(-30*time.Second))) require.NoError(t, err) require.Equal(t, ledger.Account{ Address: "multi", @@ -310,7 +270,7 @@ func TestGetAccount(t *testing.T) { t.Run("find account with volumes", func(t *testing.T) { t.Parallel() - account, err := store.GetAccountWithVolumes(ctx, ledgercontroller.NewGetAccountQuery("multi"). + account, err := store.GetAccountWithVolumes(ctx, ledgerstore.NewGetAccountQuery("multi"). WithExpandVolumes()) require.NoError(t, err) require.Equal(t, ledger.Account{ @@ -327,7 +287,7 @@ func TestGetAccount(t *testing.T) { t.Run("find account with effective volumes", func(t *testing.T) { t.Parallel() - account, err := store.GetAccountWithVolumes(ctx, ledgercontroller.NewGetAccountQuery("multi"). + account, err := store.GetAccountWithVolumes(ctx, ledgerstore.NewGetAccountQuery("multi"). WithExpandEffectiveVolumes()) require.NoError(t, err) require.Equal(t, ledger.Account{ @@ -345,7 +305,7 @@ func TestGetAccount(t *testing.T) { t.Run("find account using pit", func(t *testing.T) { t.Parallel() - account, err := store.GetAccountWithVolumes(ctx, ledgercontroller.NewGetAccountQuery("multi").WithPIT(now)) + account, err := store.GetAccountWithVolumes(ctx, ledgerstore.NewGetAccountQuery("multi").WithPIT(now)) require.NoError(t, err) require.Equal(t, ledger.Account{ Address: "multi", @@ -356,7 +316,7 @@ func TestGetAccount(t *testing.T) { t.Run("not existent account", func(t *testing.T) { t.Parallel() - _, err := store.GetAccountWithVolumes(ctx, ledgercontroller.NewGetAccountQuery("account_not_existing")) + _, err := store.GetAccountWithVolumes(ctx, ledgerstore.NewGetAccountQuery("account_not_existing")) require.Error(t, err) }) @@ -372,7 +332,7 @@ func TestCountAccounts(t *testing.T) { ))) require.NoError(t, err) - countAccounts, err := store.CountAccounts(ctx, ledgercontroller.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}))) + countAccounts, err := store.CountAccounts(ctx, ledgerstore.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}))) require.NoError(t, err) require.EqualValues(t, 2, countAccounts) // world + central_bank } diff --git a/internal/storage/ledger/legacy/adapters.go b/internal/storage/ledger/legacy/adapters.go index e6ad4b72a..3ea005891 100644 --- a/internal/storage/ledger/legacy/adapters.go +++ b/internal/storage/ledger/legacy/adapters.go @@ -3,7 +3,6 @@ package legacy import ( "context" "database/sql" - "github.com/formancehq/go-libs/v2/bun/bunpaginate" "github.com/formancehq/go-libs/v2/metadata" "github.com/formancehq/go-libs/v2/migrations" "github.com/formancehq/go-libs/v2/time" @@ -19,6 +18,27 @@ type DefaultStoreAdapter struct { isFullUpToDate bool } +// todo; handle compat with v1 +func (d *DefaultStoreAdapter) Accounts() ledgercontroller.PaginatedResource[ledger.Account, any, ledgercontroller.OffsetPaginatedQuery[any]] { + return d.newStore.Accounts() +} + +func (d *DefaultStoreAdapter) Logs() ledgercontroller.PaginatedResource[ledger.Log, any, ledgercontroller.ColumnPaginatedQuery[any]] { + return d.newStore.Logs() +} + +func (d *DefaultStoreAdapter) Transactions() ledgercontroller.PaginatedResource[ledger.Transaction, any, ledgercontroller.ColumnPaginatedQuery[any]] { + return d.newStore.Transactions() +} + +func (d *DefaultStoreAdapter) AggregatedBalances() ledgercontroller.Resource[ledger.AggregatedVolumes, ledgercontroller.GetAggregatedVolumesOptions] { + return d.newStore.AggregatedVolumes() +} + +func (d *DefaultStoreAdapter) Volumes() ledgercontroller.PaginatedResource[ledger.VolumesWithBalanceByAssetByAccount, ledgercontroller.GetVolumesOptions, ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]] { + return d.newStore.Volumes() +} + func (d *DefaultStoreAdapter) GetDB() bun.IDB { return d.newStore.GetDB() } @@ -47,7 +67,7 @@ func (d *DefaultStoreAdapter) UpdateAccountsMetadata(ctx context.Context, m map[ return d.newStore.UpdateAccountsMetadata(ctx, m) } -func (d *DefaultStoreAdapter) UpsertAccounts(ctx context.Context, accounts ... *ledger.Account) error { +func (d *DefaultStoreAdapter) UpsertAccounts(ctx context.Context, accounts ...*ledger.Account) error { return d.newStore.UpsertAccounts(ctx, accounts...) } @@ -63,82 +83,10 @@ func (d *DefaultStoreAdapter) LockLedger(ctx context.Context) error { return d.newStore.LockLedger(ctx) } -func (d *DefaultStoreAdapter) ListLogs(ctx context.Context, q ledgercontroller.GetLogsQuery) (*bunpaginate.Cursor[ledger.Log], error) { - if !d.isFullUpToDate { - return d.legacyStore.GetLogs(ctx, q) - } - - return d.newStore.ListLogs(ctx, q) -} - func (d *DefaultStoreAdapter) ReadLogWithIdempotencyKey(ctx context.Context, ik string) (*ledger.Log, error) { return d.newStore.ReadLogWithIdempotencyKey(ctx, ik) } -func (d *DefaultStoreAdapter) ListTransactions(ctx context.Context, q ledgercontroller.ListTransactionsQuery) (*bunpaginate.Cursor[ledger.Transaction], error) { - if !d.isFullUpToDate { - return d.legacyStore.GetTransactions(ctx, q) - } - - return d.newStore.ListTransactions(ctx, q) -} - -func (d *DefaultStoreAdapter) CountTransactions(ctx context.Context, q ledgercontroller.ListTransactionsQuery) (int, error) { - if !d.isFullUpToDate { - return d.legacyStore.CountTransactions(ctx, q) - } - - return d.newStore.CountTransactions(ctx, q) -} - -func (d *DefaultStoreAdapter) GetTransaction(ctx context.Context, query ledgercontroller.GetTransactionQuery) (*ledger.Transaction, error) { - if !d.isFullUpToDate { - return d.legacyStore.GetTransactionWithVolumes(ctx, query) - } - - return d.newStore.GetTransaction(ctx, query) -} - -func (d *DefaultStoreAdapter) CountAccounts(ctx context.Context, q ledgercontroller.ListAccountsQuery) (int, error) { - if !d.isFullUpToDate { - return d.legacyStore.CountAccounts(ctx, q) - } - - return d.newStore.CountAccounts(ctx, q) -} - -func (d *DefaultStoreAdapter) ListAccounts(ctx context.Context, q ledgercontroller.ListAccountsQuery) (*bunpaginate.Cursor[ledger.Account], error) { - if !d.isFullUpToDate { - return d.legacyStore.GetAccountsWithVolumes(ctx, q) - } - - return d.newStore.ListAccounts(ctx, q) -} - -func (d *DefaultStoreAdapter) GetAccount(ctx context.Context, q ledgercontroller.GetAccountQuery) (*ledger.Account, error) { - if !d.isFullUpToDate { - return d.legacyStore.GetAccountWithVolumes(ctx, q) - } - - return d.newStore.GetAccount(ctx, q) -} - -func (d *DefaultStoreAdapter) GetAggregatedBalances(ctx context.Context, q ledgercontroller.GetAggregatedBalanceQuery) (ledger.BalancesByAssets, error) { - if !d.isFullUpToDate { - return d.legacyStore.GetAggregatedBalances(ctx, q) - } - - return d.newStore.GetAggregatedBalances(ctx, q) -} - -func (d *DefaultStoreAdapter) GetVolumesWithBalances(ctx context.Context, q ledgercontroller.GetVolumesWithBalancesQuery) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { - if !d.isFullUpToDate { - return d.legacyStore.GetVolumesWithBalances(ctx, q) - } - - return d.newStore.GetVolumesWithBalances(ctx, q) -} - func (d *DefaultStoreAdapter) IsUpToDate(ctx context.Context) (bool, error) { return d.newStore.HasMinimalVersion(ctx) } diff --git a/internal/storage/ledger/legacy/balances.go b/internal/storage/ledger/legacy/balances.go index 5266fdb6d..7a3b02cbd 100644 --- a/internal/storage/ledger/legacy/balances.go +++ b/internal/storage/ledger/legacy/balances.go @@ -7,11 +7,10 @@ import ( "github.com/formancehq/go-libs/v2/platform/postgres" "github.com/formancehq/go-libs/v2/query" ledger "github.com/formancehq/ledger/internal" - ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" "github.com/uptrace/bun" ) -func (store *Store) GetAggregatedBalances(ctx context.Context, q ledgercontroller.GetAggregatedBalanceQuery) (ledger.BalancesByAssets, error) { +func (store *Store) GetAggregatedBalances(ctx context.Context, q GetAggregatedBalanceQuery) (ledger.BalancesByAssets, error) { var ( needMetadata bool diff --git a/internal/storage/ledger/legacy/balances_test.go b/internal/storage/ledger/legacy/balances_test.go index d6c185946..e2adaac9d 100644 --- a/internal/storage/ledger/legacy/balances_test.go +++ b/internal/storage/ledger/legacy/balances_test.go @@ -3,7 +3,7 @@ package legacy_test import ( - ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" + ledgerstore "github.com/formancehq/ledger/internal/storage/ledger/legacy" "github.com/google/go-cmp/cmp" "math/big" "testing" @@ -74,7 +74,7 @@ func TestGetBalancesAggregated(t *testing.T) { t.Run("aggregate on all", func(t *testing.T) { t.Parallel() - cursor, err := store.GetAggregatedBalances(ctx, ledgercontroller.NewGetAggregatedBalancesQuery(ledgercontroller.PITFilter{}, nil, false)) + cursor, err := store.GetAggregatedBalances(ctx, ledgerstore.NewGetAggregatedBalancesQuery(ledgerstore.PITFilter{}, nil, false)) require.NoError(t, err) RequireEqual(t, ledger.BalancesByAssets{ "USD": big.NewInt(0), @@ -83,7 +83,7 @@ func TestGetBalancesAggregated(t *testing.T) { }) t.Run("filter on address", func(t *testing.T) { t.Parallel() - ret, err := store.GetAggregatedBalances(ctx, ledgercontroller.NewGetAggregatedBalancesQuery(ledgercontroller.PITFilter{}, + ret, err := store.GetAggregatedBalances(ctx, ledgerstore.NewGetAggregatedBalancesQuery(ledgerstore.PITFilter{}, query.Match("address", "users:"), false)) require.NoError(t, err) require.Equal(t, ledger.BalancesByAssets{ @@ -95,7 +95,7 @@ func TestGetBalancesAggregated(t *testing.T) { }) t.Run("using pit on effective date", func(t *testing.T) { t.Parallel() - ret, err := store.GetAggregatedBalances(ctx, ledgercontroller.NewGetAggregatedBalancesQuery(ledgercontroller.PITFilter{ + ret, err := store.GetAggregatedBalances(ctx, ledgerstore.NewGetAggregatedBalancesQuery(ledgerstore.PITFilter{ PIT: pointer.For(now.Add(-time.Second)), }, query.Match("address", "users:"), false)) require.NoError(t, err) @@ -108,7 +108,7 @@ func TestGetBalancesAggregated(t *testing.T) { }) t.Run("using pit on insertion date", func(t *testing.T) { t.Parallel() - ret, err := store.GetAggregatedBalances(ctx, ledgercontroller.NewGetAggregatedBalancesQuery(ledgercontroller.PITFilter{ + ret, err := store.GetAggregatedBalances(ctx, ledgerstore.NewGetAggregatedBalancesQuery(ledgerstore.PITFilter{ PIT: pointer.For(now), }, query.Match("address", "users:"), true)) require.NoError(t, err) @@ -122,7 +122,7 @@ func TestGetBalancesAggregated(t *testing.T) { t.Run("using a metadata and pit", func(t *testing.T) { t.Parallel() - ret, err := store.GetAggregatedBalances(ctx, ledgercontroller.NewGetAggregatedBalancesQuery(ledgercontroller.PITFilter{ + ret, err := store.GetAggregatedBalances(ctx, ledgerstore.NewGetAggregatedBalancesQuery(ledgerstore.PITFilter{ PIT: pointer.For(now.Add(time.Minute)), }, query.Match("metadata[category]", "premium"), false)) require.NoError(t, err) @@ -135,7 +135,7 @@ func TestGetBalancesAggregated(t *testing.T) { }) t.Run("using a metadata without pit", func(t *testing.T) { t.Parallel() - ret, err := store.GetAggregatedBalances(ctx, ledgercontroller.NewGetAggregatedBalancesQuery(ledgercontroller.PITFilter{}, + ret, err := store.GetAggregatedBalances(ctx, ledgerstore.NewGetAggregatedBalancesQuery(ledgerstore.PITFilter{}, query.Match("metadata[category]", "premium"), false)) require.NoError(t, err) require.Equal(t, ledger.BalancesByAssets{ @@ -144,7 +144,7 @@ func TestGetBalancesAggregated(t *testing.T) { }) t.Run("when no matching", func(t *testing.T) { t.Parallel() - ret, err := store.GetAggregatedBalances(ctx, ledgercontroller.NewGetAggregatedBalancesQuery(ledgercontroller.PITFilter{}, + ret, err := store.GetAggregatedBalances(ctx, ledgerstore.NewGetAggregatedBalancesQuery(ledgerstore.PITFilter{}, query.Match("metadata[category]", "guest"), false)) require.NoError(t, err) require.Equal(t, ledger.BalancesByAssets{}, ret) @@ -152,7 +152,7 @@ func TestGetBalancesAggregated(t *testing.T) { t.Run("using a filter exist on metadata", func(t *testing.T) { t.Parallel() - ret, err := store.GetAggregatedBalances(ctx, ledgercontroller.NewGetAggregatedBalancesQuery(ledgercontroller.PITFilter{}, query.Exists("metadata", "category"), false)) + ret, err := store.GetAggregatedBalances(ctx, ledgerstore.NewGetAggregatedBalancesQuery(ledgerstore.PITFilter{}, query.Exists("metadata", "category"), false)) require.NoError(t, err) require.Equal(t, ledger.BalancesByAssets{ "USD": big.NewInt(0).Add( diff --git a/internal/storage/ledger/legacy/logs.go b/internal/storage/ledger/legacy/logs.go index 069f4fd94..a022aa789 100644 --- a/internal/storage/ledger/legacy/logs.go +++ b/internal/storage/ledger/legacy/logs.go @@ -35,7 +35,7 @@ func (store *Store) logsQueryBuilder(q ledgercontroller.PaginatedQueryOptions[an } } -func (store *Store) GetLogs(ctx context.Context, q ledgercontroller.GetLogsQuery) (*bunpaginate.Cursor[ledger.Log], error) { +func (store *Store) GetLogs(ctx context.Context, q GetLogsQuery) (*bunpaginate.Cursor[ledger.Log], error) { logs, err := paginateWithColumn[ledgercontroller.PaginatedQueryOptions[any], ledgerstore.Log](store, ctx, (*bunpaginate.ColumnPaginatedQuery[ledgercontroller.PaginatedQueryOptions[any]])(&q), store.logsQueryBuilder(q.Options), diff --git a/internal/storage/ledger/legacy/logs_test.go b/internal/storage/ledger/legacy/logs_test.go index 1f4f297f5..6e6e298f9 100644 --- a/internal/storage/ledger/legacy/logs_test.go +++ b/internal/storage/ledger/legacy/logs_test.go @@ -4,6 +4,7 @@ package legacy_test import ( ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" + ledgerstore "github.com/formancehq/ledger/internal/storage/ledger/legacy" "testing" "github.com/formancehq/go-libs/v2/bun/bunpaginate" @@ -33,20 +34,20 @@ func TestLogsList(t *testing.T) { require.NoError(t, err) } - cursor, err := store.GetLogs(ctx, ledgercontroller.NewListLogsQuery(ledgercontroller.NewPaginatedQueryOptions[any](nil))) + cursor, err := store.GetLogs(ctx, ledgerstore.NewListLogsQuery(ledgercontroller.NewPaginatedQueryOptions[any](nil))) require.NoError(t, err) require.Equal(t, bunpaginate.QueryDefaultPageSize, cursor.PageSize) require.Equal(t, 3, len(cursor.Data)) require.EqualValues(t, 3, cursor.Data[0].ID) - cursor, err = store.GetLogs(ctx, ledgercontroller.NewListLogsQuery(ledgercontroller.NewPaginatedQueryOptions[any](nil).WithPageSize(1))) + cursor, err = store.GetLogs(ctx, ledgerstore.NewListLogsQuery(ledgercontroller.NewPaginatedQueryOptions[any](nil).WithPageSize(1))) require.NoError(t, err) // Should get only the first log. require.Equal(t, 1, cursor.PageSize) require.EqualValues(t, 3, cursor.Data[0].ID) - cursor, err = store.GetLogs(ctx, ledgercontroller.NewListLogsQuery(ledgercontroller.NewPaginatedQueryOptions[any](nil). + cursor, err = store.GetLogs(ctx, ledgerstore.NewListLogsQuery(ledgercontroller.NewPaginatedQueryOptions[any](nil). WithQueryBuilder(query.And( query.Gte("date", now.Add(-2*time.Hour)), query.Lt("date", now.Add(-time.Hour)), diff --git a/internal/storage/ledger/legacy/queries.go b/internal/storage/ledger/legacy/queries.go new file mode 100644 index 000000000..ade76c7aa --- /dev/null +++ b/internal/storage/ledger/legacy/queries.go @@ -0,0 +1,159 @@ +package legacy + +import ( + "github.com/formancehq/go-libs/v2/bun/bunpaginate" + "github.com/formancehq/go-libs/v2/pointer" + "github.com/formancehq/go-libs/v2/query" + "github.com/formancehq/go-libs/v2/time" + "github.com/formancehq/ledger/internal/controller/ledger" +) + +type PITFilter struct { + PIT *time.Time `json:"pit"` + OOT *time.Time `json:"oot"` +} + +type PITFilterWithVolumes struct { + PITFilter + ExpandVolumes bool `json:"volumes"` + ExpandEffectiveVolumes bool `json:"effectiveVolumes"` +} + +type FiltersForVolumes struct { + PITFilter + UseInsertionDate bool + GroupLvl int +} + +func NewGetVolumesWithBalancesQuery(opts ledger.PaginatedQueryOptions[FiltersForVolumes]) GetVolumesWithBalancesQuery { + return GetVolumesWithBalancesQuery{ + PageSize: opts.PageSize, + Order: bunpaginate.OrderAsc, + Options: opts, + } +} + +type ListTransactionsQuery bunpaginate.ColumnPaginatedQuery[ledger.PaginatedQueryOptions[PITFilterWithVolumes]] + +func (q ListTransactionsQuery) WithColumn(column string) ListTransactionsQuery { + ret := pointer.For((bunpaginate.ColumnPaginatedQuery[ledger.PaginatedQueryOptions[PITFilterWithVolumes]])(q)) + ret = ret.WithColumn(column) + + return ListTransactionsQuery(*ret) +} + +func NewListTransactionsQuery(options ledger.PaginatedQueryOptions[PITFilterWithVolumes]) ListTransactionsQuery { + return ListTransactionsQuery{ + PageSize: options.PageSize, + Column: "id", + Order: bunpaginate.OrderDesc, + Options: options, + } +} + +type GetTransactionQuery struct { + PITFilterWithVolumes + ID int +} + +func (q GetTransactionQuery) WithExpandVolumes() GetTransactionQuery { + q.ExpandVolumes = true + + return q +} + +func (q GetTransactionQuery) WithExpandEffectiveVolumes() GetTransactionQuery { + q.ExpandEffectiveVolumes = true + + return q +} + +func NewGetTransactionQuery(id int) GetTransactionQuery { + return GetTransactionQuery{ + PITFilterWithVolumes: PITFilterWithVolumes{}, + ID: id, + } +} + +type ListAccountsQuery bunpaginate.OffsetPaginatedQuery[ledger.PaginatedQueryOptions[PITFilterWithVolumes]] + +func (q ListAccountsQuery) WithExpandVolumes() ListAccountsQuery { + q.Options.Options.ExpandVolumes = true + + return q +} + +func (q ListAccountsQuery) WithExpandEffectiveVolumes() ListAccountsQuery { + q.Options.Options.ExpandEffectiveVolumes = true + + return q +} + +func NewListAccountsQuery(opts ledger.PaginatedQueryOptions[PITFilterWithVolumes]) ListAccountsQuery { + return ListAccountsQuery{ + PageSize: opts.PageSize, + Order: bunpaginate.OrderAsc, + Options: opts, + } +} + +type GetAccountQuery struct { + PITFilterWithVolumes + Addr string +} + +func (q GetAccountQuery) WithPIT(pit time.Time) GetAccountQuery { + q.PIT = &pit + + return q +} + +func (q GetAccountQuery) WithExpandVolumes() GetAccountQuery { + q.ExpandVolumes = true + + return q +} + +func (q GetAccountQuery) WithExpandEffectiveVolumes() GetAccountQuery { + q.ExpandEffectiveVolumes = true + + return q +} + +func NewGetAccountQuery(addr string) GetAccountQuery { + return GetAccountQuery{ + Addr: addr, + } +} + +type GetAggregatedBalanceQuery struct { + PITFilter + QueryBuilder query.Builder + UseInsertionDate bool +} + +func NewGetAggregatedBalancesQuery(filter PITFilter, qb query.Builder, useInsertionDate bool) GetAggregatedBalanceQuery { + return GetAggregatedBalanceQuery{ + PITFilter: filter, + QueryBuilder: qb, + UseInsertionDate: useInsertionDate, + } +} + +type GetVolumesWithBalancesQuery bunpaginate.OffsetPaginatedQuery[ledger.PaginatedQueryOptions[FiltersForVolumes]] + +type GetLogsQuery bunpaginate.ColumnPaginatedQuery[ledger.PaginatedQueryOptions[any]] + +func (q GetLogsQuery) WithOrder(order bunpaginate.Order) GetLogsQuery { + q.Order = order + return q +} + +func NewListLogsQuery(options ledger.PaginatedQueryOptions[any]) GetLogsQuery { + return GetLogsQuery{ + PageSize: options.PageSize, + Column: "id", + Order: bunpaginate.OrderDesc, + Options: options, + } +} diff --git a/internal/storage/ledger/legacy/transactions.go b/internal/storage/ledger/legacy/transactions.go index a7dfa99ae..b9bd8399a 100644 --- a/internal/storage/ledger/legacy/transactions.go +++ b/internal/storage/ledger/legacy/transactions.go @@ -20,7 +20,7 @@ var ( metadataRegex = regexp.MustCompile(`metadata\[(.+)]`) ) -func (store *Store) buildTransactionQuery(p ledgercontroller.PITFilterWithVolumes, query *bun.SelectQuery) *bun.SelectQuery { +func (store *Store) buildTransactionQuery(p PITFilterWithVolumes, query *bun.SelectQuery) *bun.SelectQuery { selectMetadata := query.NewSelect(). ModelTableExpr(store.GetPrefixedRelationName("transactions_metadata")). @@ -64,7 +64,7 @@ func (store *Store) buildTransactionQuery(p ledgercontroller.PITFilterWithVolume return query } -func (store *Store) transactionQueryContext(qb query.Builder, q ledgercontroller.ListTransactionsQuery) (string, []any, error) { +func (store *Store) transactionQueryContext(qb query.Builder, q ListTransactionsQuery) (string, []any, error) { return qb.Build(query.ContextFn(func(key, operator string, value any) (string, []any, error) { switch { @@ -144,7 +144,7 @@ func (store *Store) transactionQueryContext(qb query.Builder, q ledgercontroller })) } -func (store *Store) buildTransactionListQuery(selectQuery *bun.SelectQuery, q ledgercontroller.PaginatedQueryOptions[ledgercontroller.PITFilterWithVolumes], where string, args []any) *bun.SelectQuery { +func (store *Store) buildTransactionListQuery(selectQuery *bun.SelectQuery, q ledgercontroller.PaginatedQueryOptions[PITFilterWithVolumes], where string, args []any) *bun.SelectQuery { selectQuery = store.buildTransactionQuery(q.Options, selectQuery) if where != "" { @@ -154,7 +154,7 @@ func (store *Store) buildTransactionListQuery(selectQuery *bun.SelectQuery, q le return selectQuery } -func (store *Store) GetTransactions(ctx context.Context, q ledgercontroller.ListTransactionsQuery) (*bunpaginate.Cursor[ledger.Transaction], error) { +func (store *Store) GetTransactions(ctx context.Context, q ListTransactionsQuery) (*bunpaginate.Cursor[ledger.Transaction], error) { var ( where string @@ -168,15 +168,15 @@ func (store *Store) GetTransactions(ctx context.Context, q ledgercontroller.List } } - return paginateWithColumn[ledgercontroller.PaginatedQueryOptions[ledgercontroller.PITFilterWithVolumes], ledger.Transaction](store, ctx, - (*bunpaginate.ColumnPaginatedQuery[ledgercontroller.PaginatedQueryOptions[ledgercontroller.PITFilterWithVolumes]])(&q), + return paginateWithColumn[ledgercontroller.PaginatedQueryOptions[PITFilterWithVolumes], ledger.Transaction](store, ctx, + (*bunpaginate.ColumnPaginatedQuery[ledgercontroller.PaginatedQueryOptions[PITFilterWithVolumes]])(&q), func(query *bun.SelectQuery) *bun.SelectQuery { return store.buildTransactionListQuery(query, q.Options, where, args) }, ) } -func (store *Store) CountTransactions(ctx context.Context, q ledgercontroller.ListTransactionsQuery) (int, error) { +func (store *Store) CountTransactions(ctx context.Context, q ListTransactionsQuery) (int, error) { var ( where string @@ -196,7 +196,7 @@ func (store *Store) CountTransactions(ctx context.Context, q ledgercontroller.Li }) } -func (store *Store) GetTransactionWithVolumes(ctx context.Context, filter ledgercontroller.GetTransactionQuery) (*ledger.Transaction, error) { +func (store *Store) GetTransactionWithVolumes(ctx context.Context, filter GetTransactionQuery) (*ledger.Transaction, error) { return fetch[*ledger.Transaction](store, true, ctx, func(query *bun.SelectQuery) *bun.SelectQuery { return store.buildTransactionQuery(filter.PITFilterWithVolumes, query). diff --git a/internal/storage/ledger/legacy/transactions_test.go b/internal/storage/ledger/legacy/transactions_test.go index 090a6be47..23b778f5a 100644 --- a/internal/storage/ledger/legacy/transactions_test.go +++ b/internal/storage/ledger/legacy/transactions_test.go @@ -6,6 +6,7 @@ import ( "context" "fmt" ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" + ledgerstore "github.com/formancehq/ledger/internal/storage/ledger/legacy" "math/big" "testing" @@ -46,7 +47,7 @@ func TestGetTransactionWithVolumes(t *testing.T) { err = store.newStore.CommitTransaction(ctx, &tx2) require.NoError(t, err) - tx, err := store.GetTransactionWithVolumes(ctx, ledgercontroller.NewGetTransactionQuery(tx1.ID). + tx, err := store.GetTransactionWithVolumes(ctx, ledgerstore.NewGetTransactionQuery(tx1.ID). WithExpandVolumes(). WithExpandEffectiveVolumes()) require.NoError(t, err) @@ -68,7 +69,7 @@ func TestGetTransactionWithVolumes(t *testing.T) { }, }, tx.PostCommitVolumes) - tx, err = store.GetTransactionWithVolumes(ctx, ledgercontroller.NewGetTransactionQuery(tx2.ID). + tx, err = store.GetTransactionWithVolumes(ctx, ledgerstore.NewGetTransactionQuery(tx2.ID). WithExpandVolumes(). WithExpandEffectiveVolumes()) require.NoError(t, err) @@ -104,7 +105,7 @@ func TestCountTransactions(t *testing.T) { require.NoError(t, err) } - count, err := store.CountTransactions(context.Background(), ledgercontroller.NewListTransactionsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}))) + count, err := store.CountTransactions(context.Background(), ledgerstore.NewListTransactionsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}))) require.NoError(t, err, "counting transactions should not fail") require.Equal(t, 3, count, "count should be equal") } @@ -158,7 +159,7 @@ func TestGetTransactions(t *testing.T) { // refresh tx3 // we can't take the result of the call on RevertTransaction nor UpdateTransactionMetadata as the result does not contains pc(e)v tx3 := func() ledger.Transaction { - tx3, err := store.newStore.GetTransaction(ctx, ledgercontroller.NewGetTransactionQuery(tx3BeforeRevert.ID). + tx3, err := store.Store.GetTransactionWithVolumes(ctx, ledgerstore.NewGetTransactionQuery(tx3BeforeRevert.ID). WithExpandVolumes(). WithExpandEffectiveVolumes()) require.NoError(t, err) @@ -175,44 +176,44 @@ func TestGetTransactions(t *testing.T) { type testCase struct { name string - query ledgercontroller.PaginatedQueryOptions[ledgercontroller.PITFilterWithVolumes] + query ledgercontroller.PaginatedQueryOptions[ledgerstore.PITFilterWithVolumes] expected []ledger.Transaction expectError error } testCases := []testCase{ { name: "nominal", - query: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}), + query: ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}), expected: []ledger.Transaction{tx5, tx4, tx3, tx2, tx1}, }, { name: "address filter", - query: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). + query: ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). WithQueryBuilder(query.Match("account", "bob")), expected: []ledger.Transaction{tx2}, }, { name: "address filter using segments matching two addresses by individual segments", - query: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). + query: ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). WithQueryBuilder(query.Match("account", "users:amazon")), expected: []ledger.Transaction{}, }, { name: "address filter using segment", - query: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). + query: ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). WithQueryBuilder(query.Match("account", "users:")), expected: []ledger.Transaction{tx5, tx4, tx3}, }, { name: "filter using metadata", - query: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). + query: ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). WithQueryBuilder(query.Match("metadata[category]", "2")), expected: []ledger.Transaction{tx2}, }, { name: "using point in time", - query: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ + query: ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{ + PITFilter: ledgerstore.PITFilter{ PIT: pointer.For(now.Add(-time.Hour)), }, }), @@ -220,20 +221,20 @@ func TestGetTransactions(t *testing.T) { }, { name: "reverted transactions", - query: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). + query: ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). WithQueryBuilder(query.Match("reverted", true)), expected: []ledger.Transaction{tx3}, }, { name: "filter using exists metadata", - query: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). + query: ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). WithQueryBuilder(query.Exists("metadata", "category")), expected: []ledger.Transaction{tx3, tx2, tx1}, }, { name: "filter using metadata and pit", - query: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ + query: ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{ + PITFilter: ledgerstore.PITFilter{ PIT: pointer.For(tx3.Timestamp), }, }). @@ -242,13 +243,13 @@ func TestGetTransactions(t *testing.T) { }, { name: "filter using not exists metadata", - query: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). + query: ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). WithQueryBuilder(query.Not(query.Exists("metadata", "category"))), expected: []ledger.Transaction{tx5, tx4}, }, { name: "filter using timestamp", - query: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). + query: ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). WithQueryBuilder(query.Match("timestamp", tx5.Timestamp.Format(time.RFC3339Nano))), expected: []ledger.Transaction{tx5, tx4}, }, @@ -262,7 +263,7 @@ func TestGetTransactions(t *testing.T) { tc.query.Options.ExpandVolumes = true tc.query.Options.ExpandEffectiveVolumes = true - cursor, err := store.GetTransactions(ctx, ledgercontroller.NewListTransactionsQuery(tc.query)) + cursor, err := store.GetTransactions(ctx, ledgerstore.NewListTransactionsQuery(tc.query)) if tc.expectError != nil { require.True(t, errors.Is(err, tc.expectError)) } else { @@ -270,7 +271,7 @@ func TestGetTransactions(t *testing.T) { require.Len(t, cursor.Data, len(tc.expected)) RequireEqual(t, tc.expected, cursor.Data) - count, err := store.CountTransactions(ctx, ledgercontroller.NewListTransactionsQuery(tc.query)) + count, err := store.CountTransactions(ctx, ledgerstore.NewListTransactionsQuery(tc.query)) require.NoError(t, err) require.EqualValues(t, len(tc.expected), count) diff --git a/internal/storage/ledger/legacy/volumes.go b/internal/storage/ledger/legacy/volumes.go index 1e079ae0f..c427227c6 100644 --- a/internal/storage/ledger/legacy/volumes.go +++ b/internal/storage/ledger/legacy/volumes.go @@ -12,7 +12,7 @@ import ( "github.com/uptrace/bun" ) -func (store *Store) volumesQueryContext(q ledgercontroller.GetVolumesWithBalancesQuery) (string, []any, bool, error) { +func (store *Store) volumesQueryContext(q GetVolumesWithBalancesQuery) (string, []any, bool, error) { metadataRegex := regexp.MustCompile(`metadata\[(.+)]`) balanceRegex := regexp.MustCompile(`balance\[(.*)]`) @@ -90,7 +90,7 @@ func (store *Store) volumesQueryContext(q ledgercontroller.GetVolumesWithBalance } -func (store *Store) buildVolumesWithBalancesQuery(query *bun.SelectQuery, q ledgercontroller.GetVolumesWithBalancesQuery, where string, args []any, useMetadata bool) *bun.SelectQuery { +func (store *Store) buildVolumesWithBalancesQuery(query *bun.SelectQuery, q GetVolumesWithBalancesQuery, where string, args []any, useMetadata bool) *bun.SelectQuery { filtersForVolumes := q.Options.Options dateFilterColumn := "effective_date" @@ -165,7 +165,7 @@ func (store *Store) buildVolumesWithBalancesQuery(query *bun.SelectQuery, q ledg return globalQuery } -func (store *Store) GetVolumesWithBalances(ctx context.Context, q ledgercontroller.GetVolumesWithBalancesQuery) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { +func (store *Store) GetVolumesWithBalances(ctx context.Context, q GetVolumesWithBalancesQuery) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { var ( where string args []any @@ -179,8 +179,8 @@ func (store *Store) GetVolumesWithBalances(ctx context.Context, q ledgercontroll } } - return paginateWithOffsetWithoutModel[ledgercontroller.PaginatedQueryOptions[ledgercontroller.FiltersForVolumes], ledger.VolumesWithBalanceByAssetByAccount]( - store, ctx, (*bunpaginate.OffsetPaginatedQuery[ledgercontroller.PaginatedQueryOptions[ledgercontroller.FiltersForVolumes]])(&q), + return paginateWithOffsetWithoutModel[ledgercontroller.PaginatedQueryOptions[FiltersForVolumes], ledger.VolumesWithBalanceByAssetByAccount]( + store, ctx, (*bunpaginate.OffsetPaginatedQuery[ledgercontroller.PaginatedQueryOptions[FiltersForVolumes]])(&q), func(query *bun.SelectQuery) *bun.SelectQuery { return store.buildVolumesWithBalancesQuery(query, q, where, args, useMetadata) }, diff --git a/internal/storage/ledger/legacy/volumes_test.go b/internal/storage/ledger/legacy/volumes_test.go index c5ca7a0a8..4f5553167 100644 --- a/internal/storage/ledger/legacy/volumes_test.go +++ b/internal/storage/ledger/legacy/volumes_test.go @@ -5,6 +5,7 @@ package legacy_test import ( "github.com/formancehq/go-libs/v2/pointer" ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" + ledgerstore "github.com/formancehq/ledger/internal/storage/ledger/legacy" "math/big" "testing" @@ -100,7 +101,7 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for insertion date", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.FiltersForVolumes{UseInsertionDate: true}))) + volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.FiltersForVolumes{UseInsertionDate: true}))) require.NoError(t, err) require.Len(t, volumes.Data, 4) @@ -108,7 +109,7 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for effective date", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.FiltersForVolumes{UseInsertionDate: false}))) + volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.FiltersForVolumes{UseInsertionDate: false}))) require.NoError(t, err) require.Len(t, volumes.Data, 4) @@ -116,9 +117,9 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for insertion date with previous pit", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{PIT: &previousPIT, OOT: nil}, + volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( + ledgerstore.FiltersForVolumes{ + PITFilter: ledgerstore.PITFilter{PIT: &previousPIT, OOT: nil}, UseInsertionDate: true, }))) @@ -137,9 +138,9 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for insertion date with futur pit", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{PIT: &futurPIT, OOT: nil}, + volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( + ledgerstore.FiltersForVolumes{ + PITFilter: ledgerstore.PITFilter{PIT: &futurPIT, OOT: nil}, UseInsertionDate: true, }))) require.NoError(t, err) @@ -149,9 +150,9 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for insertion date with previous oot", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{PIT: nil, OOT: &previousOOT}, + volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( + ledgerstore.FiltersForVolumes{ + PITFilter: ledgerstore.PITFilter{PIT: nil, OOT: &previousOOT}, UseInsertionDate: true, }))) require.NoError(t, err) @@ -161,9 +162,9 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for insertion date with future oot", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{PIT: nil, OOT: &futurOOT}, + volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( + ledgerstore.FiltersForVolumes{ + PITFilter: ledgerstore.PITFilter{PIT: nil, OOT: &futurOOT}, UseInsertionDate: true, }))) @@ -182,9 +183,9 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for effective date with previous pit", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{PIT: &previousPIT, OOT: nil}, + volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( + ledgerstore.FiltersForVolumes{ + PITFilter: ledgerstore.PITFilter{PIT: &previousPIT, OOT: nil}, UseInsertionDate: false, }))) @@ -203,9 +204,9 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for effective date with futur pit", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{PIT: &futurPIT, OOT: nil}, + volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( + ledgerstore.FiltersForVolumes{ + PITFilter: ledgerstore.PITFilter{PIT: &futurPIT, OOT: nil}, UseInsertionDate: false, }))) require.NoError(t, err) @@ -215,9 +216,9 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for effective date with previous oot", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{PIT: nil, OOT: &previousOOT}, + volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( + ledgerstore.FiltersForVolumes{ + PITFilter: ledgerstore.PITFilter{PIT: nil, OOT: &previousOOT}, UseInsertionDate: false, }))) require.NoError(t, err) @@ -227,9 +228,9 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for effective date with futur oot", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{PIT: nil, OOT: &futurOOT}, + volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( + ledgerstore.FiltersForVolumes{ + PITFilter: ledgerstore.PITFilter{PIT: nil, OOT: &futurOOT}, UseInsertionDate: false, }))) @@ -248,9 +249,9 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for insertion date with future PIT and now OOT", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{PIT: &futurPIT, OOT: &now}, + volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( + ledgerstore.FiltersForVolumes{ + PITFilter: ledgerstore.PITFilter{PIT: &futurPIT, OOT: &now}, UseInsertionDate: true, }))) @@ -270,9 +271,9 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for insertion date with previous OOT and now PIT", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{PIT: &now, OOT: &previousOOT}, + volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( + ledgerstore.FiltersForVolumes{ + PITFilter: ledgerstore.PITFilter{PIT: &now, OOT: &previousOOT}, UseInsertionDate: true, }))) @@ -292,9 +293,9 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for effective date with future PIT and now OOT", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{PIT: &futurPIT, OOT: &now}, + volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( + ledgerstore.FiltersForVolumes{ + PITFilter: ledgerstore.PITFilter{PIT: &futurPIT, OOT: &now}, UseInsertionDate: false, }))) @@ -313,9 +314,9 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for insertion date with previous OOT and now PIT", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{PIT: &now, OOT: &previousOOT}, + volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( + ledgerstore.FiltersForVolumes{ + PITFilter: ledgerstore.PITFilter{PIT: &now, OOT: &previousOOT}, UseInsertionDate: false, }))) @@ -337,10 +338,10 @@ func TestVolumesList(t *testing.T) { t.Parallel() volumes, err := store.GetVolumesWithBalances(ctx, - ledgercontroller.NewGetVolumesWithBalancesQuery( + ledgerstore.NewGetVolumesWithBalancesQuery( ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{PIT: &now, OOT: &previousOOT}, + ledgerstore.FiltersForVolumes{ + PITFilter: ledgerstore.PITFilter{PIT: &now, OOT: &previousOOT}, UseInsertionDate: false, }).WithQueryBuilder(query.Match("account", "account:1"))), ) @@ -363,9 +364,9 @@ func TestVolumesList(t *testing.T) { t.Parallel() volumes, err := store.GetVolumesWithBalances(ctx, - ledgercontroller.NewGetVolumesWithBalancesQuery( + ledgerstore.NewGetVolumesWithBalancesQuery( ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{}).WithQueryBuilder(query.Match("metadata[foo]", "bar"))), + ledgerstore.FiltersForVolumes{}).WithQueryBuilder(query.Match("metadata[foo]", "bar"))), ) require.NoError(t, err) @@ -377,9 +378,9 @@ func TestVolumesList(t *testing.T) { t.Parallel() volumes, err := store.GetVolumesWithBalances(ctx, - ledgercontroller.NewGetVolumesWithBalancesQuery( + ledgerstore.NewGetVolumesWithBalancesQuery( ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{}).WithQueryBuilder(query.Exists("metadata", "category"))), + ledgerstore.FiltersForVolumes{}).WithQueryBuilder(query.Exists("metadata", "category"))), ) require.NoError(t, err) @@ -390,9 +391,9 @@ func TestVolumesList(t *testing.T) { t.Parallel() volumes, err := store.GetVolumesWithBalances(ctx, - ledgercontroller.NewGetVolumesWithBalancesQuery( + ledgerstore.NewGetVolumesWithBalancesQuery( ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{}).WithQueryBuilder(query.Exists("metadata", "foo"))), + ledgerstore.FiltersForVolumes{}).WithQueryBuilder(query.Exists("metadata", "foo"))), ) require.NoError(t, err) @@ -461,9 +462,9 @@ func TestVolumesAggregate(t *testing.T) { t.Run("Aggregation Volumes with balance for GroupLvl 0", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery( + volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery( ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ + ledgerstore.FiltersForVolumes{ UseInsertionDate: true, GroupLvl: 0, }).WithQueryBuilder(query.Match("account", "account::")))) @@ -474,9 +475,9 @@ func TestVolumesAggregate(t *testing.T) { t.Run("Aggregation Volumes with balance for GroupLvl 1", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery( + volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery( ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ + ledgerstore.FiltersForVolumes{ UseInsertionDate: true, GroupLvl: 1, }).WithQueryBuilder(query.Match("account", "account::")))) @@ -487,9 +488,9 @@ func TestVolumesAggregate(t *testing.T) { t.Run("Aggregation Volumes with balance for GroupLvl 2", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery( + volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery( ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ + ledgerstore.FiltersForVolumes{ UseInsertionDate: true, GroupLvl: 2, }).WithQueryBuilder(query.Match("account", "account::")))) @@ -500,9 +501,9 @@ func TestVolumesAggregate(t *testing.T) { t.Run("Aggregation Volumes with balance for GroupLvl 3", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery( + volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery( ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ + ledgerstore.FiltersForVolumes{ UseInsertionDate: true, GroupLvl: 3, }).WithQueryBuilder(query.Match("account", "account::")))) @@ -514,10 +515,10 @@ func TestVolumesAggregate(t *testing.T) { t.Run("Aggregation Volumes with balance for GroupLvl 1 && PIT && OOT && effectiveDate", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery( + volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery( ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{ + ledgerstore.FiltersForVolumes{ + PITFilter: ledgerstore.PITFilter{ PIT: &pit, OOT: &oot, }, @@ -548,10 +549,10 @@ func TestVolumesAggregate(t *testing.T) { t.Run("Aggregation Volumes with balance for GroupLvl 1 && PIT && OOT && effectiveDate && Balance Filter 1", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery( + volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery( ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{ + ledgerstore.FiltersForVolumes{ + PITFilter: ledgerstore.PITFilter{ PIT: &pit, OOT: &oot, }, @@ -575,10 +576,10 @@ func TestVolumesAggregate(t *testing.T) { t.Run("Aggregation Volumes with balance for GroupLvl 1 && Balance Filter 2", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery( + volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery( ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{}, + ledgerstore.FiltersForVolumes{ + PITFilter: ledgerstore.PITFilter{}, UseInsertionDate: true, GroupLvl: 2, }).WithQueryBuilder( @@ -620,9 +621,9 @@ func TestVolumesAggregate(t *testing.T) { t.Parallel() volumes, err := store.GetVolumesWithBalances(ctx, - ledgercontroller.NewGetVolumesWithBalancesQuery( + ledgerstore.NewGetVolumesWithBalancesQuery( ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ + ledgerstore.FiltersForVolumes{ GroupLvl: 1, }).WithQueryBuilder(query.And( query.Match("account", "account::"), @@ -638,11 +639,11 @@ func TestVolumesAggregate(t *testing.T) { t.Parallel() volumes, err := store.GetVolumesWithBalances(ctx, - ledgercontroller.NewGetVolumesWithBalancesQuery( + ledgerstore.NewGetVolumesWithBalancesQuery( ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ + ledgerstore.FiltersForVolumes{ GroupLvl: 1, - PITFilter: ledgercontroller.PITFilter{ + PITFilter: ledgerstore.PITFilter{ PIT: pointer.For(now.Add(time.Minute)), }, }).WithQueryBuilder(query.And( @@ -659,9 +660,9 @@ func TestVolumesAggregate(t *testing.T) { t.Parallel() volumes, err := store.GetVolumesWithBalances(ctx, - ledgercontroller.NewGetVolumesWithBalancesQuery( + ledgerstore.NewGetVolumesWithBalancesQuery( ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ + ledgerstore.FiltersForVolumes{ GroupLvl: 1, }).WithQueryBuilder(query.And( query.Match("metadata[foo]", "bar"), diff --git a/internal/storage/ledger/logs.go b/internal/storage/ledger/logs.go index 2f6da528f..47d764d2e 100644 --- a/internal/storage/ledger/logs.go +++ b/internal/storage/ledger/logs.go @@ -9,10 +9,8 @@ import ( "github.com/formancehq/ledger/pkg/features" "errors" - "github.com/formancehq/go-libs/v2/bun/bunpaginate" "github.com/formancehq/go-libs/v2/platform/postgres" "github.com/formancehq/go-libs/v2/pointer" - "github.com/formancehq/go-libs/v2/query" ledger "github.com/formancehq/ledger/internal" ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" ) @@ -46,18 +44,18 @@ func (j RawMessage) Value() (driver.Value, error) { return string(j), nil } -func (s *Store) InsertLog(ctx context.Context, log *ledger.Log) error { +func (store *Store) InsertLog(ctx context.Context, log *ledger.Log) error { _, err := tracing.TraceWithMetric( ctx, "InsertLog", - s.tracer, - s.insertLogHistogram, + store.tracer, + store.insertLogHistogram, tracing.NoResult(func(ctx context.Context) error { // We lock logs table as we need than the last log does not change until the transaction commit - if s.ledger.HasFeature(features.FeatureHashLogs, "SYNC") { - _, err := s.db.NewRaw(`select pg_advisory_xact_lock(?)`, s.ledger.ID).Exec(ctx) + if store.ledger.HasFeature(features.FeatureHashLogs, "SYNC") { + _, err := store.db.NewRaw(`select pg_advisory_xact_lock(?)`, store.ledger.ID).Exec(ctx) if err != nil { return postgres.ResolveError(err) } @@ -78,19 +76,19 @@ func (s *Store) InsertLog(ctx context.Context, log *ledger.Log) error { return err } - query := s.db. + query := store.db. NewInsert(). Model(&Log{ Log: log, - Ledger: s.ledger.Name, + Ledger: store.ledger.Name, Data: payloadData, Memento: mementoData, }). - ModelTableExpr(s.GetPrefixedRelationName("logs")). + ModelTableExpr(store.GetPrefixedRelationName("logs")). Returning("*") if log.ID == 0 { - query = query.Value("id", "nextval(?)", s.GetPrefixedRelationName(fmt.Sprintf(`"log_id_%d"`, s.ledger.ID))) + query = query.Value("id", "nextval(?)", store.GetPrefixedRelationName(fmt.Sprintf(`"log_id_%d"`, store.ledger.ID))) } _, err = query.Exec(ctx) @@ -113,57 +111,20 @@ func (s *Store) InsertLog(ctx context.Context, log *ledger.Log) error { return err } -func (s *Store) ListLogs(ctx context.Context, q ledgercontroller.GetLogsQuery) (*bunpaginate.Cursor[ledger.Log], error) { - return tracing.TraceWithMetric( - ctx, - "ListLogs", - s.tracer, - s.listLogsHistogram, - func(ctx context.Context) (*bunpaginate.Cursor[ledger.Log], error) { - selectQuery := s.db.NewSelect(). - ModelTableExpr(s.GetPrefixedRelationName("logs")). - ColumnExpr("*"). - Where("ledger = ?", s.ledger.Name) - - if q.Options.QueryBuilder != nil { - subQuery, args, err := q.Options.QueryBuilder.Build(query.ContextFn(func(key, operator string, value any) (string, []any, error) { - switch { - case key == "date": - return fmt.Sprintf("%s %s ?", key, query.DefaultComparisonOperatorsMapping[operator]), []any{value}, nil - default: - return "", nil, fmt.Errorf("unknown key '%s' when building query", key) - } - })) - if err != nil { - return nil, err - } - selectQuery = selectQuery.Where(subQuery, args...) - } - - cursor, err := bunpaginate.UsingColumn[ledgercontroller.PaginatedQueryOptions[any], Log](ctx, selectQuery, bunpaginate.ColumnPaginatedQuery[ledgercontroller.PaginatedQueryOptions[any]](q)) - if err != nil { - return nil, err - } - - return bunpaginate.MapCursor(cursor, Log.ToCore), nil - }, - ) -} - -func (s *Store) ReadLogWithIdempotencyKey(ctx context.Context, key string) (*ledger.Log, error) { +func (store *Store) ReadLogWithIdempotencyKey(ctx context.Context, key string) (*ledger.Log, error) { return tracing.TraceWithMetric( ctx, "ReadLogWithIdempotencyKey", - s.tracer, - s.readLogWithIdempotencyKeyHistogram, + store.tracer, + store.readLogWithIdempotencyKeyHistogram, func(ctx context.Context) (*ledger.Log, error) { ret := &Log{} - if err := s.db.NewSelect(). + if err := store.db.NewSelect(). Model(ret). - ModelTableExpr(s.GetPrefixedRelationName("logs")). + ModelTableExpr(store.GetPrefixedRelationName("logs")). Column("*"). Where("idempotency_key = ?", key). - Where("ledger = ?", s.ledger.Name). + Where("ledger = ?", store.ledger.Name). Limit(1). Scan(ctx); err != nil { return nil, postgres.ResolveError(err) diff --git a/internal/storage/ledger/logs_test.go b/internal/storage/ledger/logs_test.go index a4b284a6b..bb9341e72 100644 --- a/internal/storage/ledger/logs_test.go +++ b/internal/storage/ledger/logs_test.go @@ -5,6 +5,7 @@ package ledger_test import ( "context" "database/sql" + "github.com/formancehq/go-libs/v2/pointer" "golang.org/x/sync/errgroup" "math/big" "testing" @@ -120,9 +121,10 @@ func TestInsertLog(t *testing.T) { err := errGroup.Wait() require.NoError(t, err) - logs, err := store.ListLogs(ctx, ledgercontroller.NewListLogsQuery(ledgercontroller.PaginatedQueryOptions[any]{ + logs, err := store.Logs().Paginate(ctx, ledgercontroller.ColumnPaginatedQuery[any]{ PageSize: countLogs, - }).WithOrder(bunpaginate.OrderAsc)) + Order: pointer.For(bunpaginate.Order(bunpaginate.OrderAsc)), + }) require.NoError(t, err) var previous *ledger.Log @@ -180,26 +182,30 @@ func TestLogsList(t *testing.T) { require.NoError(t, err) } - cursor, err := store.ListLogs(context.Background(), ledgercontroller.NewListLogsQuery(ledgercontroller.NewPaginatedQueryOptions[any](nil))) + cursor, err := store.Logs().Paginate(context.Background(), ledgercontroller.ColumnPaginatedQuery[any]{}) require.NoError(t, err) require.Equal(t, bunpaginate.QueryDefaultPageSize, cursor.PageSize) require.Equal(t, 3, len(cursor.Data)) require.EqualValues(t, 3, cursor.Data[0].ID) - cursor, err = store.ListLogs(context.Background(), ledgercontroller.NewListLogsQuery(ledgercontroller.NewPaginatedQueryOptions[any](nil).WithPageSize(1))) + cursor, err = store.Logs().Paginate(context.Background(), ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: 1, + }) require.NoError(t, err) // Should get only the first log. require.Equal(t, 1, cursor.PageSize) require.EqualValues(t, 3, cursor.Data[0].ID) - cursor, err = store.ListLogs(context.Background(), ledgercontroller.NewListLogsQuery(ledgercontroller.NewPaginatedQueryOptions[any](nil). - WithQueryBuilder(query.And( - query.Gte("date", now.Add(-2*time.Hour)), - query.Lt("date", now.Add(-time.Hour)), - )). - WithPageSize(10), - )) + cursor, err = store.Logs().Paginate(context.Background(), ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: 10, + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.And( + query.Gte("date", now.Add(-2*time.Hour)), + query.Lt("date", now.Add(-time.Hour)), + ), + }, + }) require.NoError(t, err) require.Equal(t, 10, cursor.PageSize) // Should get only the second log, as StartTime is inclusive and EndTime exclusive. diff --git a/internal/storage/ledger/moves.go b/internal/storage/ledger/moves.go index 6f5e55343..00dbc86cc 100644 --- a/internal/storage/ledger/moves.go +++ b/internal/storage/ledger/moves.go @@ -2,80 +2,22 @@ package ledger import ( "context" - ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" - "github.com/formancehq/ledger/pkg/features" - "github.com/formancehq/go-libs/v2/platform/postgres" - "github.com/formancehq/go-libs/v2/time" ledger "github.com/formancehq/ledger/internal" "github.com/formancehq/ledger/internal/tracing" - "github.com/uptrace/bun" ) -func (s *Store) SortMovesBySeq(date *time.Time) (*bun.SelectQuery, error) { - - ret := s.db.NewSelect() - if !s.ledger.HasFeature(features.FeatureMovesHistory, "ON") { - return nil, ledgercontroller.NewErrMissingFeature(features.FeatureMovesHistory) - } - - ret = ret. - ModelTableExpr(s.GetPrefixedRelationName("moves")). - Where("ledger = ?", s.ledger.Name). - Order("seq desc") - - if date != nil && !date.IsZero() { - ret = ret.Where("insertion_date <= ?", date) - } - - return ret, nil -} - -func (s *Store) SelectDistinctMovesBySeq(date *time.Time) (*bun.SelectQuery, error) { - sortMovesBySeq, err := s.SortMovesBySeq(date) - if err != nil { - return nil, err - } - ret := s.db.NewSelect(). - TableExpr("(?) moves", sortMovesBySeq). - DistinctOn("accounts_address, asset"). - Column("accounts_address", "asset"). - ColumnExpr("first_value(post_commit_volumes) over (partition by (accounts_address, asset) order by seq desc) as post_commit_volumes"). - Where("ledger = ?", s.ledger.Name) - - if date != nil && !date.IsZero() { - ret = ret.Where("insertion_date <= ?", date) - } - - return ret, nil -} - -func (s *Store) SelectDistinctMovesByEffectiveDate(date *time.Time) *bun.SelectQuery { - ret := s.db.NewSelect(). - TableExpr(s.GetPrefixedRelationName("moves")). - DistinctOn("accounts_address, asset"). - Column("accounts_address", "asset"). - ColumnExpr("first_value(post_commit_effective_volumes) over (partition by (accounts_address, asset) order by effective_date desc, seq desc) as post_commit_effective_volumes"). - Where("ledger = ?", s.ledger.Name) - - if date != nil && !date.IsZero() { - ret = ret.Where("effective_date <= ?", date) - } - - return ret -} - -func (s *Store) InsertMoves(ctx context.Context, moves ...*ledger.Move) error { +func (store *Store) InsertMoves(ctx context.Context, moves ...*ledger.Move) error { _, err := tracing.TraceWithMetric( ctx, "InsertMoves", - s.tracer, - s.insertMovesHistogram, + store.tracer, + store.insertMovesHistogram, tracing.NoResult(func(ctx context.Context) error { - _, err := s.db.NewInsert(). + _, err := store.db.NewInsert(). Model(&moves). - Value("ledger", "?", s.ledger.Name). - ModelTableExpr(s.GetPrefixedRelationName("moves")). + Value("ledger", "?", store.ledger.Name). + ModelTableExpr(store.GetPrefixedRelationName("moves")). Returning("post_commit_volumes, post_commit_effective_volumes"). Exec(ctx) diff --git a/internal/storage/ledger/moves_test.go b/internal/storage/ledger/moves_test.go index 02ace80c4..4d4bb90c4 100644 --- a/internal/storage/ledger/moves_test.go +++ b/internal/storage/ledger/moves_test.go @@ -5,7 +5,6 @@ package ledger_test import ( "database/sql" "fmt" - "github.com/formancehq/go-libs/v2/pointer" "math/big" "math/rand" "testing" @@ -171,14 +170,19 @@ func TestMovesInsert(t *testing.T) { } wp.StopAndWait() - aggregatedBalances, err := store.GetAggregatedBalances(ctx, ledgercontroller.NewGetAggregatedBalancesQuery(ledgercontroller.PITFilter{ - // By using a PIT, we force the usage of the moves table. - // If it was not specified, the test would not been correct. - PIT: pointer.For(time.Now()), - }, nil, true)) + aggregatedVolumes, err := store.AggregatedVolumes().GetOne(ctx, ledgercontroller.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{ + Opts: ledgercontroller.GetAggregatedVolumesOptions{ + UseInsertionDate: true, + }, + }) require.NoError(t, err) - RequireEqual(t, ledger.BalancesByAssets{ - "USD": big.NewInt(0), - }, aggregatedBalances) + RequireEqual(t, ledger.AggregatedVolumes{ + Aggregated: ledger.VolumesByAssets{ + "USD": { + Input: big.NewInt(1000), + Output: big.NewInt(1000), + }, + }, + }, *aggregatedVolumes) }) } diff --git a/internal/storage/ledger/paginator.go b/internal/storage/ledger/paginator.go new file mode 100644 index 000000000..b73b9f0f6 --- /dev/null +++ b/internal/storage/ledger/paginator.go @@ -0,0 +1,11 @@ +package ledger + +import ( + "github.com/formancehq/go-libs/v2/bun/bunpaginate" + "github.com/uptrace/bun" +) + +type paginator[ResourceType any, PaginationOptions any] interface { + paginate(selectQuery *bun.SelectQuery, opts PaginationOptions) (*bun.SelectQuery, error) + buildCursor(ret []ResourceType, opts PaginationOptions) (*bunpaginate.Cursor[ResourceType], error) +} diff --git a/internal/storage/ledger/paginator_column.go b/internal/storage/ledger/paginator_column.go new file mode 100644 index 000000000..e9ab8c35b --- /dev/null +++ b/internal/storage/ledger/paginator_column.go @@ -0,0 +1,240 @@ +package ledger + +import ( + "fmt" + "github.com/formancehq/go-libs/v2/bun/bunpaginate" + "github.com/formancehq/go-libs/v2/time" + ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" + "github.com/uptrace/bun" + "math/big" + "reflect" + "strings" + libtime "time" +) + +type columnPaginator[ResourceType, OptionsType any] struct { + defaultPaginationColumn string + defaultOrder bunpaginate.Order +} + +//nolint:unused +func (o columnPaginator[ResourceType, OptionsType]) paginate(sb *bun.SelectQuery, query ledgercontroller.ColumnPaginatedQuery[OptionsType]) (*bun.SelectQuery, error) { + + paginationColumn := o.defaultPaginationColumn + originalOrder := o.defaultOrder + if query.Order != nil { + originalOrder = *query.Order + } + + pageSize := query.PageSize + if pageSize == 0 { + pageSize = bunpaginate.QueryDefaultPageSize + } + + sb = sb.Limit(int(pageSize) + 1) // Fetch one additional item to find the next token + order := originalOrder + if query.Reverse { + order = originalOrder.Reverse() + } + orderExpression := fmt.Sprintf("%s %s", paginationColumn, order) + sb = sb.ColumnExpr("row_number() OVER (ORDER BY " + orderExpression + ")") + + if query.PaginationID != nil { + if query.Reverse { + switch originalOrder { + case bunpaginate.OrderAsc: + sb = sb.Where(fmt.Sprintf("%s < ?", paginationColumn), query.PaginationID) + case bunpaginate.OrderDesc: + sb = sb.Where(fmt.Sprintf("%s > ?", paginationColumn), query.PaginationID) + } + } else { + switch originalOrder { + case bunpaginate.OrderAsc: + sb = sb.Where(fmt.Sprintf("%s >= ?", paginationColumn), query.PaginationID) + case bunpaginate.OrderDesc: + sb = sb.Where(fmt.Sprintf("%s <= ?", paginationColumn), query.PaginationID) + } + } + } + + return sb, nil +} + +//nolint:unused +func (o columnPaginator[ResourceType, OptionsType]) buildCursor(ret []ResourceType, query ledgercontroller.ColumnPaginatedQuery[OptionsType]) (*bunpaginate.Cursor[ResourceType], error) { + + paginationColumn := query.Column + if paginationColumn == "" { + paginationColumn = o.defaultPaginationColumn + } + + pageSize := query.PageSize + if pageSize == 0 { + pageSize = bunpaginate.QueryDefaultPageSize + } + + order := o.defaultOrder + if query.Order != nil { + order = *query.Order + } + + var v ResourceType + fields := findPaginationFieldPath(v, paginationColumn) + + var ( + paginationIDs = make([]*big.Int, 0) + ) + for _, t := range ret { + paginationID := findPaginationField(t, fields...) + if query.Bottom == nil { + query.Bottom = paginationID + } + paginationIDs = append(paginationIDs, paginationID) + } + + hasMore := len(ret) > int(pageSize) + if hasMore { + ret = ret[:len(ret)-1] + } + if query.Reverse { + for i := 0; i < len(ret)/2; i++ { + ret[i], ret[len(ret)-i-1] = ret[len(ret)-i-1], ret[i] + } + } + + var previous, next *ledgercontroller.ColumnPaginatedQuery[OptionsType] + + if query.Reverse { + cp := query + cp.Reverse = false + next = &cp + + if hasMore { + cp := query + cp.PaginationID = paginationIDs[len(paginationIDs)-2] + previous = &cp + } + } else { + if hasMore { + cp := query + cp.PaginationID = paginationIDs[len(paginationIDs)-1] + next = &cp + } + if query.PaginationID != nil { + if (order == bunpaginate.OrderAsc && query.PaginationID.Cmp(query.Bottom) > 0) || (order == bunpaginate.OrderDesc && query.PaginationID.Cmp(query.Bottom) < 0) { + cp := query + cp.Reverse = true + previous = &cp + } + } + } + + return &bunpaginate.Cursor[ResourceType]{ + PageSize: int(pageSize), + HasMore: next != nil, + Previous: encodeCursor[OptionsType, ledgercontroller.ColumnPaginatedQuery[OptionsType]](previous), + Next: encodeCursor[OptionsType, ledgercontroller.ColumnPaginatedQuery[OptionsType]](next), + Data: ret, + }, nil +} + +var _ paginator[any, ledgercontroller.ColumnPaginatedQuery[any]] = &columnPaginator[any, any]{} + +//nolint:unused +func findPaginationFieldPath(v any, paginationColumn string) []reflect.StructField { + + typeOfT := reflect.TypeOf(v) + for i := 0; i < typeOfT.NumField(); i++ { + field := typeOfT.Field(i) + fieldType := field.Type + + // If the field is a pointer, we unreference it to target the concrete type + // For example: + // type Object struct { + // *AnotherObject + // } + for { + if field.Type.Kind() == reflect.Ptr { + fieldType = field.Type.Elem() + } + break + } + + switch fieldType.Kind() { + case reflect.Struct: + if fieldType.AssignableTo(reflect.TypeOf(time.Time{})) || + fieldType.AssignableTo(reflect.TypeOf(libtime.Time{})) || + fieldType.AssignableTo(reflect.TypeOf(big.Int{})) || + fieldType.AssignableTo(reflect.TypeOf(bunpaginate.BigInt{})) { + + if fields := checkTag(field, paginationColumn); len(fields) > 0 { + return fields + } + } else { + fields := findPaginationFieldPath(reflect.New(fieldType).Elem().Interface(), paginationColumn) + if len(fields) > 0 { + return fields + } + } + default: + if fields := checkTag(field, paginationColumn); len(fields) > 0 { + return fields + } + } + } + + return nil +} + +//nolint:unused +func checkTag(field reflect.StructField, paginationColumn string) []reflect.StructField { + tag := field.Tag.Get("bun") + column := strings.Split(tag, ",")[0] + if column == paginationColumn { + return []reflect.StructField{field} + } + + return nil +} + +//nolint:unused +func findPaginationField(v any, fields ...reflect.StructField) *big.Int { + vOf := reflect.ValueOf(v) + field := vOf.FieldByName(fields[0].Name) + if len(fields) == 1 { + switch rawPaginationID := field.Interface().(type) { + case time.Time: + return big.NewInt(rawPaginationID.UTC().UnixMicro()) + case *time.Time: + return big.NewInt(rawPaginationID.UTC().UnixMicro()) + case *libtime.Time: + return big.NewInt(rawPaginationID.UTC().UnixMicro()) + case libtime.Time: + return big.NewInt(rawPaginationID.UTC().UnixMicro()) + case *bunpaginate.BigInt: + return (*big.Int)(rawPaginationID) + case bunpaginate.BigInt: + return (*big.Int)(&rawPaginationID) + case *big.Int: + return rawPaginationID + case big.Int: + return &rawPaginationID + case int64: + return big.NewInt(rawPaginationID) + case int: + return big.NewInt(int64(rawPaginationID)) + default: + panic(fmt.Sprintf("invalid paginationID, type %T not handled", rawPaginationID)) + } + } + + return findPaginationField(v, fields[1:]...) +} + +//nolint:unused +func encodeCursor[OptionsType any, PaginatedQueryType ledgercontroller.PaginatedQuery[OptionsType]](v *PaginatedQueryType) string { + if v == nil { + return "" + } + return bunpaginate.EncodeCursor(v) +} diff --git a/internal/storage/ledger/paginator_offset.go b/internal/storage/ledger/paginator_offset.go new file mode 100644 index 000000000..7fcc09537 --- /dev/null +++ b/internal/storage/ledger/paginator_offset.go @@ -0,0 +1,79 @@ +package ledger + +import ( + "fmt" + "github.com/formancehq/go-libs/v2/bun/bunpaginate" + ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" + "github.com/uptrace/bun" + "math" +) + +type offsetPaginator[ResourceType, OptionsType any] struct { + defaultPaginationColumn string + defaultOrder bunpaginate.Order +} + +//nolint:unused +func (o offsetPaginator[ResourceType, OptionsType]) paginate(sb *bun.SelectQuery, query ledgercontroller.OffsetPaginatedQuery[OptionsType]) (*bun.SelectQuery, error) { + + paginationColumn := o.defaultPaginationColumn + originalOrder := o.defaultOrder + if query.Order != nil { + originalOrder = *query.Order + } + + orderExpression := fmt.Sprintf("%s %s", paginationColumn, originalOrder) + sb = sb.ColumnExpr("row_number() OVER (ORDER BY " + orderExpression + ")") + + if query.Offset > math.MaxInt32 { + return nil, fmt.Errorf("offset value exceeds maximum allowed value") + } + if query.Offset > 0 { + sb = sb.Offset(int(query.Offset)) + } + + if query.PageSize > 0 { + sb = sb.Limit(int(query.PageSize) + 1) + } + + return sb, nil +} + +//nolint:unused +func (o offsetPaginator[ResourceType, OptionsType]) buildCursor(ret []ResourceType, query ledgercontroller.OffsetPaginatedQuery[OptionsType]) (*bunpaginate.Cursor[ResourceType], error) { + + var previous, next *ledgercontroller.OffsetPaginatedQuery[OptionsType] + + // Page with transactions before + if query.Offset > 0 { + cp := query + offset := int(query.Offset) - int(query.PageSize) + if offset < 0 { + offset = 0 + } + cp.Offset = uint64(offset) + previous = &cp + } + + // Page with transactions after + if query.PageSize != 0 && len(ret) > int(query.PageSize) { + cp := query + // Check for potential overflow + if query.Offset > math.MaxUint64-query.PageSize { + return nil, fmt.Errorf("offset overflow") + } + cp.Offset = query.Offset + query.PageSize + next = &cp + ret = ret[:len(ret)-1] + } + + return &bunpaginate.Cursor[ResourceType]{ + PageSize: int(query.PageSize), + HasMore: next != nil, + Previous: encodeCursor[OptionsType, ledgercontroller.OffsetPaginatedQuery[OptionsType]](previous), + Next: encodeCursor[OptionsType, ledgercontroller.OffsetPaginatedQuery[OptionsType]](next), + Data: ret, + }, nil +} + +var _ paginator[any, ledgercontroller.OffsetPaginatedQuery[any]] = &offsetPaginator[any, any]{} diff --git a/internal/storage/ledger/resource.go b/internal/storage/ledger/resource.go new file mode 100644 index 000000000..bb074a120 --- /dev/null +++ b/internal/storage/ledger/resource.go @@ -0,0 +1,364 @@ +package ledger + +import ( + "context" + "fmt" + "github.com/formancehq/go-libs/v2/bun/bunpaginate" + "github.com/formancehq/go-libs/v2/platform/postgres" + "github.com/formancehq/go-libs/v2/pointer" + "github.com/formancehq/go-libs/v2/query" + ledger "github.com/formancehq/ledger/internal" + ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" + "github.com/uptrace/bun" + "regexp" + "slices" +) + +func convertOperatorToSQL(operator string) string { + switch operator { + case "$match": + return "=" + case "$lt": + return "<" + case "$gt": + return ">" + case "$lte": + return "<=" + case "$gte": + return ">=" + } + panic("unreachable") +} + +type joinCondition struct { + left string + right string +} + +type propertyValidator interface { + validate(ledger.Ledger, string, string, any) error +} +type propertyValidatorFunc func(ledger.Ledger, string, string, any) error + +func (p propertyValidatorFunc) validate(l ledger.Ledger, operator string, key string, value any) error { + return p(l, operator, key, value) +} + +func acceptOperators(operators ...string) propertyValidator { + return propertyValidatorFunc(func(l ledger.Ledger, operator string, key string, value any) error { + if !slices.Contains(operators, operator) { + return ledgercontroller.NewErrInvalidQuery("operator '%s' is not allowed", operator) + } + return nil + }) +} + +type filter struct { + name string + aliases []string + matchers []func(key string) bool + validators []propertyValidator +} + +type repositoryHandlerBuildContext[Opts any] struct { + ledgercontroller.ResourceQuery[Opts] + filters map[string]any +} + +func (ctx repositoryHandlerBuildContext[Opts]) useFilter(v string, matchers ...func(value any) bool) bool { + value, ok := ctx.filters[v] + if !ok { + return false + } + for _, matcher := range matchers { + if !matcher(value) { + return false + } + } + + return true +} + +type repositoryHandler[Opts any] interface { + filters() []filter + buildDataset(store *Store, query repositoryHandlerBuildContext[Opts]) (*bun.SelectQuery, error) + resolveFilter(store *Store, query ledgercontroller.ResourceQuery[Opts], operator, property string, value any) (string, []any, error) + project(store *Store, query ledgercontroller.ResourceQuery[Opts], selectQuery *bun.SelectQuery) (*bun.SelectQuery, error) + expand(store *Store, query ledgercontroller.ResourceQuery[Opts], property string) (*bun.SelectQuery, *joinCondition, error) +} + +type resourceRepository[ResourceType, OptionsType any] struct { + resourceHandler repositoryHandler[OptionsType] + store *Store + ledger ledger.Ledger +} + +func (r *resourceRepository[ResourceType, OptionsType]) validateFilters(builder query.Builder) (map[string]any, error) { + if builder == nil { + return nil, nil + } + + ret := make(map[string]any) + properties := r.resourceHandler.filters() + if err := builder.Walk(func(operator string, key string, value any) (err error) { + + found := false + for _, property := range properties { + if len(property.matchers) > 0 { + for _, matcher := range property.matchers { + if found = matcher(key); found { + break + } + } + } else { + options := append([]string{property.name}, property.aliases...) + for _, option := range options { + if found, err = regexp.MatchString("^"+option+"$", key); err != nil { + return fmt.Errorf("failed to match regex for key '%s': %w", key, err) + } else if found { + break + } + } + } + if !found { + continue + } + + for _, validator := range property.validators { + if err := validator.validate(r.ledger, operator, key, value); err != nil { + return err + } + } + ret[property.name] = value + break + } + + if !found { + return ledgercontroller.NewErrInvalidQuery("unknown key '%s' when building query", key) + } + + return nil + }); err != nil { + return nil, err + } + + return ret, nil +} + +func (r *resourceRepository[ResourceType, OptionsType]) buildFilteredDataset(q ledgercontroller.ResourceQuery[OptionsType]) (*bun.SelectQuery, error) { + + filters, err := r.validateFilters(q.Builder) + if err != nil { + return nil, err + } + + dataset, err := r.resourceHandler.buildDataset(r.store, repositoryHandlerBuildContext[OptionsType]{ + ResourceQuery: q, + filters: filters, + }) + if err != nil { + return nil, err + } + + dataset = r.store.db.NewSelect(). + ModelTableExpr("(?) dataset", dataset) + + if q.Builder != nil { + // Convert filters to where clause + where, args, err := q.Builder.Build(query.ContextFn(func(key, operator string, value any) (string, []any, error) { + return r.resourceHandler.resolveFilter(r.store, q, operator, key, value) + })) + if err != nil { + return nil, err + } + if len(args) > 0 { + dataset = dataset.Where(where, args...) + } else { + dataset = dataset.Where(where) + } + } + + return r.resourceHandler.project(r.store, q, dataset) +} + +func (r *resourceRepository[ResourceType, OptionsType]) expand(dataset *bun.SelectQuery, q ledgercontroller.ResourceQuery[OptionsType]) (*bun.SelectQuery, error) { + dataset = r.store.db.NewSelect(). + With("dataset", dataset). + ModelTableExpr("dataset"). + ColumnExpr("*") + + slices.Sort(q.Expand) + + for i, expand := range q.Expand { + selectQuery, joinCondition, err := r.resourceHandler.expand(r.store, q, expand) + if err != nil { + return nil, err + } + + if selectQuery == nil { + continue + } + + expandCTEName := fmt.Sprintf("expand%d", i) + dataset = dataset. + With(expandCTEName, selectQuery). + Join(fmt.Sprintf( + "left join %s on %s.%s = dataset.%s", + expandCTEName, + expandCTEName, + joinCondition.right, + joinCondition.left, + )) + } + + return dataset, nil +} + +func (r *resourceRepository[ResourceType, OptionsType]) GetOne(ctx context.Context, query ledgercontroller.ResourceQuery[OptionsType]) (*ResourceType, error) { + + finalQuery, err := r.buildFilteredDataset(query) + if err != nil { + return nil, err + } + + finalQuery, err = r.expand(finalQuery, query) + if err != nil { + return nil, err + } + + ret := make([]ResourceType, 0) + if err := finalQuery. + Model(&ret). + Limit(1). + Scan(ctx); err != nil { + return nil, err + } + if len(ret) == 0 { + return nil, postgres.ErrNotFound + } + + return &ret[0], nil +} + +func (r *resourceRepository[ResourceType, OptionsType]) Count(ctx context.Context, query ledgercontroller.ResourceQuery[OptionsType]) (int, error) { + + finalQuery, err := r.buildFilteredDataset(query) + if err != nil { + return 0, err + } + + count, err := finalQuery.Count(ctx) + return count, postgres.ResolveError(err) +} + +func newResourceRepository[ResourceType, OptionsType any]( + store *Store, + l ledger.Ledger, + handler repositoryHandler[OptionsType], +) *resourceRepository[ResourceType, OptionsType] { + return &resourceRepository[ResourceType, OptionsType]{ + resourceHandler: handler, + store: store, + ledger: l, + } +} + +type paginatedResourceRepository[ResourceType, OptionsType any, PaginationQueryType ledgercontroller.PaginatedQuery[OptionsType]] struct { + *resourceRepository[ResourceType, OptionsType] + paginator paginator[ResourceType, PaginationQueryType] +} + +func (r *paginatedResourceRepository[ResourceType, OptionsType, PaginationQueryType]) Paginate( + ctx context.Context, + paginationOptions PaginationQueryType, +) (*bunpaginate.Cursor[ResourceType], error) { + + var resourceQuery ledgercontroller.ResourceQuery[OptionsType] + switch v := any(paginationOptions).(type) { + case ledgercontroller.OffsetPaginatedQuery[OptionsType]: + resourceQuery = v.Options + case ledgercontroller.ColumnPaginatedQuery[OptionsType]: + resourceQuery = v.Options + default: + panic("should not happen") + } + + finalQuery, err := r.buildFilteredDataset(resourceQuery) + if err != nil { + return nil, fmt.Errorf("building filtered dataset: %w", err) + } + + finalQuery, err = r.paginator.paginate(finalQuery, paginationOptions) + if err != nil { + return nil, fmt.Errorf("paginating request: %w", err) + } + + finalQuery, err = r.expand(finalQuery, resourceQuery) + if err != nil { + return nil, fmt.Errorf("expanding results: %w", err) + } + finalQuery = finalQuery.Order("row_number") + + ret := make([]ResourceType, 0) + err = finalQuery.Model(&ret).Scan(ctx) + if err != nil { + return nil, fmt.Errorf("scanning results: %w", err) + } + + return r.paginator.buildCursor(ret, paginationOptions) +} + +func newPaginatedResourceRepository[ResourceType, OptionsType any, PaginationQueryType ledgercontroller.PaginatedQuery[OptionsType]]( + store *Store, + l ledger.Ledger, + handler repositoryHandler[OptionsType], + paginator paginator[ResourceType, PaginationQueryType], +) *paginatedResourceRepository[ResourceType, OptionsType, PaginationQueryType] { + return &paginatedResourceRepository[ResourceType, OptionsType, PaginationQueryType]{ + resourceRepository: newResourceRepository[ResourceType, OptionsType](store, l, handler), + paginator: paginator, + } +} + +type paginatedResourceRepositoryMapper[ToResourceType any, OriginalResourceType interface { + ToCore() ToResourceType +}, OptionsType any, PaginationQueryType ledgercontroller.PaginatedQuery[OptionsType]] struct { + *paginatedResourceRepository[OriginalResourceType, OptionsType, PaginationQueryType] +} + +func (m paginatedResourceRepositoryMapper[ToResourceType, OriginalResourceType, OptionsType, PaginationQueryType]) Paginate( + ctx context.Context, + paginationOptions PaginationQueryType, +) (*bunpaginate.Cursor[ToResourceType], error) { + cursor, err := m.paginatedResourceRepository.Paginate(ctx, paginationOptions) + if err != nil { + return nil, err + } + + return bunpaginate.MapCursor(cursor, OriginalResourceType.ToCore), nil +} + +func (m paginatedResourceRepositoryMapper[ToResourceType, OriginalResourceType, OptionsType, PaginationQueryType]) GetOne( + ctx context.Context, + query ledgercontroller.ResourceQuery[OptionsType], +) (*ToResourceType, error) { + item, err := m.paginatedResourceRepository.GetOne(ctx, query) + if err != nil { + return nil, err + } + + return pointer.For((*item).ToCore()), nil +} + +func newPaginatedResourceRepositoryMapper[ToResourceType any, OriginalResourceType interface { + ToCore() ToResourceType +}, OptionsType any, PaginationQueryType ledgercontroller.PaginatedQuery[OptionsType]]( + store *Store, + l ledger.Ledger, + handler repositoryHandler[OptionsType], + paginator paginator[OriginalResourceType, PaginationQueryType], +) *paginatedResourceRepositoryMapper[ToResourceType, OriginalResourceType, OptionsType, PaginationQueryType] { + return &paginatedResourceRepositoryMapper[ToResourceType, OriginalResourceType, OptionsType, PaginationQueryType]{ + paginatedResourceRepository: newPaginatedResourceRepository(store, l, handler, paginator), + } +} diff --git a/internal/storage/ledger/resource_accounts.go b/internal/storage/ledger/resource_accounts.go new file mode 100644 index 000000000..6a0a7f5d9 --- /dev/null +++ b/internal/storage/ledger/resource_accounts.go @@ -0,0 +1,187 @@ +package ledger + +import ( + "fmt" + ledger "github.com/formancehq/ledger/internal" + ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" + "github.com/formancehq/ledger/pkg/features" + "github.com/stoewer/go-strcase" + "github.com/uptrace/bun" +) + +type accountsResourceHandler struct{} + +func (h accountsResourceHandler) filters() []filter { + return []filter{ + { + name: "address", + validators: []propertyValidator{ + propertyValidatorFunc(func(l ledger.Ledger, operator string, key string, value any) error { + return validateAddressFilter(l, operator, value) + }), + }, + }, + { + name: "first_usage", + validators: []propertyValidator{ + acceptOperators("$lt", "$gt", "$lte", "$gte", "$match"), + }, + }, + { + name: `balance(\[.*])?`, + validators: []propertyValidator{ + acceptOperators("$lt", "$gt", "$lte", "$gte", "$match"), + }, + }, + { + name: "metadata", + validators: []propertyValidator{ + acceptOperators("$exists"), + }, + }, + { + name: `metadata\[.*]`, + validators: []propertyValidator{ + acceptOperators("$match"), + }, + }, + } +} + +func (h accountsResourceHandler) buildDataset(store *Store, opts repositoryHandlerBuildContext[any]) (*bun.SelectQuery, error) { + ret := store.db.NewSelect() + + // Build the query + ret = ret. + ModelTableExpr(store.GetPrefixedRelationName("accounts")). + Column("address", "address_array", "first_usage", "insertion_date", "updated_at"). + Where("ledger = ?", store.ledger.Name) + + if opts.PIT != nil && !opts.PIT.IsZero() { + ret = ret.Where("accounts.first_usage <= ?", opts.PIT) + } + + if store.ledger.HasFeature(features.FeatureAccountMetadataHistory, "SYNC") && opts.PIT != nil && !opts.PIT.IsZero() { + selectDistinctAccountMetadataHistories := store.db.NewSelect(). + DistinctOn("accounts_address"). + ModelTableExpr(store.GetPrefixedRelationName("accounts_metadata")). + Where("ledger = ?", store.ledger.Name). + Column("accounts_address"). + ColumnExpr("first_value(metadata) over (partition by accounts_address order by revision desc) as metadata"). + Where("date <= ?", opts.PIT) + + ret = ret. + Join( + `left join (?) accounts_metadata on accounts_metadata.accounts_address = accounts.address`, + selectDistinctAccountMetadataHistories, + ). + ColumnExpr("coalesce(accounts_metadata.metadata, '{}'::jsonb) as metadata") + } else { + ret = ret.ColumnExpr("accounts.metadata") + } + + return ret, nil +} + +func (h accountsResourceHandler) resolveFilter(store *Store, opts ledgercontroller.ResourceQuery[any], operator, property string, value any) (string, []any, error) { + switch { + case property == "address": + return filterAccountAddress(value.(string), "address"), nil, nil + case property == "first_usage": + return fmt.Sprintf("first_usage %s ?", convertOperatorToSQL(operator)), []any{value}, nil + case balanceRegex.MatchString(property) || property == "balance": + + selectBalance := store.db.NewSelect(). + Where("accounts_address = dataset.address"). + Where("ledger = ?", store.ledger.Name) + + if opts.PIT != nil && !opts.PIT.IsZero() { + if !store.ledger.HasFeature(features.FeatureMovesHistory, "ON") { + return "", nil, ledgercontroller.NewErrMissingFeature(features.FeatureMovesHistory) + } + selectBalance = selectBalance. + ModelTableExpr(store.GetPrefixedRelationName("moves")). + DistinctOn("asset"). + ColumnExpr("first_value((post_commit_volumes).inputs - (post_commit_volumes).outputs) over (partition by (accounts_address, asset) order by seq desc) as balance"). + Where("insertion_date <= ?", opts.PIT) + } else { + selectBalance = selectBalance. + ModelTableExpr(store.GetPrefixedRelationName("accounts_volumes")). + ColumnExpr("input - output as balance") + } + + if balanceRegex.MatchString(property) { + selectBalance = selectBalance.Where("asset = ?", balanceRegex.FindAllStringSubmatch(property, 2)[0][1]) + } + + return store.db.NewSelect(). + TableExpr("(?) balance", selectBalance). + ColumnExpr(fmt.Sprintf("balance %s ?", convertOperatorToSQL(operator)), value). + String(), nil, nil + case property == "metadata": + return "metadata -> ? is not null", []any{value}, nil + + case metadataRegex.Match([]byte(property)): + match := metadataRegex.FindAllStringSubmatch(property, 3) + + return "metadata @> ?", []any{map[string]any{ + match[0][1]: value, + }}, nil + default: + return "", nil, ledgercontroller.NewErrInvalidQuery("invalid filter property %s", property) + } +} + +func (h accountsResourceHandler) project(store *Store, query ledgercontroller.ResourceQuery[any], selectQuery *bun.SelectQuery) (*bun.SelectQuery, error) { + return selectQuery.ColumnExpr("*"), nil +} + +func (h accountsResourceHandler) expand(store *Store, opts ledgercontroller.ResourceQuery[any], property string) (*bun.SelectQuery, *joinCondition, error) { + switch property { + case "volumes": + if !store.ledger.HasFeature(features.FeatureMovesHistory, "ON") { + return nil, nil, ledgercontroller.NewErrInvalidQuery("feature %s must be 'ON' to use volumes", features.FeatureMovesHistory) + } + case "effectiveVolumes": + if !store.ledger.HasFeature(features.FeatureMovesHistoryPostCommitEffectiveVolumes, "SYNC") { + return nil, nil, ledgercontroller.NewErrInvalidQuery("feature %s must be 'SYNC' to use effectiveVolumes", features.FeatureMovesHistoryPostCommitEffectiveVolumes) + } + } + + selectRowsQuery := store.db.NewSelect(). + Where("accounts_address in (select address from dataset)") + if opts.UsePIT() { + selectRowsQuery = selectRowsQuery. + ModelTableExpr(store.GetPrefixedRelationName("moves")). + DistinctOn("accounts_address, asset"). + Column("accounts_address", "asset"). + Where("ledger = ?", store.ledger.Name) + if property == "volumes" { + selectRowsQuery = selectRowsQuery. + ColumnExpr("first_value(post_commit_volumes) over (partition by (accounts_address, asset) order by seq desc) as volumes"). + Where("insertion_date <= ?", opts.PIT) + } else { + selectRowsQuery = selectRowsQuery. + ColumnExpr("first_value(post_commit_volumes) over (partition by (accounts_address, asset) order by effective_date desc) as volumes"). + Where("effective_date <= ?", opts.PIT) + } + } else { + selectRowsQuery = selectRowsQuery. + ModelTableExpr(store.GetPrefixedRelationName("accounts_volumes")). + Column("asset", "accounts_address"). + ColumnExpr("(input, output)::"+store.GetPrefixedRelationName("volumes")+" as volumes"). + Where("ledger = ?", store.ledger.Name) + } + + return store.db.NewSelect(). + With("rows", selectRowsQuery). + ModelTableExpr("rows"). + Column("accounts_address"). + ColumnExpr("public.aggregate_objects(json_build_object(asset, json_build_object('input', (volumes).inputs, 'output', (volumes).outputs))::jsonb) as " + strcase.SnakeCase(property)). + Group("accounts_address"), &joinCondition{ + left: "address", + right: "accounts_address", + }, nil +} + +var _ repositoryHandler[any] = accountsResourceHandler{} diff --git a/internal/storage/ledger/resource_aggregated_balances.go b/internal/storage/ledger/resource_aggregated_balances.go new file mode 100644 index 000000000..b5a32370c --- /dev/null +++ b/internal/storage/ledger/resource_aggregated_balances.go @@ -0,0 +1,172 @@ +package ledger + +import ( + "errors" + "fmt" + ledger "github.com/formancehq/ledger/internal" + ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" + "github.com/formancehq/ledger/pkg/features" + "github.com/uptrace/bun" +) + +type aggregatedBalancesResourceRepositoryHandler struct{} + +func (h aggregatedBalancesResourceRepositoryHandler) filters() []filter { + return []filter{ + { + name: "address", + validators: []propertyValidator{ + propertyValidatorFunc(func(l ledger.Ledger, operator string, key string, value any) error { + return validateAddressFilter(l, operator, value) + }), + }, + }, + { + name: "metadata", + matchers: []func(string) bool{ + func(key string) bool { + return key == "metadata" || metadataRegex.Match([]byte(key)) + }, + }, + validators: []propertyValidator{ + propertyValidatorFunc(func(l ledger.Ledger, operator string, key string, value any) error { + if key == "metadata" { + if operator != "$exists" { + return fmt.Errorf("unsupported operator %s for metadata", operator) + } + return nil + } + if operator != "$match" { + return fmt.Errorf("unsupported operator %s for metadata", operator) + } + return nil + }), + }, + }, + } +} + +func (h aggregatedBalancesResourceRepositoryHandler) buildDataset(store *Store, query repositoryHandlerBuildContext[ledgercontroller.GetAggregatedVolumesOptions]) (*bun.SelectQuery, error) { + + if query.UsePIT() { + ret := store.db.NewSelect(). + ModelTableExpr(store.GetPrefixedRelationName("moves")). + DistinctOn("accounts_address, asset"). + Column("accounts_address", "asset"). + Where("ledger = ?", store.ledger.Name) + if query.Opts.UseInsertionDate { + if !store.ledger.HasFeature(features.FeatureMovesHistory, "ON") { + return nil, ledgercontroller.NewErrMissingFeature(features.FeatureMovesHistory) + } + + ret = ret. + ColumnExpr("first_value(post_commit_volumes) over (partition by (accounts_address, asset) order by seq desc) as volumes"). + Where("insertion_date <= ?", query.PIT) + } else { + if !store.ledger.HasFeature(features.FeatureMovesHistoryPostCommitEffectiveVolumes, "SYNC") { + return nil, ledgercontroller.NewErrMissingFeature(features.FeatureMovesHistoryPostCommitEffectiveVolumes) + } + + ret = ret. + ColumnExpr("first_value(post_commit_effective_volumes) over (partition by (accounts_address, asset) order by effective_date desc, seq desc) as volumes"). + Where("effective_date <= ?", query.PIT) + } + + if query.useFilter("address", isPartialAddress) { + subQuery := store.db.NewSelect(). + TableExpr(store.GetPrefixedRelationName("accounts")). + Column("address_array"). + Where("accounts.address = accounts_address"). + Where("ledger = ?", store.ledger.Name) + + ret = ret. + ColumnExpr("accounts.address_array as accounts_address_array"). + Join(`join lateral (?) accounts on true`, subQuery) + } + + if query.useFilter("metadata") { + subQuery := store.db.NewSelect(). + DistinctOn("accounts_address"). + ModelTableExpr(store.GetPrefixedRelationName("accounts_metadata")). + ColumnExpr("first_value(metadata) over (partition by accounts_address order by revision desc) as metadata"). + Where("ledger = ?", store.ledger.Name). + Where("accounts_metadata.accounts_address = moves.accounts_address"). + Where("date <= ?", query.PIT) + + ret = ret. + Join(`left join lateral (?) accounts_metadata on true`, subQuery). + Column("metadata") + } + + return ret, nil + } else { + ret := store.db.NewSelect(). + ModelTableExpr(store.GetPrefixedRelationName("accounts_volumes")). + Column("asset", "accounts_address"). + ColumnExpr("(input, output)::"+store.GetPrefixedRelationName("volumes")+" as volumes"). + Where("ledger = ?", store.ledger.Name) + + if query.useFilter("metadata") || query.useFilter("address", isPartialAddress) { + subQuery := store.db.NewSelect(). + TableExpr(store.GetPrefixedRelationName("accounts")). + Column("address"). + Where("ledger = ?", store.ledger.Name). + Where("accounts.address = accounts_address") + + if query.useFilter("address") { + subQuery = subQuery.ColumnExpr("address_array as accounts_address_array") + ret = ret.Column("accounts_address_array") + } + if query.useFilter("metadata") { + subQuery = subQuery.ColumnExpr("metadata") + ret = ret.Column("metadata") + } + + ret = ret. + Join(`join lateral (?) accounts on true`, subQuery) + } + + return ret, nil + } +} + +func (h aggregatedBalancesResourceRepositoryHandler) resolveFilter(store *Store, query ledgercontroller.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions], operator, property string, value any) (string, []any, error) { + switch { + case property == "address": + return filterAccountAddress(value.(string), "accounts_address"), nil, nil + case metadataRegex.Match([]byte(property)) || property == "metadata": + if property == "metadata" { + return "metadata -> ? is not null", []any{value}, nil + } else { + match := metadataRegex.FindAllStringSubmatch(property, 3) + + return "metadata @> ?", []any{map[string]any{ + match[0][1]: value, + }}, nil + } + default: + return "", nil, ledgercontroller.NewErrInvalidQuery("unknown key '%s' when building query", property) + } +} + +func (h aggregatedBalancesResourceRepositoryHandler) expand(_ *Store, _ ledgercontroller.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions], property string) (*bun.SelectQuery, *joinCondition, error) { + return nil, nil, errors.New("no expand available for aggregated balances") +} + +func (h aggregatedBalancesResourceRepositoryHandler) project( + store *Store, + _ ledgercontroller.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions], + selectQuery *bun.SelectQuery, +) (*bun.SelectQuery, error) { + sumVolumesForAsset := store.db.NewSelect(). + TableExpr("(?) values", selectQuery). + Group("asset"). + Column("asset"). + ColumnExpr("json_build_object('input', sum(((volumes).inputs)::numeric), 'output', sum(((volumes).outputs)::numeric)) as volumes") + + return store.db.NewSelect(). + TableExpr("(?) values", sumVolumesForAsset). + ColumnExpr("aggregate_objects(json_build_object(asset, volumes)::jsonb) as aggregated"), nil +} + +var _ repositoryHandler[ledgercontroller.GetAggregatedVolumesOptions] = aggregatedBalancesResourceRepositoryHandler{} diff --git a/internal/storage/ledger/resource_logs.go b/internal/storage/ledger/resource_logs.go new file mode 100644 index 000000000..3c2b6d361 --- /dev/null +++ b/internal/storage/ledger/resource_logs.go @@ -0,0 +1,45 @@ +package ledger + +import ( + "errors" + "fmt" + ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" + "github.com/uptrace/bun" +) + +type logsResourceHandler struct{} + +func (h logsResourceHandler) filters() []filter { + return []filter{ + { + // todo: add validators + name: "date", + }, + } +} + +func (h logsResourceHandler) buildDataset(store *Store, _ repositoryHandlerBuildContext[any]) (*bun.SelectQuery, error) { + return store.db.NewSelect(). + ModelTableExpr(store.GetPrefixedRelationName("logs")). + ColumnExpr("*"). + Where("ledger = ?", store.ledger.Name), nil +} + +func (h logsResourceHandler) resolveFilter(_ *Store, _ ledgercontroller.ResourceQuery[any], operator, property string, value any) (string, []any, error) { + switch { + case property == "date": + return fmt.Sprintf("%s %s ?", property, convertOperatorToSQL(operator)), []any{value}, nil + default: + return "", nil, fmt.Errorf("unknown key '%s' when building query", property) + } +} + +func (h logsResourceHandler) expand(_ *Store, _ ledgercontroller.ResourceQuery[any], _ string) (*bun.SelectQuery, *joinCondition, error) { + return nil, nil, errors.New("no expand supported") +} + +func (h logsResourceHandler) project(store *Store, query ledgercontroller.ResourceQuery[any], selectQuery *bun.SelectQuery) (*bun.SelectQuery, error) { + return selectQuery.ColumnExpr("*"), nil +} + +var _ repositoryHandler[any] = logsResourceHandler{} diff --git a/internal/storage/ledger/resource_transactions.go b/internal/storage/ledger/resource_transactions.go new file mode 100644 index 000000000..8c94597aa --- /dev/null +++ b/internal/storage/ledger/resource_transactions.go @@ -0,0 +1,191 @@ +package ledger + +import ( + "fmt" + ledger "github.com/formancehq/ledger/internal" + ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" + "github.com/formancehq/ledger/pkg/features" + "github.com/uptrace/bun" + "slices" +) + +type transactionsResourceHandler struct{} + +func (h transactionsResourceHandler) filters() []filter { + return []filter{ + { + name: "reverted", + validators: []propertyValidator{ + acceptOperators("$match"), + }, + }, + { + name: "account", + validators: []propertyValidator{ + propertyValidatorFunc(func(l ledger.Ledger, operator string, key string, value any) error { + return validateAddressFilter(l, operator, value) + }), + }, + }, + { + name: "source", + validators: []propertyValidator{ + propertyValidatorFunc(func(l ledger.Ledger, operator string, key string, value any) error { + return validateAddressFilter(l, operator, value) + }), + }, + }, + { + name: "destination", + validators: []propertyValidator{ + propertyValidatorFunc(func(l ledger.Ledger, operator string, key string, value any) error { + return validateAddressFilter(l, operator, value) + }), + }, + }, + { + // todo: add validators + name: "timestamp", + }, + { + name: "metadata", + validators: []propertyValidator{ + acceptOperators("$exists"), + }, + }, + { + name: `metadata\[.*]`, + validators: []propertyValidator{ + acceptOperators("$match"), + }, + }, + { + name: "id", + }, + } +} + +func (h transactionsResourceHandler) buildDataset(store *Store, opts repositoryHandlerBuildContext[any]) (*bun.SelectQuery, error) { + ret := store.db.NewSelect(). + ModelTableExpr(store.GetPrefixedRelationName("transactions")). + Column( + "ledger", + "id", + "timestamp", + "reference", + "inserted_at", + "updated_at", + "postings", + "sources", + "destinations", + "sources_arrays", + "destinations_arrays", + ). + Where("ledger = ?", store.ledger.Name) + + if slices.Contains(opts.Expand, "volumes") { + ret = ret.Column("post_commit_volumes") + } + + if opts.PIT != nil && !opts.PIT.IsZero() { + ret = ret.Where("timestamp <= ?", opts.PIT) + } + + if store.ledger.HasFeature(features.FeatureAccountMetadataHistory, "SYNC") && opts.PIT != nil && !opts.PIT.IsZero() { + selectDistinctTransactionMetadataHistories := store.db.NewSelect(). + DistinctOn("transactions_id"). + ModelTableExpr(store.GetPrefixedRelationName("transactions_metadata")). + Where("ledger = ?", store.ledger.Name). + Column("transactions_id", "metadata"). + Order("transactions_id", "revision desc"). + Where("date <= ?", opts.PIT) + + ret = ret. + Join( + `left join (?) transactions_metadata on transactions_metadata.transactions_id = transactions.id`, + selectDistinctTransactionMetadataHistories, + ). + ColumnExpr("coalesce(transactions_metadata.metadata, '{}'::jsonb) as metadata") + } else { + ret = ret.ColumnExpr("metadata") + } + + if opts.UsePIT() { + ret = ret.ColumnExpr("(case when transactions.reverted_at <= ? then transactions.reverted_at else null end) as reverted_at", opts.PIT) + } else { + ret = ret.Column("reverted_at") + } + + return ret, nil +} + +func (h transactionsResourceHandler) resolveFilter(store *Store, opts ledgercontroller.ResourceQuery[any], operator, property string, value any) (string, []any, error) { + switch { + case property == "id": + return fmt.Sprintf("id %s ?", convertOperatorToSQL(operator)), []any{value}, nil + case property == "reference" || property == "timestamp": + return fmt.Sprintf("%s %s ?", property, convertOperatorToSQL(operator)), []any{value}, nil + case property == "reverted": + ret := "reverted_at is" + if value.(bool) { + ret += " not" + } + return ret + " null", nil, nil + case property == "account": + return filterAccountAddressOnTransactions(value.(string), true, true), nil, nil + case property == "source": + return filterAccountAddressOnTransactions(value.(string), true, false), nil, nil + case property == "destination": + return filterAccountAddressOnTransactions(value.(string), false, true), nil, nil + case metadataRegex.Match([]byte(property)): + match := metadataRegex.FindAllStringSubmatch(property, 3) + + return "metadata @> ?", []any{map[string]any{ + match[0][1]: value, + }}, nil + + case property == "metadata": + return "metadata -> ? is not null", []any{value}, nil + default: + return "", nil, fmt.Errorf("unsupported filter: %s", property) + } +} + +func (h transactionsResourceHandler) project(store *Store, query ledgercontroller.ResourceQuery[any], selectQuery *bun.SelectQuery) (*bun.SelectQuery, error) { + return selectQuery.ColumnExpr("*"), nil +} + +func (h transactionsResourceHandler) expand(store *Store, opts ledgercontroller.ResourceQuery[any], property string) (*bun.SelectQuery, *joinCondition, error) { + if property != "effectiveVolumes" { + return nil, nil, nil + } + + ret := store.db.NewSelect(). + TableExpr( + "(?) data", + store.db.NewSelect(). + TableExpr( + "(?) moves", + store.db.NewSelect(). + DistinctOn("transactions_id, accounts_address, asset"). + ModelTableExpr(store.GetPrefixedRelationName("moves")). + Column("transactions_id", "accounts_address", "asset"). + ColumnExpr(`first_value(moves.post_commit_effective_volumes) over (partition by (transactions_id, accounts_address, asset) order by seq desc) as post_commit_effective_volumes`). + Where("ledger = ?", store.ledger.Name). + Where("transactions_id in (select id from dataset)"), + ). + Column("transactions_id", "accounts_address"). + ColumnExpr(`public.aggregate_objects(json_build_object(moves.asset, json_build_object('input', (moves.post_commit_effective_volumes).inputs, 'output', (moves.post_commit_effective_volumes).outputs))::jsonb) AS post_commit_effective_volumes`). + Group("transactions_id", "accounts_address"), + ). + Column("transactions_id"). + ColumnExpr("public.aggregate_objects(json_build_object(accounts_address, post_commit_effective_volumes)::jsonb) AS post_commit_effective_volumes"). + Group("transactions_id") + + return ret, &joinCondition{ + left: "id", + right: "transactions_id", + }, nil +} + +var _ repositoryHandler[any] = transactionsResourceHandler{} diff --git a/internal/storage/ledger/resource_volumes.go b/internal/storage/ledger/resource_volumes.go new file mode 100644 index 000000000..c378c7f97 --- /dev/null +++ b/internal/storage/ledger/resource_volumes.go @@ -0,0 +1,216 @@ +package ledger + +import ( + "errors" + "fmt" + ledger "github.com/formancehq/ledger/internal" + ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" + "github.com/formancehq/ledger/pkg/features" + "github.com/uptrace/bun" + "strings" +) + +type volumesResourceHandler struct{} + +func (h volumesResourceHandler) filters() []filter { + return []filter{ + { + name: "address", + aliases: []string{"account"}, + validators: []propertyValidator{ + propertyValidatorFunc(func(l ledger.Ledger, operator string, key string, value any) error { + return validateAddressFilter(l, operator, value) + }), + }, + }, + { + name: `balance(\[.*])?`, + validators: []propertyValidator{ + acceptOperators("$lt", "$gt", "$lte", "$gte", "$match"), + }, + }, + { + name: "metadata", + matchers: []func(string) bool{ + func(key string) bool { + return key == "metadata" || metadataRegex.Match([]byte(key)) + }, + }, + validators: []propertyValidator{ + propertyValidatorFunc(func(l ledger.Ledger, operator string, key string, value any) error { + if key == "metadata" { + if operator != "$exists" { + return fmt.Errorf("unsupported operator %s for metadata", operator) + } + return nil + } + if operator != "$match" { + return fmt.Errorf("unsupported operator %s for metadata", operator) + } + return nil + }), + }, + }, + } +} + +func (h volumesResourceHandler) buildDataset(store *Store, query repositoryHandlerBuildContext[ledgercontroller.GetVolumesOptions]) (*bun.SelectQuery, error) { + + var selectVolumes *bun.SelectQuery + + needAddressSegments := query.useFilter("address", isPartialAddress) + if !query.UsePIT() && !query.UseOOT() { + selectVolumes = store.db.NewSelect(). + Column("asset", "input", "output"). + ColumnExpr("input - output as balance"). + ColumnExpr("accounts_address as account"). + ModelTableExpr(store.GetPrefixedRelationName("accounts_volumes")). + Where("ledger = ?", store.ledger.Name). + Order("accounts_address", "asset") + + if query.useFilter("metadata") || needAddressSegments { + subQuery := store.db.NewSelect(). + TableExpr(store.GetPrefixedRelationName("accounts")). + Column("address"). + Where("ledger = ?", store.ledger.Name). + Where("accounts.address = accounts_address") + + if needAddressSegments { + subQuery = subQuery.ColumnExpr("address_array as account_array") + selectVolumes = selectVolumes.Column("account_array") + } + if query.useFilter("metadata") { + subQuery = subQuery.ColumnExpr("metadata") + selectVolumes = selectVolumes.Column("metadata") + } + + selectVolumes = selectVolumes. + Join(`join lateral (?) accounts on true`, subQuery) + } + } else { + if !store.ledger.HasFeature(features.FeatureMovesHistory, "ON") { + return nil, ledgercontroller.NewErrMissingFeature(features.FeatureMovesHistory) + } + + selectVolumes = store.db.NewSelect(). + Column("asset"). + ColumnExpr("accounts_address as account"). + ColumnExpr("sum(case when not is_source then amount else 0 end) as input"). + ColumnExpr("sum(case when is_source then amount else 0 end) as output"). + ColumnExpr("sum(case when not is_source then amount else -amount end) as balance"). + ModelTableExpr(store.GetPrefixedRelationName("moves")). + Where("ledger = ?", store.ledger.Name). + GroupExpr("accounts_address, asset"). + Order("accounts_address", "asset") + + dateFilterColumn := "effective_date" + if query.Opts.UseInsertionDate { + dateFilterColumn = "insertion_date" + } + + if query.UsePIT() { + selectVolumes = selectVolumes.Where(dateFilterColumn+" <= ?", query.PIT) + } + + if query.UseOOT() { + selectVolumes = selectVolumes.Where(dateFilterColumn+" >= ?", query.OOT) + } + + if needAddressSegments { + subQuery := store.db.NewSelect(). + TableExpr(store.GetPrefixedRelationName("accounts")). + Column("address_array"). + Where("accounts.address = accounts_address"). + Where("ledger = ?", store.ledger.Name) + + selectVolumes. + ColumnExpr("(array_agg(accounts.address_array))[1] as account_array"). + Join(`join lateral (?) accounts on true`, subQuery) + } + + if query.useFilter("metadata") { + subQuery := store.db.NewSelect(). + DistinctOn("accounts_address"). + ModelTableExpr(store.GetPrefixedRelationName("accounts_metadata")). + ColumnExpr("first_value(metadata) over (partition by accounts_address order by revision desc) as metadata"). + Where("ledger = ?", store.ledger.Name). + Where("accounts_metadata.accounts_address = moves.accounts_address"). + Where("date <= ?", query.PIT) + + selectVolumes = selectVolumes. + Join(`left join lateral (?) accounts_metadata on true`, subQuery). + ColumnExpr("(array_agg(metadata))[1] as metadata") + } + } + + return selectVolumes, nil +} + +func (h volumesResourceHandler) resolveFilter( + store *Store, + opts ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions], + operator, property string, + value any, +) (string, []any, error) { + + switch { + case property == "address" || property == "account": + return filterAccountAddress(value.(string), "account"), nil, nil + case balanceRegex.MatchString(property) || property == "balance": + clauses := make([]string, 0) + args := make([]any, 0) + + clauses = append(clauses, "balance "+convertOperatorToSQL(operator)+" ?") + args = append(args, value) + + if balanceRegex.MatchString(property) { + clauses = append(clauses, "asset = ?") + args = append(args, balanceRegex.FindAllStringSubmatch(property, 2)[0][1]) + } + + return "(" + strings.Join(clauses, ") and (") + ")", args, nil + case metadataRegex.Match([]byte(property)) || property == "metadata": + if property == "metadata" { + return "metadata -> ? is not null", []any{value}, nil + } else { + match := metadataRegex.FindAllStringSubmatch(property, 3) + + return "metadata @> ?", []any{map[string]any{ + match[0][1]: value, + }}, nil + } + default: + return "", nil, fmt.Errorf("unsupported filter %s", property) + } +} + +func (h volumesResourceHandler) project( + store *Store, + query ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions], + selectQuery *bun.SelectQuery, +) (*bun.SelectQuery, error) { + selectQuery = selectQuery.DistinctOn("account, asset") + + if query.Opts.GroupLvl == 0 { + return selectQuery.ColumnExpr("*"), nil + } + + intermediate := store.db.NewSelect(). + ModelTableExpr("(?) data", selectQuery). + Column("asset", "input", "output", "balance"). + ColumnExpr(fmt.Sprintf(`(array_to_string((string_to_array(account, ':'))[1:LEAST(array_length(string_to_array(account, ':'),1),%d)],':')) as account`, query.Opts.GroupLvl)) + + return store.db.NewSelect(). + ModelTableExpr("(?) data", intermediate). + Column("account", "asset"). + ColumnExpr("sum(input) as input"). + ColumnExpr("sum(output) as output"). + ColumnExpr("sum(balance) as balance"). + GroupExpr("account, asset"), nil +} + +func (h volumesResourceHandler) expand(_ *Store, _ ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions], property string) (*bun.SelectQuery, *joinCondition, error) { + return nil, nil, errors.New("no expansion available") +} + +var _ repositoryHandler[ledgercontroller.GetVolumesOptions] = volumesResourceHandler{} diff --git a/internal/storage/ledger/store.go b/internal/storage/ledger/store.go index 34a2b8809..f847f0dff 100644 --- a/internal/storage/ledger/store.go +++ b/internal/storage/ledger/store.go @@ -4,8 +4,10 @@ import ( "context" "database/sql" "fmt" + "github.com/formancehq/go-libs/v2/bun/bunpaginate" "github.com/formancehq/go-libs/v2/migrations" "github.com/formancehq/go-libs/v2/platform/postgres" + ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" "github.com/formancehq/ledger/internal/storage/bucket" "github.com/formancehq/ledger/pkg/features" "go.opentelemetry.io/otel/metric" @@ -25,43 +27,80 @@ type Store struct { tracer trace.Tracer meter metric.Meter - listAccountsHistogram metric.Int64Histogram checkBucketSchemaHistogram metric.Int64Histogram checkLedgerSchemaHistogram metric.Int64Histogram - getAccountHistogram metric.Int64Histogram - countAccountsHistogram metric.Int64Histogram updateAccountsMetadataHistogram metric.Int64Histogram - deleteAccountMetadataHistogram metric.Int64Histogram - upsertAccountsHistogram metric.Int64Histogram - getBalancesHistogram metric.Int64Histogram + deleteAccountMetadataHistogram metric.Int64Histogram + upsertAccountsHistogram metric.Int64Histogram + getBalancesHistogram metric.Int64Histogram insertLogHistogram metric.Int64Histogram - listLogsHistogram metric.Int64Histogram readLogWithIdempotencyKeyHistogram metric.Int64Histogram insertMovesHistogram metric.Int64Histogram - countTransactionsHistogram metric.Int64Histogram - getTransactionHistogram metric.Int64Histogram insertTransactionHistogram metric.Int64Histogram revertTransactionHistogram metric.Int64Histogram updateTransactionMetadataHistogram metric.Int64Histogram deleteTransactionMetadataHistogram metric.Int64Histogram updateBalancesHistogram metric.Int64Histogram getVolumesWithBalancesHistogram metric.Int64Histogram - listTransactionsHistogram metric.Int64Histogram } -func (s *Store) BeginTX(ctx context.Context, options *sql.TxOptions) (*Store, error) { - tx, err := s.db.BeginTx(ctx, options) +func (store *Store) Volumes() ledgercontroller.PaginatedResource[ + ledger.VolumesWithBalanceByAssetByAccount, + ledgercontroller.GetVolumesOptions, + ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]] { + return newPaginatedResourceRepository(store, store.ledger, &volumesResourceHandler{}, offsetPaginator[ledger.VolumesWithBalanceByAssetByAccount, ledgercontroller.GetVolumesOptions]{ + defaultPaginationColumn: "account", + defaultOrder: bunpaginate.OrderAsc, + }) +} + +func (store *Store) AggregatedVolumes() ledgercontroller.Resource[ledger.AggregatedVolumes, ledgercontroller.GetAggregatedVolumesOptions] { + return newResourceRepository[ledger.AggregatedVolumes, ledgercontroller.GetAggregatedVolumesOptions](store, store.ledger, &aggregatedBalancesResourceRepositoryHandler{}) +} + +func (store *Store) Transactions() ledgercontroller.PaginatedResource[ + ledger.Transaction, + any, + ledgercontroller.ColumnPaginatedQuery[any]] { + return newPaginatedResourceRepository(store, store.ledger, &transactionsResourceHandler{}, columnPaginator[ledger.Transaction, any]{ + defaultPaginationColumn: "id", + defaultOrder: bunpaginate.OrderDesc, + }) +} + +func (store *Store) Logs() ledgercontroller.PaginatedResource[ + ledger.Log, + any, + ledgercontroller.ColumnPaginatedQuery[any]] { + return newPaginatedResourceRepositoryMapper[ledger.Log, Log, any, ledgercontroller.ColumnPaginatedQuery[any]](store, store.ledger, &logsResourceHandler{}, columnPaginator[Log, any]{ + defaultPaginationColumn: "id", + defaultOrder: bunpaginate.OrderDesc, + }) +} + +func (store *Store) Accounts() ledgercontroller.PaginatedResource[ + ledger.Account, + any, + ledgercontroller.OffsetPaginatedQuery[any]] { + return newPaginatedResourceRepository(store, store.ledger, &accountsResourceHandler{}, offsetPaginator[ledger.Account, any]{ + defaultPaginationColumn: "address", + defaultOrder: bunpaginate.OrderAsc, + }) +} + +func (store *Store) BeginTX(ctx context.Context, options *sql.TxOptions) (*Store, error) { + tx, err := store.db.BeginTx(ctx, options) if err != nil { return nil, postgres.ResolveError(err) } - cp := *s + cp := *store cp.db = tx return &cp, nil } -func (s *Store) Commit() error { - switch db := s.db.(type) { +func (store *Store) Commit() error { + switch db := store.db.(type) { case bun.Tx: return db.Commit() default: @@ -69,8 +108,8 @@ func (s *Store) Commit() error { } } -func (s *Store) Rollback() error { - switch db := s.db.(type) { +func (store *Store) Rollback() error { + switch db := store.db.(type) { case bun.Tx: return db.Rollback() default: @@ -78,44 +117,44 @@ func (s *Store) Rollback() error { } } -func (s *Store) GetLedger() ledger.Ledger { - return s.ledger +func (store *Store) GetLedger() ledger.Ledger { + return store.ledger } -func (s *Store) GetDB() bun.IDB { - return s.db +func (store *Store) GetDB() bun.IDB { + return store.db } -func (s *Store) GetBucket() bucket.Bucket { - return s.bucket +func (store *Store) GetBucket() bucket.Bucket { + return store.bucket } -func (s *Store) GetPrefixedRelationName(v string) string { - return fmt.Sprintf(`"%s".%s`, s.ledger.Bucket, v) +func (store *Store) GetPrefixedRelationName(v string) string { + return fmt.Sprintf(`"%s".%s`, store.ledger.Bucket, v) } -func (s *Store) validateAddressFilter(operator string, value any) error { +func validateAddressFilter(ledger ledger.Ledger, operator string, value any) error { if operator != "$match" { - return errors.New("'address' column can only be used with $match") + return fmt.Errorf("'address' column can only be used with $match, operator used is: %s", operator) } if value, ok := value.(string); !ok { return fmt.Errorf("invalid 'address' filter") - } else if isSegmentedAddress(value) && !s.ledger.HasFeature(features.FeatureIndexAddressSegments, "ON") { + } else if isSegmentedAddress(value) && !ledger.HasFeature(features.FeatureIndexAddressSegments, "ON") { return fmt.Errorf("feature %s must be 'ON' to use segments address", features.FeatureIndexAddressSegments) } return nil } -func (s *Store) LockLedger(ctx context.Context) error { - _, err := s.db.NewRaw(`lock table ` + s.GetPrefixedRelationName("logs")).Exec(ctx) +func (store *Store) LockLedger(ctx context.Context) error { + _, err := store.db.NewRaw(`lock table ` + store.GetPrefixedRelationName("logs")).Exec(ctx) return postgres.ResolveError(err) } -func New(db bun.IDB, bucket bucket.Bucket, ledger ledger.Ledger, opts ...Option) *Store { +func New(db bun.IDB, bucket bucket.Bucket, l ledger.Ledger, opts ...Option) *Store { ret := &Store{ db: db, - ledger: ledger, + ledger: l, bucket: bucket, } for _, opt := range append(defaultOptions, opts...) { @@ -123,11 +162,6 @@ func New(db bun.IDB, bucket bucket.Bucket, ledger ledger.Ledger, opts ...Option) } var err error - ret.listAccountsHistogram, err = ret.meter.Int64Histogram("store.listAccounts") - if err != nil { - panic(err) - } - ret.checkBucketSchemaHistogram, err = ret.meter.Int64Histogram("store.checkBucketSchema") if err != nil { panic(err) @@ -138,16 +172,6 @@ func New(db bun.IDB, bucket bucket.Bucket, ledger ledger.Ledger, opts ...Option) panic(err) } - ret.getAccountHistogram, err = ret.meter.Int64Histogram("store.getAccount") - if err != nil { - panic(err) - } - - ret.countAccountsHistogram, err = ret.meter.Int64Histogram("store.countAccounts") - if err != nil { - panic(err) - } - ret.updateAccountsMetadataHistogram, err = ret.meter.Int64Histogram("store.updateAccountsMetadata") if err != nil { panic(err) @@ -173,11 +197,6 @@ func New(db bun.IDB, bucket bucket.Bucket, ledger ledger.Ledger, opts ...Option) panic(err) } - ret.listLogsHistogram, err = ret.meter.Int64Histogram("store.listLogs") - if err != nil { - panic(err) - } - ret.readLogWithIdempotencyKeyHistogram, err = ret.meter.Int64Histogram("store.readLogWithIdempotencyKey") if err != nil { panic(err) @@ -188,16 +207,6 @@ func New(db bun.IDB, bucket bucket.Bucket, ledger ledger.Ledger, opts ...Option) panic(err) } - ret.countTransactionsHistogram, err = ret.meter.Int64Histogram("store.countTransactions") - if err != nil { - panic(err) - } - - ret.getTransactionHistogram, err = ret.meter.Int64Histogram("store.getTransaction") - if err != nil { - panic(err) - } - ret.insertTransactionHistogram, err = ret.meter.Int64Histogram("store.insertTransaction") if err != nil { panic(err) @@ -228,24 +237,19 @@ func New(db bun.IDB, bucket bucket.Bucket, ledger ledger.Ledger, opts ...Option) panic(err) } - ret.listTransactionsHistogram, err = ret.meter.Int64Histogram("store.listTransactions") - if err != nil { - panic(err) - } - return ret } -func (s *Store) HasMinimalVersion(ctx context.Context) (bool, error) { - return s.bucket.HasMinimalVersion(ctx) +func (store *Store) HasMinimalVersion(ctx context.Context) (bool, error) { + return store.bucket.HasMinimalVersion(ctx) } -func (s *Store) GetMigrationsInfo(ctx context.Context) ([]migrations.Info, error) { - return s.bucket.GetMigrationsInfo(ctx) +func (store *Store) GetMigrationsInfo(ctx context.Context) ([]migrations.Info, error) { + return store.bucket.GetMigrationsInfo(ctx) } -func (s *Store) WithDB(db bun.IDB) *Store { - ret := *s +func (store *Store) WithDB(db bun.IDB) *Store { + ret := *store ret.db = db return &ret diff --git a/internal/storage/ledger/transactions.go b/internal/storage/ledger/transactions.go index 9de7d93fd..58d461091 100644 --- a/internal/storage/ledger/transactions.go +++ b/internal/storage/ledger/transactions.go @@ -26,7 +26,6 @@ import ( "github.com/formancehq/go-libs/v2/bun/bunpaginate" "github.com/formancehq/go-libs/v2/metadata" - "github.com/formancehq/go-libs/v2/query" ledger "github.com/formancehq/ledger/internal" "github.com/uptrace/bun" ) @@ -35,240 +34,36 @@ var ( metadataRegex = regexp.MustCompile(`metadata\[(.+)]`) ) -func (s *Store) selectDistinctTransactionMetadataHistories(date *time.Time) *bun.SelectQuery { - ret := s.db.NewSelect(). - DistinctOn("transactions_id"). - ModelTableExpr(s.GetPrefixedRelationName("transactions_metadata")). - Where("ledger = ?", s.ledger.Name). - Column("transactions_id", "metadata"). - Order("transactions_id", "revision desc") +func (store *Store) CommitTransaction(ctx context.Context, tx *ledger.Transaction) error { - if date != nil && !date.IsZero() { - ret = ret.Where("date <= ?", date) - } - - return ret -} - -func (s *Store) selectTransactions(date *time.Time, expandVolumes, expandEffectiveVolumes bool, q query.Builder) (*bun.SelectQuery, error) { - - ret := s.db.NewSelect() - if expandEffectiveVolumes && !s.ledger.HasFeature(features.FeatureMovesHistoryPostCommitEffectiveVolumes, "SYNC") { - return nil, ledgercontroller.NewErrMissingFeature(features.FeatureMovesHistoryPostCommitEffectiveVolumes) - } - - if q != nil { - if err := q.Walk(func(operator, key string, value any) error { - switch { - case key == "reverted": - if operator != "$match" { - return ledgercontroller.NewErrInvalidQuery("'reverted' column can only be used with $match") - } - switch value.(type) { - case bool: - return nil - default: - return ledgercontroller.NewErrInvalidQuery("'reverted' can only be used with bool value") - } - case key == "account": - return s.validateAddressFilter(operator, value) - case key == "source": - return s.validateAddressFilter(operator, value) - case key == "destination": - return s.validateAddressFilter(operator, value) - case key == "timestamp": - case metadataRegex.Match([]byte(key)): - if operator != "$match" { - return ledgercontroller.NewErrInvalidQuery("'metadata[xxx]' column can only be used with $match") - } - case key == "metadata": - if operator != "$exists" { - return ledgercontroller.NewErrInvalidQuery("'metadata' key filter can only be used with $exists") - } - default: - return ledgercontroller.NewErrInvalidQuery("unknown key '%s' when building query", key) - } - - return nil - }); err != nil { - return nil, err - } - } - - ret = ret. - ModelTableExpr(s.GetPrefixedRelationName("transactions")). - Column( - "ledger", - "id", - "timestamp", - "reference", - "inserted_at", - "updated_at", - "postings", - "sources", - "destinations", - "sources_arrays", - "destinations_arrays", - "reverted_at", - "post_commit_volumes", - ). - Where("ledger = ?", s.ledger.Name) - - if date != nil && !date.IsZero() { - ret = ret.Where("timestamp <= ?", date) - } - - if s.ledger.HasFeature(features.FeatureAccountMetadataHistory, "SYNC") && date != nil && !date.IsZero() { - ret = ret. - Join( - `left join (?) transactions_metadata on transactions_metadata.transactions_id = transactions.id`, - s.selectDistinctTransactionMetadataHistories(date), - ). - ColumnExpr("coalesce(transactions_metadata.metadata, '{}'::jsonb) as metadata") - } else { - ret = ret.ColumnExpr("metadata") - } - - if s.ledger.HasFeature(features.FeatureMovesHistoryPostCommitEffectiveVolumes, "SYNC") && expandEffectiveVolumes { - ret = ret. - Join( - `join (?) pcev on pcev.transactions_id = transactions.id`, - s.db.NewSelect(). - TableExpr( - "(?) data", - s.db.NewSelect(). - TableExpr( - "(?) moves", - s.db.NewSelect(). - DistinctOn("transactions_id, accounts_address, asset"). - ModelTableExpr(s.GetPrefixedRelationName("moves")). - Column("transactions_id", "accounts_address", "asset"). - Where("ledger = ?", s.ledger.Name). - ColumnExpr(`first_value(moves.post_commit_effective_volumes) over (partition by (transactions_id, accounts_address, asset) order by seq desc) as post_commit_effective_volumes`), - ). - Column("transactions_id"). - ColumnExpr(` - json_build_object( - moves.accounts_address, - json_build_object( - moves.asset, - json_build_object( - 'input', (moves.post_commit_effective_volumes).inputs, - 'output', (moves.post_commit_effective_volumes).outputs - ) - ) - ) as post_commit_effective_volumes - `), - ). - Column("transactions_id"). - ColumnExpr("public.aggregate_objects(post_commit_effective_volumes::jsonb) as post_commit_effective_volumes"). - Group("transactions_id"), - ). - ColumnExpr("pcev.*") - } - - // Create a parent query which set reverted_at to null if the date passed as argument is before - ret = s.db.NewSelect(). - ModelTableExpr("(?) transactions", ret). - Column( - "ledger", - "id", - "timestamp", - "reference", - "inserted_at", - "updated_at", - "postings", - "sources", - "destinations", - "sources_arrays", - "destinations_arrays", - "metadata", - ) - if expandVolumes { - ret = ret.Column("post_commit_volumes") - } - if expandEffectiveVolumes { - ret = ret.Column("post_commit_effective_volumes") - } - if date != nil && !date.IsZero() { - ret = ret.ColumnExpr("(case when transactions.reverted_at <= ? then transactions.reverted_at else null end) as reverted_at", date) - } else { - ret = ret.Column("reverted_at") - } - - if q != nil { - where, args, err := q.Build(query.ContextFn(func(key, operator string, value any) (string, []any, error) { - switch { - case key == "reference" || key == "timestamp": - return fmt.Sprintf("%s %s ?", key, query.DefaultComparisonOperatorsMapping[operator]), []any{value}, nil - case key == "reverted": - ret := "reverted_at is" - if value.(bool) { - ret += " not" - } - return ret + " null", nil, nil - case key == "account": - return filterAccountAddressOnTransactions(value.(string), true, true), nil, nil - case key == "source": - return filterAccountAddressOnTransactions(value.(string), true, false), nil, nil - case key == "destination": - return filterAccountAddressOnTransactions(value.(string), false, true), nil, nil - case metadataRegex.Match([]byte(key)): - match := metadataRegex.FindAllStringSubmatch(key, 3) - - return "metadata @> ?", []any{map[string]any{ - match[0][1]: value, - }}, nil - - case key == "metadata": - return "metadata -> ? is not null", []any{value}, nil - case key == "timestamp": - return fmt.Sprintf("timestamp %s ?", convertOperatorToSQL(operator)), []any{value}, nil - default: - return "", nil, ledgercontroller.NewErrInvalidQuery("unknown key '%s' when building query", key) - } - })) - if err != nil { - return nil, err - } - - if len(args) > 0 { - ret = ret.Where(where, args...) - } else { - ret = ret.Where(where) - } - } - - return ret, nil -} - -func (s *Store) CommitTransaction(ctx context.Context, tx *ledger.Transaction) error { - - postCommitVolumes, err := s.UpdateVolumes(ctx, tx.VolumeUpdates()...) + postCommitVolumes, err := store.UpdateVolumes(ctx, tx.VolumeUpdates()...) if err != nil { return fmt.Errorf("failed to update balances: %w", err) } tx.PostCommitVolumes = postCommitVolumes.Copy() - err = s.InsertTransaction(ctx, tx) + err = store.InsertTransaction(ctx, tx) if err != nil { return fmt.Errorf("failed to insert transaction: %w", err) } - err = s.UpsertAccounts(ctx, collectionutils.Map(tx.InvolvedAccounts(), func(address string) *ledger.Account { + err = store.UpsertAccounts(ctx, collectionutils.Map(tx.InvolvedAccounts(), func(address string) *ledger.Account { return &ledger.Account{ - Address: address, - FirstUsage: tx.Timestamp, - Metadata: make(metadata.Metadata), + Address: address, + FirstUsage: tx.Timestamp, + Metadata: make(metadata.Metadata), + InsertionDate: tx.InsertedAt, + UpdatedAt: tx.InsertedAt, } })...) if err != nil { return fmt.Errorf("upserting accounts: %w", err) } - if s.ledger.HasFeature(features.FeatureMovesHistory, "ON") { + if store.ledger.HasFeature(features.FeatureMovesHistory, "ON") { moves := ledger.Moves{} - postings := tx.Postings + postings := make([]ledger.Posting, len(tx.Postings)) + copy(postings, tx.Postings) slices.Reverse(postings) for _, posting := range postings { @@ -298,11 +93,11 @@ func (s *Store) CommitTransaction(ctx context.Context, tx *ledger.Transaction) e slices.Reverse(moves) - if err := s.InsertMoves(ctx, moves...); err != nil { + if err := store.InsertMoves(ctx, moves...); err != nil { return fmt.Errorf("failed to insert moves: %w", err) } - if s.ledger.HasFeature(features.FeatureMovesHistoryPostCommitEffectiveVolumes, "SYNC") { + if store.ledger.HasFeature(features.FeatureMovesHistoryPostCommitEffectiveVolumes, "SYNC") { tx.PostCommitEffectiveVolumes = moves.ComputePostCommitEffectiveVolumes() } } @@ -310,105 +105,21 @@ func (s *Store) CommitTransaction(ctx context.Context, tx *ledger.Transaction) e return nil } -func (s *Store) ListTransactions(ctx context.Context, q ledgercontroller.ListTransactionsQuery) (*bunpaginate.Cursor[ledger.Transaction], error) { - return tracing.TraceWithMetric( - ctx, - "ListTransactions", - s.tracer, - s.listTransactionsHistogram, - func(ctx context.Context) (*bunpaginate.Cursor[ledger.Transaction], error) { - selectTransactions, err := s.selectTransactions( - q.Options.Options.PIT, - q.Options.Options.ExpandVolumes, - q.Options.Options.ExpandEffectiveVolumes, - q.Options.QueryBuilder, - ) - if err != nil { - return nil, err - } - cursor, err := bunpaginate.UsingColumn[ledgercontroller.PaginatedQueryOptions[ledgercontroller.PITFilterWithVolumes], ledger.Transaction]( - ctx, - selectTransactions, - bunpaginate.ColumnPaginatedQuery[ledgercontroller.PaginatedQueryOptions[ledgercontroller.PITFilterWithVolumes]](q), - ) - if err != nil { - return nil, err - } - - return cursor, nil - }, - ) -} - -func (s *Store) CountTransactions(ctx context.Context, q ledgercontroller.ListTransactionsQuery) (int, error) { - return tracing.TraceWithMetric( - ctx, - "CountTransactions", - s.tracer, - s.countTransactionsHistogram, - func(ctx context.Context) (int, error) { - selectTransactions, err := s.selectTransactions( - q.Options.Options.PIT, - q.Options.Options.ExpandVolumes, - q.Options.Options.ExpandEffectiveVolumes, - q.Options.QueryBuilder, - ) - if err != nil { - return 0, err - } - return s.db.NewSelect(). - TableExpr("(?) data", selectTransactions). - Count(ctx) - }, - ) -} - -func (s *Store) GetTransaction(ctx context.Context, filter ledgercontroller.GetTransactionQuery) (*ledger.Transaction, error) { - return tracing.TraceWithMetric( - ctx, - "GetTransaction", - s.tracer, - s.getTransactionHistogram, - func(ctx context.Context) (*ledger.Transaction, error) { - - ret := &ledger.Transaction{} - selectTransactions, err := s.selectTransactions( - filter.PIT, - filter.ExpandVolumes, - filter.ExpandEffectiveVolumes, - nil, - ) - if err != nil { - return nil, err - } - if err := selectTransactions. - Where("transactions.id = ?", filter.ID). - Limit(1). - Model(ret). - Scan(ctx); err != nil { - return nil, postgres.ResolveError(err) - } - - return ret, nil - }, - ) -} - -func (s *Store) InsertTransaction(ctx context.Context, tx *ledger.Transaction) error { - _, err := tracing.TraceWithMetric( +func (store *Store) InsertTransaction(ctx context.Context, tx *ledger.Transaction) error { + return tracing.SkipResult(tracing.TraceWithMetric( ctx, "InsertTransaction", - s.tracer, - s.insertTransactionHistogram, + store.tracer, + store.insertTransactionHistogram, func(ctx context.Context) (*ledger.Transaction, error) { - query := s.db.NewInsert(). + query := store.db.NewInsert(). Model(tx). - ModelTableExpr(s.GetPrefixedRelationName("transactions")). - Value("ledger", "?", s.ledger.Name). + ModelTableExpr(store.GetPrefixedRelationName("transactions")). + Value("ledger", "?", store.ledger.Name). Returning("id, timestamp, inserted_at") if tx.ID == 0 { - query = query.Value("id", "nextval(?)", s.GetPrefixedRelationName(fmt.Sprintf(`"transaction_id_%d"`, s.ledger.ID))) + query = query.Value("id", "nextval(?)", store.GetPrefixedRelationName(fmt.Sprintf(`"transaction_id_%d"`, store.ledger.ID))) } _, err := query.Exec(ctx) @@ -432,31 +143,29 @@ func (s *Store) InsertTransaction(ctx context.Context, tx *ledger.Transaction) e attribute.String("timestamp", tx.Timestamp.Format(time.RFC3339Nano)), ) }, - ) - - return err + )) } // updateTxWithRetrieve try to apply to provided update query and check (if the update return no rows modified), that the row exists -func (s *Store) updateTxWithRetrieve(ctx context.Context, id int, query *bun.UpdateQuery) (*ledger.Transaction, bool, error) { +func (store *Store) updateTxWithRetrieve(ctx context.Context, id int, query *bun.UpdateQuery) (*ledger.Transaction, bool, error) { type modifiedEntity struct { ledger.Transaction `bun:",extend"` Modified bool `bun:"modified"` } me := &modifiedEntity{} - err := s.db.NewSelect(). + err := store.db.NewSelect(). With("upd", query). ModelTableExpr( "(?) transactions", - s.db.NewSelect(). + store.db.NewSelect(). ColumnExpr("upd.*, true as modified"). ModelTableExpr("upd"). UnionAll( - s.db.NewSelect(). - ModelTableExpr(s.GetPrefixedRelationName("transactions")). + store.db.NewSelect(). + ModelTableExpr(store.GetPrefixedRelationName("transactions")). ColumnExpr("*, false as modified"). - Where("id = ? and ledger = ?", id, s.ledger.Name). + Where("id = ? and ledger = ?", id, store.ledger.Name). Limit(1), ), ). @@ -464,26 +173,23 @@ func (s *Store) updateTxWithRetrieve(ctx context.Context, id int, query *bun.Upd ColumnExpr("*"). Limit(1). Scan(ctx) - if err != nil { - return nil, false, postgres.ResolveError(err) - } - return &me.Transaction, me.Modified, nil + return &me.Transaction, me.Modified, postgres.ResolveError(err) } -func (s *Store) RevertTransaction(ctx context.Context, id int, at time.Time) (tx *ledger.Transaction, modified bool, err error) { +func (store *Store) RevertTransaction(ctx context.Context, id int, at time.Time) (tx *ledger.Transaction, modified bool, err error) { _, err = tracing.TraceWithMetric( ctx, "RevertTransaction", - s.tracer, - s.revertTransactionHistogram, + store.tracer, + store.revertTransactionHistogram, func(ctx context.Context) (*ledger.Transaction, error) { - query := s.db.NewUpdate(). + query := store.db.NewUpdate(). Model(&ledger.Transaction{}). - ModelTableExpr(s.GetPrefixedRelationName("transactions")). + ModelTableExpr(store.GetPrefixedRelationName("transactions")). Where("id = ?", id). Where("reverted_at is null"). - Where("ledger = ?", s.ledger.Name). + Where("ledger = ?", store.ledger.Name). Returning("*") if at.IsZero() { query = query. @@ -495,71 +201,62 @@ func (s *Store) RevertTransaction(ctx context.Context, id int, at time.Time) (tx Set("updated_at = ?", at) } - tx, modified, err = s.updateTxWithRetrieve(ctx, id, query) + tx, modified, err = store.updateTxWithRetrieve(ctx, id, query) return nil, err }, ) - if err != nil { - return nil, false, err - } return tx, modified, err } -func (s *Store) UpdateTransactionMetadata(ctx context.Context, id int, m metadata.Metadata) (tx *ledger.Transaction, modified bool, err error) { +func (store *Store) UpdateTransactionMetadata(ctx context.Context, id int, m metadata.Metadata) (tx *ledger.Transaction, modified bool, err error) { _, err = tracing.TraceWithMetric( ctx, "UpdateTransactionMetadata", - s.tracer, - s.updateTransactionMetadataHistogram, + store.tracer, + store.updateTransactionMetadataHistogram, func(ctx context.Context) (*ledger.Transaction, error) { - tx, modified, err = s.updateTxWithRetrieve( + tx, modified, err = store.updateTxWithRetrieve( ctx, id, - s.db.NewUpdate(). + store.db.NewUpdate(). Model(&ledger.Transaction{}). - ModelTableExpr(s.GetPrefixedRelationName("transactions")). + ModelTableExpr(store.GetPrefixedRelationName("transactions")). Where("id = ?", id). - Where("ledger = ?", s.ledger.Name). + Where("ledger = ?", store.ledger.Name). Set("metadata = metadata || ?", m). Set("updated_at = (now() at time zone 'utc')"). Where("not (metadata @> ?)", m). Returning("*"), ) - return nil, err + return nil, postgres.ResolveError(err) }, ) - if err != nil { - return nil, false, err - } return tx, modified, err } -func (s *Store) DeleteTransactionMetadata(ctx context.Context, id int, key string) (tx *ledger.Transaction, modified bool, err error) { +func (store *Store) DeleteTransactionMetadata(ctx context.Context, id int, key string) (tx *ledger.Transaction, modified bool, err error) { _, err = tracing.TraceWithMetric( ctx, "DeleteTransactionMetadata", - s.tracer, - s.deleteTransactionMetadataHistogram, + store.tracer, + store.deleteTransactionMetadataHistogram, func(ctx context.Context) (*ledger.Transaction, error) { - tx, modified, err = s.updateTxWithRetrieve( + tx, modified, err = store.updateTxWithRetrieve( ctx, id, - s.db.NewUpdate(). + store.db.NewUpdate(). Model(&ledger.Transaction{}). - ModelTableExpr(s.GetPrefixedRelationName("transactions")). + ModelTableExpr(store.GetPrefixedRelationName("transactions")). Set("metadata = metadata - ?", key). Set("updated_at = (now() at time zone 'utc')"). Where("id = ?", id). - Where("ledger = ?", s.ledger.Name). + Where("ledger = ?", store.ledger.Name). Where("metadata -> ? is not null", key). Returning("*"), ) - return nil, err + return nil, postgres.ResolveError(err) }, ) - if err != nil { - return nil, false, err - } return tx, modified, err } diff --git a/internal/storage/ledger/transactions_test.go b/internal/storage/ledger/transactions_test.go index 32702020c..f7442370b 100644 --- a/internal/storage/ledger/transactions_test.go +++ b/internal/storage/ledger/transactions_test.go @@ -52,9 +52,10 @@ func TestTransactionsGetWithVolumes(t *testing.T) { err = store.CommitTransaction(ctx, &tx2) require.NoError(t, err) - tx, err := store.GetTransaction(ctx, ledgercontroller.NewGetTransactionQuery(tx1.ID). - WithExpandVolumes(). - WithExpandEffectiveVolumes()) + tx, err := store.Transactions().GetOne(ctx, ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("id", tx1.ID), + Expand: []string{"volumes", "effectiveVolumes"}, + }) require.NoError(t, err) require.Equal(t, tx1.Postings, tx.Postings) require.Equal(t, tx1.Reference, tx.Reference) @@ -75,9 +76,10 @@ func TestTransactionsGetWithVolumes(t *testing.T) { }, }, tx.PostCommitVolumes) - tx, err = store.GetTransaction(ctx, ledgercontroller.NewGetTransactionQuery(tx2.ID). - WithExpandVolumes(). - WithExpandEffectiveVolumes()) + tx, err = store.Transactions().GetOne(ctx, ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("id", tx2.ID), + Expand: []string{"volumes", "effectiveVolumes"}, + }) require.NoError(t, err) require.Equal(t, tx2.Postings, tx.Postings) require.Equal(t, tx2.Reference, tx.Reference) @@ -111,7 +113,7 @@ func TestTransactionsCount(t *testing.T) { require.NoError(t, err) } - count, err := store.CountTransactions(ctx, ledgercontroller.NewListTransactionsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}))) + count, err := store.Transactions().Count(ctx, ledgercontroller.ResourceQuery[any]{}) require.NoError(t, err, "counting transactions should not fail") require.Equal(t, 3, count, "count should be equal") } @@ -148,11 +150,17 @@ func TestTransactionUpdateMetadata(t *testing.T) { require.NoError(t, err) // Check that the database returns metadata - tx, err := store.GetTransaction(ctx, ledgercontroller.NewGetTransactionQuery(tx1.ID).WithExpandVolumes().WithExpandEffectiveVolumes()) + tx, err := store.Transactions().GetOne(ctx, ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("id", tx1.ID), + Expand: []string{"volumes", "effectiveVolumes"}, + }) require.NoError(t, err, "getting transaction should not fail") require.Equal(t, tx.Metadata, metadata.Metadata{"foo1": "bar2"}, "metadata should be equal") - tx, err = store.GetTransaction(ctx, ledgercontroller.NewGetTransactionQuery(tx2.ID).WithExpandVolumes().WithExpandEffectiveVolumes()) + tx, err = store.Transactions().GetOne(ctx, ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("id", tx2.ID), + Expand: []string{"volumes", "effectiveVolumes"}, + }) require.NoError(t, err, "getting transaction should not fail") require.Equal(t, tx.Metadata, metadata.Metadata{"foo2": "bar2"}, "metadata should be equal") @@ -185,7 +193,9 @@ func TestTransactionDeleteMetadata(t *testing.T) { require.NoError(t, err) // Get from database and check metadata presence - tx, err := store.GetTransaction(ctx, ledgercontroller.NewGetTransactionQuery(tx1.ID)) + tx, err := store.Transactions().GetOne(ctx, ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("id", tx1.ID), + }) require.NoError(t, err) require.Equal(t, tx.Metadata, metadata.Metadata{"foo1": "bar1", "foo2": "bar2"}) @@ -194,7 +204,9 @@ func TestTransactionDeleteMetadata(t *testing.T) { require.NoError(t, err) require.True(t, modified) - tx, err = store.GetTransaction(ctx, ledgercontroller.NewGetTransactionQuery(tx1.ID)) + tx, err = store.Transactions().GetOne(ctx, ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("id", tx1.ID), + }) require.NoError(t, err) require.Equal(t, metadata.Metadata{"foo2": "bar2"}, tx.Metadata) @@ -437,12 +449,12 @@ func TestTransactionsCommit(t *testing.T) { require.NoError(t, err) } - cursor, err := store.ListTransactions(ctx, ledgercontroller.NewListTransactionsQuery( - ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - ExpandVolumes: true, - }). - WithPageSize(countTx)), - ) + cursor, err := store.Transactions().Paginate(ctx, ledgercontroller.ColumnPaginatedQuery[any]{ + PageSize: countTx, + Options: ledgercontroller.ResourceQuery[any]{ + Expand: []string{"volumes"}, + }, + }) require.NoError(t, err) require.Len(t, cursor.Data, countTx) @@ -506,7 +518,10 @@ func TestInsertTransactionInPast(t *testing.T) { err = store.CommitTransaction(ctx, &tx4) require.NoError(t, err) - tx2FromDatabase, err := store.GetTransaction(ctx, ledgercontroller.NewGetTransactionQuery(tx2.ID).WithExpandVolumes().WithExpandEffectiveVolumes()) + tx2FromDatabase, err := store.Transactions().GetOne(ctx, ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("id", tx2.ID), + Expand: []string{"volumes", "effectiveVolumes"}, + }) require.NoError(t, err) RequireEqual(t, ledger.PostCommitVolumes{ @@ -518,7 +533,9 @@ func TestInsertTransactionInPast(t *testing.T) { }, }, tx2FromDatabase.PostCommitEffectiveVolumes) - account, err := store.GetAccount(ctx, ledgercontroller.NewGetAccountQuery("bank")) + account, err := store.Accounts().GetOne(ctx, ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("address", "bank"), + }) require.NoError(t, err) require.Equal(t, tx4.Timestamp, account.FirstUsage) } @@ -558,10 +575,9 @@ func TestTransactionsRevert(t *testing.T) { require.False(t, reverted) // Revert a not existing transaction - revertedTx, reverted, err = store.RevertTransaction(ctx, 2, time.Time{}) + _, reverted, err = store.RevertTransaction(ctx, 2, time.Time{}) require.True(t, errors.Is(err, postgres.ErrNotFound)) require.False(t, reverted) - require.Nil(t, revertedTx) } func TestTransactionsInsert(t *testing.T) { @@ -660,6 +676,7 @@ func TestTransactionsList(t *testing.T) { tx1 := ledger.NewTransaction(). WithPostings( ledger.NewPosting("world", "alice", "USD", big.NewInt(100)), + ledger.NewPosting("world", "alice", "EUR", big.NewInt(100)), ). WithMetadata(metadata.Metadata{"category": "1"}). WithTimestamp(now.Add(-3 * time.Hour)) @@ -700,9 +717,10 @@ func TestTransactionsList(t *testing.T) { // refresh tx3 // we can't take the result of the call on RevertTransaction nor UpdateTransactionMetadata as the result does not contains pc(e)v tx3 := func() ledger.Transaction { - tx3, err := store.GetTransaction(ctx, ledgercontroller.NewGetTransactionQuery(tx3BeforeRevert.ID). - WithExpandVolumes(). - WithExpandEffectiveVolumes()) + tx3, err := store.Transactions().GetOne(ctx, ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("id", tx3BeforeRevert.ID), + Expand: []string{"volumes", "effectiveVolumes"}, + }) require.NoError(t, err) return *tx3 }() @@ -717,87 +735,114 @@ func TestTransactionsList(t *testing.T) { type testCase struct { name string - query ledgercontroller.PaginatedQueryOptions[ledgercontroller.PITFilterWithVolumes] + query ledgercontroller.ColumnPaginatedQuery[any] expected []ledger.Transaction expectError error } testCases := []testCase{ { name: "nominal", - query: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}), + query: ledgercontroller.ColumnPaginatedQuery[any]{}, expected: []ledger.Transaction{tx5, tx4, tx3, tx2, tx1}, }, { name: "address filter", - query: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Match("account", "bob")), + query: ledgercontroller.ColumnPaginatedQuery[any]{ + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("account", "bob"), + }, + }, expected: []ledger.Transaction{tx2}, }, { name: "address filter using segments matching two addresses by individual segments", - query: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Match("account", "users:amazon")), + query: ledgercontroller.ColumnPaginatedQuery[any]{ + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("account", "users:amazon"), + }, + }, expected: []ledger.Transaction{}, }, { name: "address filter using segment", - query: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Match("account", "users:")), + query: ledgercontroller.ColumnPaginatedQuery[any]{ + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("account", "users:"), + }, + }, expected: []ledger.Transaction{tx5, tx4, tx3}, }, { name: "filter using metadata", - query: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Match("metadata[category]", "2")), + query: ledgercontroller.ColumnPaginatedQuery[any]{ + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("metadata[category]", "2"), + }, + }, expected: []ledger.Transaction{tx2}, }, { name: "using point in time", - query: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ + query: ledgercontroller.ColumnPaginatedQuery[any]{ + Options: ledgercontroller.ResourceQuery[any]{ PIT: pointer.For(now.Add(-time.Hour)), }, - }), + }, expected: []ledger.Transaction{tx3BeforeRevert, tx2, tx1}, }, { name: "filter using invalid key", - query: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Match("invalid", "2")), + query: ledgercontroller.ColumnPaginatedQuery[any]{ + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("invalid", "2"), + }, + }, expectError: ledgercontroller.ErrInvalidQuery{}, }, { name: "reverted transactions", - query: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Match("reverted", true)), + query: ledgercontroller.ColumnPaginatedQuery[any]{ + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("reverted", true), + }, + }, expected: []ledger.Transaction{tx3}, }, { name: "filter using exists metadata", - query: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Exists("metadata", "category")), + query: ledgercontroller.ColumnPaginatedQuery[any]{ + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Exists("metadata", "category"), + }, + }, expected: []ledger.Transaction{tx3, tx2, tx1}, }, { name: "filter using metadata and pit", - query: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: pointer.For(tx3.Timestamp), + query: ledgercontroller.ColumnPaginatedQuery[any]{ + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("metadata[category]", "2"), + PIT: pointer.For(tx3.Timestamp), }, - }). - WithQueryBuilder(query.Match("metadata[category]", "2")), + }, expected: []ledger.Transaction{tx2}, }, { name: "filter using not exists metadata", - query: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Not(query.Exists("metadata", "category"))), + query: ledgercontroller.ColumnPaginatedQuery[any]{ + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Not(query.Exists("metadata", "category")), + }, + }, expected: []ledger.Transaction{tx5, tx4}, }, { name: "filter using timestamp", - query: ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.PITFilterWithVolumes{}). - WithQueryBuilder(query.Match("timestamp", tx5.Timestamp.Format(time.RFC3339Nano))), + query: ledgercontroller.ColumnPaginatedQuery[any]{ + Options: ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("timestamp", tx5.Timestamp.Format(time.RFC3339Nano)), + }, + }, expected: []ledger.Transaction{tx5, tx4}, }, } @@ -807,10 +852,11 @@ func TestTransactionsList(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - tc.query.Options.ExpandVolumes = true - tc.query.Options.ExpandEffectiveVolumes = true + store.DumpTables(ctx, "transactions") - cursor, err := store.ListTransactions(ctx, ledgercontroller.NewListTransactionsQuery(tc.query)) + tc.query.Options.Expand = []string{"volumes", "effectiveVolumes"} + + cursor, err := store.Transactions().Paginate(ctx, tc.query) if tc.expectError != nil { require.True(t, errors.Is(err, tc.expectError)) } else { @@ -818,11 +864,11 @@ func TestTransactionsList(t *testing.T) { require.Len(t, cursor.Data, len(tc.expected)) RequireEqual(t, tc.expected, cursor.Data) - count, err := store.CountTransactions(ctx, ledgercontroller.NewListTransactionsQuery(tc.query)) + count, err := store.Transactions().Count(ctx, tc.query.Options) require.NoError(t, err) require.EqualValues(t, len(tc.expected), count) } }) } -} +} \ No newline at end of file diff --git a/internal/storage/ledger/utils.go b/internal/storage/ledger/utils.go index 11b64f438..a7721de7b 100644 --- a/internal/storage/ledger/utils.go +++ b/internal/storage/ledger/utils.go @@ -8,30 +8,20 @@ import ( func isSegmentedAddress(address string) bool { src := strings.Split(address, ":") - needSegmentCheck := false for _, segment := range src { - needSegmentCheck = segment == "" - if needSegmentCheck { - break + if segment == "" { + return true } } - return needSegmentCheck + return false } func filterAccountAddress(address, key string) string { parts := make([]string, 0) - src := strings.Split(address, ":") - needSegmentCheck := false - for _, segment := range src { - needSegmentCheck = segment == "" - if needSegmentCheck { - break - } - } - - if needSegmentCheck { + if isPartialAddress(address) { + src := strings.Split(address, ":") parts = append(parts, fmt.Sprintf("jsonb_array_length(%s_array) = %d", key, len(src))) for i, segment := range src { @@ -46,3 +36,7 @@ func filterAccountAddress(address, key string) string { return strings.Join(parts, " and ") } + +func isPartialAddress(address any) bool { + return isSegmentedAddress(address.(string)) +} diff --git a/internal/storage/ledger/volumes.go b/internal/storage/ledger/volumes.go index 48f207e21..f11932c33 100644 --- a/internal/storage/ledger/volumes.go +++ b/internal/storage/ledger/volumes.go @@ -2,26 +2,18 @@ package ledger import ( "context" - "fmt" "github.com/formancehq/go-libs/v2/collectionutils" "github.com/formancehq/go-libs/v2/platform/postgres" - "github.com/formancehq/ledger/internal/tracing" - "github.com/formancehq/ledger/pkg/features" - - "github.com/formancehq/go-libs/v2/bun/bunpaginate" - lquery "github.com/formancehq/go-libs/v2/query" - "github.com/formancehq/go-libs/v2/time" ledger "github.com/formancehq/ledger/internal" - ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" - "github.com/uptrace/bun" + "github.com/formancehq/ledger/internal/tracing" ) -func (s *Store) UpdateVolumes(ctx context.Context, accountVolumes ...ledger.AccountsVolumes) (ledger.PostCommitVolumes, error) { +func (store *Store) UpdateVolumes(ctx context.Context, accountVolumes ...ledger.AccountsVolumes) (ledger.PostCommitVolumes, error) { return tracing.TraceWithMetric( ctx, "UpdateBalances", - s.tracer, - s.updateBalancesHistogram, + store.tracer, + store.updateBalancesHistogram, func(ctx context.Context) (ledger.PostCommitVolumes, error) { type AccountsVolumesWithLedger struct { @@ -32,13 +24,13 @@ func (s *Store) UpdateVolumes(ctx context.Context, accountVolumes ...ledger.Acco accountsVolumesWithLedger := collectionutils.Map(accountVolumes, func(from ledger.AccountsVolumes) AccountsVolumesWithLedger { return AccountsVolumesWithLedger{ AccountsVolumes: from, - Ledger: s.ledger.Name, + Ledger: store.ledger.Name, } }) - _, err := s.db.NewInsert(). + _, err := store.db.NewInsert(). Model(&accountsVolumesWithLedger). - ModelTableExpr(s.GetPrefixedRelationName("accounts_volumes")). + ModelTableExpr(store.GetPrefixedRelationName("accounts_volumes")). On("conflict (ledger, accounts_address, asset) do update"). Set("input = accounts_volumes.input + excluded.input"). Set("output = accounts_volumes.output + excluded.output"). @@ -63,200 +55,3 @@ func (s *Store) UpdateVolumes(ctx context.Context, accountVolumes ...ledger.Acco }, ) } - -func (s *Store) selectVolumes(oot, pit *time.Time, useInsertionDate bool, groupLevel int, q lquery.Builder) (*bun.SelectQuery, error) { - ret := s.db.NewSelect() - - var ( - useMetadata bool - needSegmentAddress bool - ) - if q != nil { - err := q.Walk(func(operator, key string, value any) error { - switch { - case key == "account" || key == "address": - if err := s.validateAddressFilter(operator, value); err != nil { - return err - } - if !needSegmentAddress { - needSegmentAddress = isSegmentedAddress(value.(string)) // Safe cast - } - case metadataRegex.Match([]byte(key)): - if operator != "$match" { - return ledgercontroller.NewErrInvalidQuery("'metadata' column can only be used with $match") - } - useMetadata = true - case key == "metadata": - if operator != "$exists" { - return ledgercontroller.NewErrInvalidQuery("'metadata' key filter can only be used with $exists") - } - useMetadata = true - case balanceRegex.Match([]byte(key)): - default: - return ledgercontroller.NewErrInvalidQuery("unknown key '%s' when building query", key) - } - return nil - }) - if err != nil { - return nil, err - } - } - - var selectVolumes *bun.SelectQuery - - if (pit == nil || pit.IsZero()) && (oot == nil || oot.IsZero()) { - selectVolumes = s.db.NewSelect(). - DistinctOn("accounts_address, asset"). - ColumnExpr("accounts_address as address"). - Column("asset", "input", "output"). - ColumnExpr("input - output as balance"). - ModelTableExpr(s.GetPrefixedRelationName("accounts_volumes")). - Where("ledger = ?", s.ledger.Name). - Order("accounts_address", "asset") - } else { - if !s.ledger.HasFeature(features.FeatureMovesHistory, "ON") { - return nil, ledgercontroller.NewErrMissingFeature(features.FeatureMovesHistory) - } - - dateFilterColumn := "effective_date" - if useInsertionDate { - dateFilterColumn = "insertion_date" - } - - selectVolumes = s.db.NewSelect(). - ColumnExpr("accounts_address as address"). - Column("asset"). - ColumnExpr("sum(case when not is_source then amount else 0 end) as input"). - ColumnExpr("sum(case when is_source then amount else 0 end) as output"). - ColumnExpr("sum(case when not is_source then amount else -amount end) as balance"). - ModelTableExpr(s.GetPrefixedRelationName("moves")). - Where("ledger = ?", s.ledger.Name). - GroupExpr("accounts_address, asset"). - Order("accounts_address", "asset") - - if pit != nil && !pit.IsZero() { - selectVolumes = selectVolumes.Where(dateFilterColumn+" <= ?", pit) - } - - if oot != nil && !oot.IsZero() { - selectVolumes = selectVolumes.Where(dateFilterColumn+" >= ?", oot) - } - } - - ret = ret. - ModelTableExpr("(?) volumes", selectVolumes). - Column("address", "asset", "input", "output", "balance") - - if needSegmentAddress { - selectAccount := s.db.NewSelect(). - ModelTableExpr(s.GetPrefixedRelationName("accounts")). - Where("ledger = ? and address = volumes.address", s.ledger.Name). - Column("address_array") - if useMetadata && (pit == nil || pit.IsZero()) { - selectAccount = selectAccount.Column("metadata") - } - - ret = ret. - Join("join lateral (?) accounts on true", selectAccount). - Column("accounts.address_array") - if useMetadata && (pit == nil || pit.IsZero()) { - ret = ret.Column("accounts.metadata") - } - } - - if useMetadata { - switch { - case needSegmentAddress && (pit == nil || pit.IsZero()): - // nothing to do, already handled earlier - case !needSegmentAddress && (pit == nil || pit.IsZero()): - selectAccount := s.db.NewSelect(). - ModelTableExpr(s.GetPrefixedRelationName("accounts")). - Where("ledger = ? and address = volumes.address", s.ledger.Name). - Column("metadata") - - ret = ret. - Join("join lateral (?) accounts on true", selectAccount). - Column("accounts.metadata") - case pit != nil && !pit.IsZero(): - selectAccountMetadata := s.db.NewSelect(). - Column("metadata"). - ModelTableExpr(s.GetPrefixedRelationName("accounts_metadata")). - Where("ledger = ? and accounts_address = volumes.address and date <= ?", s.ledger.Name, pit) - - ret = ret. - Join("join lateral (?) accounts_metadata on true", selectAccountMetadata). - Column("accounts_metadata.metadata") - } - } - - if q != nil { - where, args, err := q.Build(lquery.ContextFn(func(key, operator string, value any) (string, []any, error) { - - switch { - case key == "account" || key == "address": - return filterAccountAddress(value.(string), "address"), nil, nil - case metadataRegex.Match([]byte(key)): - match := metadataRegex.FindAllStringSubmatch(key, 3) - return "metadata @> ?", []any{map[string]any{ - match[0][1]: value, - }}, nil - case key == "metadata": - return "metadata -> ? is not null", []any{value}, nil - case balanceRegex.Match([]byte(key)): - match := balanceRegex.FindAllStringSubmatch(key, 2) - return `balance ` + convertOperatorToSQL(operator) + ` ? and asset = ?`, []any{value, match[0][1]}, nil - default: - return "", nil, ledgercontroller.NewErrInvalidQuery("unknown key '%s' when building query", key) - } - })) - if err != nil { - return nil, err - } - ret = ret.Where(where, args...) - } - - globalQuery := s.db.NewSelect() - globalQuery = globalQuery. - With("query", ret). - ModelTableExpr("query") - - if groupLevel > 0 { - globalQuery = globalQuery. - ColumnExpr(fmt.Sprintf(`(array_to_string((string_to_array(address, ':'))[1:LEAST(array_length(string_to_array(address, ':'),1),%d)],':')) as account`, groupLevel)). - ColumnExpr("asset"). - ColumnExpr("sum(input) as input"). - ColumnExpr("sum(output) as output"). - ColumnExpr("sum(balance) as balance"). - GroupExpr("account, asset") - } else { - globalQuery = globalQuery.ColumnExpr("address as account, asset, input, output, balance") - } - - return globalQuery, nil -} - -func (s *Store) GetVolumesWithBalances(ctx context.Context, q ledgercontroller.GetVolumesWithBalancesQuery) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { - return tracing.TraceWithMetric( - ctx, - "GetVolumesWithBalances", - s.tracer, - s.getVolumesWithBalancesHistogram, - func(ctx context.Context) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { - selectVolumes, err := s.selectVolumes( - q.Options.Options.OOT, - q.Options.Options.PIT, - q.Options.Options.UseInsertionDate, - q.Options.Options.GroupLvl, - q.Options.QueryBuilder, - ) - if err != nil { - return nil, err - } - return bunpaginate.UsingOffset[ledgercontroller.PaginatedQueryOptions[ledgercontroller.FiltersForVolumes], ledger.VolumesWithBalanceByAssetByAccount]( - ctx, - selectVolumes, - bunpaginate.OffsetPaginatedQuery[ledgercontroller.PaginatedQueryOptions[ledgercontroller.FiltersForVolumes]](q), - ) - }, - ) -} diff --git a/internal/storage/ledger/volumes_test.go b/internal/storage/ledger/volumes_test.go index 2da872f8b..27b0e5b82 100644 --- a/internal/storage/ledger/volumes_test.go +++ b/internal/storage/ledger/volumes_test.go @@ -105,27 +105,34 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for insertion date", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.FiltersForVolumes{UseInsertionDate: true}))) + volumes, err := store.Volumes().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + Opts: ledgercontroller.GetVolumesOptions{ + UseInsertionDate: true, + }, + }, + }) require.NoError(t, err) - require.Len(t, volumes.Data, 4) }) t.Run("Get all volumes with balance for effective date", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions(ledgercontroller.FiltersForVolumes{UseInsertionDate: false}))) + volumes, err := store.Volumes().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{}) require.NoError(t, err) - require.Len(t, volumes.Data, 4) }) t.Run("Get all volumes with balance for insertion date with previous pit", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{PIT: &previousPIT, OOT: nil}, - UseInsertionDate: true, - }))) + volumes, err := store.Volumes().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + Opts: ledgercontroller.GetVolumesOptions{ + UseInsertionDate: true, + }, + PIT: &previousPIT, + }, + }) require.NoError(t, err) require.Len(t, volumes.Data, 3) @@ -142,35 +149,42 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for insertion date with futur pit", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{PIT: &futurPIT, OOT: nil}, - UseInsertionDate: true, - }))) + volumes, err := store.Volumes().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + Opts: ledgercontroller.GetVolumesOptions{ + UseInsertionDate: true, + }, + PIT: &futurPIT, + }, + }) require.NoError(t, err) - require.Len(t, volumes.Data, 4) }) t.Run("Get all volumes with balance for insertion date with previous oot", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{PIT: nil, OOT: &previousOOT}, - UseInsertionDate: true, - }))) + volumes, err := store.Volumes().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + Opts: ledgercontroller.GetVolumesOptions{ + UseInsertionDate: true, + }, + OOT: &previousOOT, + }, + }) require.NoError(t, err) - require.Len(t, volumes.Data, 4) }) t.Run("Get all volumes with balance for insertion date with future oot", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{PIT: nil, OOT: &futurOOT}, - UseInsertionDate: true, - }))) + volumes, err := store.Volumes().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + Opts: ledgercontroller.GetVolumesOptions{ + UseInsertionDate: true, + }, + OOT: &futurOOT, + }, + }) require.NoError(t, err) require.Len(t, volumes.Data, 3) @@ -187,11 +201,11 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for effective date with previous pit", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{PIT: &previousPIT, OOT: nil}, - UseInsertionDate: false, - }))) + volumes, err := store.Volumes().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + PIT: &previousPIT, + }, + }) require.NoError(t, err) require.Len(t, volumes.Data, 3) @@ -208,35 +222,33 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for effective date with futur pit", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{PIT: &futurPIT, OOT: nil}, - UseInsertionDate: false, - }))) + volumes, err := store.Volumes().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + PIT: &futurPIT, + }, + }) require.NoError(t, err) - require.Len(t, volumes.Data, 4) }) t.Run("Get all volumes with balance for effective date with previous oot", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{PIT: nil, OOT: &previousOOT}, - UseInsertionDate: false, - }))) + volumes, err := store.Volumes().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + OOT: &previousOOT, + }, + }) require.NoError(t, err) - require.Len(t, volumes.Data, 4) }) t.Run("Get all volumes with balance for effective date with futur oot", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{PIT: nil, OOT: &futurOOT}, - UseInsertionDate: false, - }))) + volumes, err := store.Volumes().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + OOT: &futurOOT, + }, + }) require.NoError(t, err) require.Len(t, volumes.Data, 3) @@ -253,11 +265,15 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for insertion date with future PIT and now OOT", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{PIT: &futurPIT, OOT: &now}, - UseInsertionDate: true, - }))) + volumes, err := store.Volumes().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + Opts: ledgercontroller.GetVolumesOptions{ + UseInsertionDate: true, + }, + PIT: &futurPIT, + OOT: &now, + }, + }) require.NoError(t, err) require.Len(t, volumes.Data, 4) @@ -270,16 +286,19 @@ func TestVolumesList(t *testing.T) { Balance: big.NewInt(-50), }, }, volumes.Data[0]) - }) t.Run("Get all volumes with balance for insertion date with previous OOT and now PIT", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{PIT: &now, OOT: &previousOOT}, - UseInsertionDate: true, - }))) + volumes, err := store.Volumes().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + Opts: ledgercontroller.GetVolumesOptions{ + UseInsertionDate: true, + }, + PIT: &now, + OOT: &previousOOT, + }, + }) require.NoError(t, err) require.Len(t, volumes.Data, 3) @@ -292,16 +311,16 @@ func TestVolumesList(t *testing.T) { Balance: big.NewInt(50), }, }, volumes.Data[0]) - }) t.Run("Get all volumes with balance for effective date with future PIT and now OOT", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{PIT: &futurPIT, OOT: &now}, - UseInsertionDate: false, - }))) + volumes, err := store.Volumes().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + PIT: &futurPIT, + OOT: &now, + }, + }) require.NoError(t, err) require.Len(t, volumes.Data, 3) @@ -318,11 +337,12 @@ func TestVolumesList(t *testing.T) { t.Run("Get all volumes with balance for insertion date with previous OOT and now PIT", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{PIT: &now, OOT: &previousOOT}, - UseInsertionDate: false, - }))) + volumes, err := store.Volumes().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + PIT: &now, + OOT: &previousOOT, + }, + }) require.NoError(t, err) require.Len(t, volumes.Data, 4) @@ -335,21 +355,20 @@ func TestVolumesList(t *testing.T) { Balance: big.NewInt(-50), }, }, volumes.Data[0]) - }) t.Run("Get account1 volume and Balance for insertion date with previous OOT and now PIT", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, - ledgercontroller.NewGetVolumesWithBalancesQuery( - ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{PIT: &now, OOT: &previousOOT}, - UseInsertionDate: false, - }).WithQueryBuilder(query.Match("account", "account:1"))), + volumes, err := store.Volumes().Paginate(ctx, + ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + PIT: &now, + OOT: &previousOOT, + Builder: query.Match("account", "account:1"), + }, + }, ) - require.NoError(t, err) require.Len(t, volumes.Data, 1) require.Equal(t, ledger.VolumesWithBalanceByAssetByAccount{ @@ -367,26 +386,27 @@ func TestVolumesList(t *testing.T) { t.Run("Using Metadata regex", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, - ledgercontroller.NewGetVolumesWithBalancesQuery( - ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{}).WithQueryBuilder(query.Match("metadata[foo]", "bar"))), + volumes, err := store.Volumes().Paginate(ctx, + ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + Builder: query.Match("metadata[foo]", "bar"), + }, + }, ) - require.NoError(t, err) require.Len(t, volumes.Data, 1) - }) t.Run("Using exists metadata filter 1", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, - ledgercontroller.NewGetVolumesWithBalancesQuery( - ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{}).WithQueryBuilder(query.Exists("metadata", "category"))), + volumes, err := store.Volumes().Paginate(ctx, + ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + Builder: query.Exists("metadata", "category"), + }, + }, ) - require.NoError(t, err) require.Len(t, volumes.Data, 2) }) @@ -394,12 +414,13 @@ func TestVolumesList(t *testing.T) { t.Run("Using exists metadata filter 2", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, - ledgercontroller.NewGetVolumesWithBalancesQuery( - ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{}).WithQueryBuilder(query.Exists("metadata", "foo"))), + volumes, err := store.Volumes().Paginate(ctx, + ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + Builder: query.Exists("metadata", "foo"), + }, + }, ) - require.NoError(t, err) require.Len(t, volumes.Data, 1) }) @@ -465,52 +486,59 @@ func TestVolumesAggregate(t *testing.T) { t.Run("Aggregation Volumes with balance for GroupLvl 0", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery( - ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ + volumes, err := store.Volumes().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + Opts: ledgercontroller.GetVolumesOptions{ UseInsertionDate: true, - GroupLvl: 0, - }).WithQueryBuilder(query.Match("account", "account::")))) - + }, + Builder: query.Match("account", "account::"), + }, + }) require.NoError(t, err) require.Len(t, volumes.Data, 7) }) t.Run("Aggregation Volumes with balance for GroupLvl 1", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery( - ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ + volumes, err := store.Volumes().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + Opts: ledgercontroller.GetVolumesOptions{ UseInsertionDate: true, GroupLvl: 1, - }).WithQueryBuilder(query.Match("account", "account::")))) - + }, + Builder: query.Match("account", "account::"), + }, + }) require.NoError(t, err) require.Len(t, volumes.Data, 2) }) t.Run("Aggregation Volumes with balance for GroupLvl 2", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery( - ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ + volumes, err := store.Volumes().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + Opts: ledgercontroller.GetVolumesOptions{ UseInsertionDate: true, GroupLvl: 2, - }).WithQueryBuilder(query.Match("account", "account::")))) - + }, + Builder: query.Match("account", "account::"), + }, + }) require.NoError(t, err) require.Len(t, volumes.Data, 4) }) t.Run("Aggregation Volumes with balance for GroupLvl 3", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery( - ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ + volumes, err := store.Volumes().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + Opts: ledgercontroller.GetVolumesOptions{ UseInsertionDate: true, GroupLvl: 3, - }).WithQueryBuilder(query.Match("account", "account::")))) - + }, + Builder: query.Match("account", "account::"), + }, + }) require.NoError(t, err) require.Len(t, volumes.Data, 7) }) @@ -518,16 +546,16 @@ func TestVolumesAggregate(t *testing.T) { t.Run("Aggregation Volumes with balance for GroupLvl 1 && PIT && OOT && effectiveDate", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery( - ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &pit, - OOT: &oot, - }, + volumes, err := store.Volumes().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + Opts: ledgercontroller.GetVolumesOptions{ GroupLvl: 1, - }).WithQueryBuilder(query.Match("account", "account::")))) - + }, + PIT: &pit, + OOT: &oot, + Builder: query.Match("account", "account::"), + }, + }) require.NoError(t, err) require.Len(t, volumes.Data, 2) require.Equal(t, volumes.Data[0], ledger.VolumesWithBalanceByAssetByAccount{ @@ -552,18 +580,17 @@ func TestVolumesAggregate(t *testing.T) { t.Run("Aggregation Volumes with balance for GroupLvl 1 && PIT && OOT && effectiveDate && Balance Filter 1", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery( - ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{ - PIT: &pit, - OOT: &oot, - }, - UseInsertionDate: false, - GroupLvl: 1, - }).WithQueryBuilder( - query.And(query.Match("account", "account::"), query.Gte("balance[EUR]", 50))))) + volumes, err := store.Volumes().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + Opts: ledgercontroller.GetVolumesOptions{ + GroupLvl: 1, + }, + PIT: &pit, + OOT: &oot, + Builder: query.And(query.Match("account", "account::"), query.Gte("balance[EUR]", 50)), + }, + }) require.NoError(t, err) require.Len(t, volumes.Data, 1) require.Equal(t, volumes.Data[0], ledger.VolumesWithBalanceByAssetByAccount{ @@ -579,17 +606,17 @@ func TestVolumesAggregate(t *testing.T) { t.Run("Aggregation Volumes with balance for GroupLvl 1 && Balance Filter 2", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgercontroller.NewGetVolumesWithBalancesQuery( - ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ - PITFilter: ledgercontroller.PITFilter{}, - UseInsertionDate: true, + volumes, err := store.Volumes().Paginate(ctx, ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + Opts: ledgercontroller.GetVolumesOptions{ GroupLvl: 2, - }).WithQueryBuilder( - query.Or( + UseInsertionDate: true, + }, + Builder: query.Or( query.Match("account", "account:1:"), - query.Lte("balance[USD]", 0))))) - + query.Lte("balance[USD]", 0)), + }, + }) require.NoError(t, err) require.Len(t, volumes.Data, 3) require.Equal(t, volumes.Data[0], ledger.VolumesWithBalanceByAssetByAccount{ @@ -623,16 +650,18 @@ func TestVolumesAggregate(t *testing.T) { t.Run("filter using account matching, metadata, and group", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, - ledgercontroller.NewGetVolumesWithBalancesQuery( - ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ + volumes, err := store.Volumes().Paginate(ctx, + ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + Opts: ledgercontroller.GetVolumesOptions{ GroupLvl: 1, - }).WithQueryBuilder(query.And( - query.Match("account", "account::"), - query.Match("metadata[foo]", "bar"), - ))), - ) + }, + Builder: query.And( + query.Match("account", "account::"), + query.Match("metadata[foo]", "bar"), + ), + }, + }) require.NoError(t, err) require.Len(t, volumes.Data, 1) @@ -641,19 +670,19 @@ func TestVolumesAggregate(t *testing.T) { t.Run("filter using account matching, metadata, and group and PIT", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, - ledgercontroller.NewGetVolumesWithBalancesQuery( - ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ + volumes, err := store.Volumes().Paginate(ctx, + ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + Opts: ledgercontroller.GetVolumesOptions{ GroupLvl: 1, - PITFilter: ledgercontroller.PITFilter{ - PIT: pointer.For(now.Add(time.Minute)), - }, - }).WithQueryBuilder(query.And( - query.Match("account", "account::"), - query.Match("metadata[foo]", "bar"), - ))), - ) + }, + PIT: pointer.For(now.Add(time.Minute)), + Builder: query.And( + query.Match("account", "account::"), + query.Match("metadata[foo]", "bar"), + ), + }, + }) require.NoError(t, err) require.Len(t, volumes.Data, 1) @@ -662,16 +691,16 @@ func TestVolumesAggregate(t *testing.T) { t.Run("filter using metadata matching only", func(t *testing.T) { t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, - ledgercontroller.NewGetVolumesWithBalancesQuery( - ledgercontroller.NewPaginatedQueryOptions( - ledgercontroller.FiltersForVolumes{ + volumes, err := store.Volumes().Paginate(ctx, + ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ + Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ + Opts: ledgercontroller.GetVolumesOptions{ GroupLvl: 1, - }).WithQueryBuilder(query.And( - query.Match("metadata[foo]", "bar"), - ))), + }, + Builder: query.Match("metadata[foo]", "bar"), + }, + }, ) - require.NoError(t, err) require.Len(t, volumes.Data, 1) }) diff --git a/internal/volumes.go b/internal/volumes.go index 3abcca42b..1c96bece4 100644 --- a/internal/volumes.go +++ b/internal/volumes.go @@ -153,3 +153,7 @@ func (a PostCommitVolumes) Merge(volumes PostCommitVolumes) PostCommitVolumes { return a } + +type AggregatedVolumes struct { + Aggregated VolumesByAssets `bun:"aggregated,type:jsonb"` +} diff --git a/test/e2e/api_transactions_list_test.go b/test/e2e/api_transactions_list_test.go index a4ff42884..d162b7367 100644 --- a/test/e2e/api_transactions_list_test.go +++ b/test/e2e/api_transactions_list_test.go @@ -6,7 +6,10 @@ import ( "fmt" "github.com/formancehq/go-libs/v2/bun/bunpaginate" "github.com/formancehq/go-libs/v2/logging" + "github.com/formancehq/go-libs/v2/query" . "github.com/formancehq/go-libs/v2/testing/api" + libtime "github.com/formancehq/go-libs/v2/time" + ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" "github.com/formancehq/ledger/pkg/client/models/components" "github.com/formancehq/ledger/pkg/client/models/operations" . "github.com/formancehq/ledger/pkg/testserver" @@ -46,7 +49,7 @@ var _ = Context("Ledger transactions list API tests", func() { ) When(fmt.Sprintf("creating %d transactions", txCount), func() { var ( - timestamp = time.Now().Round(time.Second).UTC() + timestamp = time.Now() transactions []components.V2Transaction ) JustBeforeEach(func() { @@ -73,6 +76,12 @@ var _ = Context("Ledger transactions list API tests", func() { Source: "world", Destination: fmt.Sprintf("account:%d", i), }, + { + Amount: big.NewInt(100), + Asset: "EUR", + Source: "world", + Destination: fmt.Sprintf("account:%d", i), + }, }, Timestamp: pointer.For(txTimestamp), }, @@ -193,7 +202,6 @@ var _ = Context("Ledger transactions list API tests", func() { operations.V2ListTransactionsRequest{ Cursor: rsp.Previous, Ledger: "default", - Expand: pointer.For("volumes,effectiveVolumes"), }, ) Expect(err).ToNot(HaveOccurred()) @@ -232,21 +240,12 @@ var _ = Context("Ledger transactions list API tests", func() { }) It("Should be ok", func() { Expect(response.Next).NotTo(BeNil()) - cursor := &bunpaginate.ColumnPaginatedQuery[map[string]any]{} + cursor := &ledgercontroller.ColumnPaginatedQuery[any]{} Expect(bunpaginate.UnmarshalCursor(*response.Next, cursor)).To(BeNil()) - Expect(cursor.Options).To(Equal(map[string]any{ - "qb": map[string]any{ - "$match": map[string]any{ - "source": "world", - }, - }, - "pageSize": float64(10), - "options": map[string]any{ - "pit": now.Format(time.RFC3339), - "oot": nil, - "volumes": false, - "effectiveVolumes": false, - }, + Expect(cursor.PageSize).To(Equal(uint64(10))) + Expect(cursor.Options).To(Equal(ledgercontroller.ResourceQuery[any]{ + Builder: query.Match("source", "world"), + PIT: pointer.For(libtime.New(now)), })) }) }) @@ -284,30 +283,15 @@ var _ = Context("Ledger transactions list API tests", func() { }) It("Should be ok", func() { Expect(response.Next).NotTo(BeNil()) - cursor := &bunpaginate.ColumnPaginatedQuery[map[string]any]{} + cursor := &ledgercontroller.ColumnPaginatedQuery[any]{} Expect(bunpaginate.UnmarshalCursor(*response.Next, cursor)).To(BeNil()) - Expect(cursor.Options).To(Equal(map[string]any{ - "qb": map[string]any{ - "$and": []any{ - map[string]any{ - "$match": map[string]any{ - "source": "world", - }, - }, - map[string]any{ - "$match": map[string]any{ - "destination": "account:", - }, - }, - }, - }, - "pageSize": float64(10), - "options": map[string]any{ - "pit": now.Format(time.RFC3339), - "oot": nil, - "volumes": false, - "effectiveVolumes": false, - }, + Expect(cursor.PageSize).To(Equal(uint64(10))) + Expect(cursor.Options).To(Equal(ledgercontroller.ResourceQuery[any]{ + Builder: query.And( + query.Match("source", "world"), + query.Match("destination", "account:"), + ), + PIT: pointer.For(libtime.New(now)), })) }) }) diff --git a/tools/generator/go.sum b/tools/generator/go.sum index 5241a1b9e..53074ed2c 100644 --- a/tools/generator/go.sum +++ b/tools/generator/go.sum @@ -280,6 +280,8 @@ github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= +github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= From 8cf1718cdac23479cfadc45542700db3d95ef31d Mon Sep 17 00:00:00 2001 From: rsln <93119071+reslene@users.noreply.github.com> Date: Fri, 20 Dec 2024 14:34:24 +0100 Subject: [PATCH 65/71] fix(open-api): make insertedAt optional (#616) * fix(openapi): insertedAt optional * chore(openapi): pre-commit + generate-client * fix: fix tests and upgrade depdendencies --------- Co-authored-by: Geoffrey Ragot --- docs/api/README.md | 2 +- go.mod | 99 ++++----- go.sum | 199 +++++++++--------- openapi.yaml | 1 - openapi/v2.yaml | 1 - pkg/client/.speakeasy/gen.lock | 6 +- .../docs/models/components/v2errorsenum.md | 2 +- .../docs/models/components/v2transaction.md | 2 +- pkg/client/models/components/v2errorsenum.go | 4 +- pkg/client/models/components/v2transaction.go | 6 +- pkg/testserver/helpers.go | 2 +- test/e2e/api_balances_aggregated_test.go | 2 +- tools/generator/go.mod | 26 +-- tools/generator/go.sum | 199 +++++++++--------- 14 files changed, 276 insertions(+), 275 deletions(-) diff --git a/docs/api/README.md b/docs/api/README.md index 6dee2686e..f5b50b13e 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -2635,7 +2635,7 @@ Authorization ( Scopes: ledger:write ) |Name|Type|Required|Restrictions|Description| |---|---|---|---|---| -|insertedAt|string(date-time)|true|none|none| +|insertedAt|string(date-time)|false|none|none| |timestamp|string(date-time)|true|none|none| |postings|[[V2Posting](#schemav2posting)]|true|none|none| |reference|string|false|none|none| diff --git a/go.mod b/go.mod index d321b71cd..fece61599 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/dop251/goja v0.0.0-20241009100908-5f46f2705ca3 github.com/formancehq/go-libs/v2 v2.0.1-0.20241121194732-b79c48b683f2 github.com/formancehq/ledger/pkg/client v0.0.0-00010101000000-000000000000 - github.com/go-chi/chi/v5 v5.1.0 + github.com/go-chi/chi/v5 v5.2.0 github.com/go-chi/cors v1.2.1 github.com/google/go-cmp v0.6.0 github.com/google/uuid v1.6.0 @@ -24,7 +24,7 @@ require ( github.com/jackc/pgx/v5 v5.7.1 github.com/jamiealquiza/tachymeter v2.0.0+incompatible github.com/logrusorgru/aurora v2.0.3+incompatible - github.com/nats-io/nats.go v1.37.0 + github.com/nats-io/nats.go v1.38.0 github.com/onsi/ginkgo/v2 v2.22.0 github.com/onsi/gomega v1.35.1 github.com/ory/dockertest/v3 v3.11.0 @@ -40,14 +40,14 @@ require ( github.com/uptrace/bun/extra/bundebug v1.2.5 github.com/xeipuuv/gojsonschema v1.2.0 github.com/xo/dburl v0.23.2 - go.opentelemetry.io/otel v1.32.0 - go.opentelemetry.io/otel/metric v1.32.0 - go.opentelemetry.io/otel/sdk/metric v1.32.0 - go.opentelemetry.io/otel/trace v1.32.0 + go.opentelemetry.io/otel v1.33.0 + go.opentelemetry.io/otel/metric v1.33.0 + go.opentelemetry.io/otel/sdk/metric v1.33.0 + go.opentelemetry.io/otel/trace v1.33.0 go.uber.org/fx v1.23.0 go.uber.org/mock v0.5.0 golang.org/x/oauth2 v0.24.0 - golang.org/x/sync v0.9.0 + golang.org/x/sync v0.10.0 ) require gopkg.in/yaml.v3 v3.0.1 // indirect @@ -55,6 +55,7 @@ require gopkg.in/yaml.v3 v3.0.1 // indirect require ( github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/jackc/pgxlisten v0.0.0-20241106001234-1d6f6656415c // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect ) require ( @@ -70,19 +71,19 @@ require ( github.com/ajg/form v1.5.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/aws/aws-msk-iam-sasl-signer-go v1.0.0 // indirect - github.com/aws/aws-sdk-go-v2 v1.32.5 // indirect - github.com/aws/aws-sdk-go-v2/config v1.28.5 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.17.46 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 // indirect - github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.24 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 // indirect + github.com/aws/aws-sdk-go-v2 v1.32.7 // indirect + github.com/aws/aws-sdk-go-v2/config v1.28.7 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.48 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.22 // indirect + github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.5.2 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.24.6 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.33.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.24.8 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.33.3 // indirect github.com/aws/smithy-go v1.22.1 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect @@ -102,7 +103,7 @@ require ( github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731 // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/formancehq/numscript v0.0.9 + github.com/formancehq/numscript v0.0.10 github.com/go-chi/chi v4.1.2+incompatible // indirect github.com/go-chi/render v1.0.3 // indirect github.com/go-logr/logr v1.4.2 // indirect @@ -119,7 +120,7 @@ require ( github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/schema v1.4.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -138,7 +139,7 @@ require ( github.com/klauspost/compress v1.17.11 // indirect github.com/lithammer/shortuuid/v3 v3.0.7 // indirect github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect - github.com/mailru/easyjson v0.7.7 // indirect + github.com/mailru/easyjson v0.9.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/minio/highwayhash v1.0.3 // indirect @@ -148,20 +149,20 @@ require ( github.com/muhlemmer/httpforwarded v0.1.0 // indirect github.com/nats-io/jwt/v2 v2.7.0 // indirect github.com/nats-io/nats-server/v2 v2.10.22 // indirect - github.com/nats-io/nkeys v0.4.8 // indirect + github.com/nats-io/nkeys v0.4.9 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opencontainers/runc v1.1.14 // indirect - github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect - github.com/riandyrn/otelchi v0.10.1 // indirect + github.com/riandyrn/otelchi v0.11.0 // indirect github.com/rs/cors v1.11.1 // indirect - github.com/shirou/gopsutil/v4 v4.24.10 // indirect + github.com/shirou/gopsutil/v4 v4.24.11 // indirect github.com/shomali11/util v0.0.0-20220717175126-f0771b70947f // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/tklauser/go-sysconf v0.3.14 // indirect @@ -181,34 +182,34 @@ require ( github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/zitadel/oidc/v2 v2.12.2 // indirect - go.opentelemetry.io/contrib/instrumentation/host v0.57.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 // indirect - go.opentelemetry.io/contrib/instrumentation/runtime v0.57.0 // indirect - go.opentelemetry.io/contrib/propagators/b3 v1.32.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 // indirect - go.opentelemetry.io/otel/log v0.8.0 // indirect - go.opentelemetry.io/otel/sdk v1.32.0 // indirect - go.opentelemetry.io/proto/otlp v1.3.1 // indirect + go.opentelemetry.io/contrib/instrumentation/host v0.58.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect + go.opentelemetry.io/contrib/instrumentation/runtime v0.58.0 // indirect + go.opentelemetry.io/contrib/propagators/b3 v1.33.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.33.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0 // indirect + go.opentelemetry.io/otel/log v0.9.0 // indirect + go.opentelemetry.io/otel/sdk v1.33.0 // indirect + go.opentelemetry.io/proto/otlp v1.4.0 // indirect go.uber.org/dig v1.18.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.29.0 // indirect - golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect - golang.org/x/net v0.31.0 // indirect - golang.org/x/sys v0.27.0 // indirect - golang.org/x/text v0.20.0 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.7.0 // indirect - golang.org/x/tools v0.27.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 // indirect - google.golang.org/grpc v1.68.0 // indirect - google.golang.org/protobuf v1.35.2 // indirect + golang.org/x/tools v0.28.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241219192143-6b3ec007d9bb // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241219192143-6b3ec007d9bb // indirect + google.golang.org/grpc v1.69.2 // indirect + google.golang.org/protobuf v1.36.0 // indirect gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 103fec330..5db9eaddd 100644 --- a/go.sum +++ b/go.sum @@ -30,32 +30,32 @@ github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYW github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/aws/aws-msk-iam-sasl-signer-go v1.0.0 h1:UyjtGmO0Uwl/K+zpzPwLoXzMhcN9xmnR2nrqJoBrg3c= github.com/aws/aws-msk-iam-sasl-signer-go v1.0.0/go.mod h1:TJAXuFs2HcMib3sN5L0gUC+Q01Qvy3DemvA55WuC+iA= -github.com/aws/aws-sdk-go-v2 v1.32.5 h1:U8vdWJuY7ruAkzaOdD7guwJjD06YSKmnKCJs7s3IkIo= -github.com/aws/aws-sdk-go-v2 v1.32.5/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= -github.com/aws/aws-sdk-go-v2/config v1.28.5 h1:Za41twdCXbuyyWv9LndXxZZv3QhTG1DinqlFsSuvtI0= -github.com/aws/aws-sdk-go-v2/config v1.28.5/go.mod h1:4VsPbHP8JdcdUDmbTVgNL/8w9SqOkM5jyY8ljIxLO3o= -github.com/aws/aws-sdk-go-v2/credentials v1.17.46 h1:AU7RcriIo2lXjUfHFnFKYsLCwgbz1E7Mm95ieIRDNUg= -github.com/aws/aws-sdk-go-v2/credentials v1.17.46/go.mod h1:1FmYyLGL08KQXQ6mcTlifyFXfJVCNJTVGuQP4m0d/UA= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 h1:sDSXIrlsFSFJtWKLQS4PUWRvrT580rrnuLydJrCQ/yA= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20/go.mod h1:WZ/c+w0ofps+/OUqMwWgnfrgzZH1DZO1RIkktICsqnY= -github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.24 h1:HfLyPCysN3MqXSQIP83f/0fNTvb8ELXBv76Jaa3LvCs= -github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.24/go.mod h1:WNDtzVHjS5Ct1HJLcVaclQivrWvK3lQWmQkaT7tzr4M= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 h1:4usbeaes3yJnCFC7kfeyhkdkPtoRYPa/hTmCqMpKpLI= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24/go.mod h1:5CI1JemjVwde8m2WG3cz23qHKPOxbpkq0HaoreEgLIY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 h1:N1zsICrQglfzaBnrfM0Ys00860C+QFwu6u/5+LomP+o= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24/go.mod h1:dCn9HbJ8+K31i8IQ8EWmWj0EiIk0+vKiHNMxTTYveAg= +github.com/aws/aws-sdk-go-v2 v1.32.7 h1:ky5o35oENWi0JYWUZkB7WYvVPP+bcRF5/Iq7JWSb5Rw= +github.com/aws/aws-sdk-go-v2 v1.32.7/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= +github.com/aws/aws-sdk-go-v2/config v1.28.7 h1:GduUnoTXlhkgnxTD93g1nv4tVPILbdNQOzav+Wpg7AE= +github.com/aws/aws-sdk-go-v2/config v1.28.7/go.mod h1:vZGX6GVkIE8uECSUHB6MWAUsd4ZcG2Yq/dMa4refR3M= +github.com/aws/aws-sdk-go-v2/credentials v1.17.48 h1:IYdLD1qTJ0zanRavulofmqut4afs45mOWEI+MzZtTfQ= +github.com/aws/aws-sdk-go-v2/credentials v1.17.48/go.mod h1:tOscxHN3CGmuX9idQ3+qbkzrjVIx32lqDSU1/0d/qXs= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.22 h1:kqOrpojG71DxJm/KDPO+Z/y1phm1JlC8/iT+5XRmAn8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.22/go.mod h1:NtSFajXVVL8TA2QNngagVZmUtXciyrHOt7xgz4faS/M= +github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.5.2 h1:fo+GuZNME9oGDc7VY+EBT+oCrco6RjRgUp1bKTcaHrU= +github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.5.2/go.mod h1:fnqb94UO6YCjBIic4WaqDYkNVAEFWOWiReVHitBBWW0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26 h1:I/5wmGMffY4happ8NOCuIUEWGUvvFp5NSeQcXl9RHcI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26/go.mod h1:FR8f4turZtNy6baO0KJ5FJUmXH/cSkI9fOngs0yl6mA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26 h1:zXFLuEuMMUOvEARXFUVJdfqZ4bvvSgdGRq/ATcrQxzM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26/go.mod h1:3o2Wpy0bogG1kyOPrgkXA8pgIfEEv0+m19O9D5+W8y8= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 h1:wtpJ4zcwrSbwhECWQoI/g6WM9zqCcSpHDJIWSbMLOu4= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5/go.mod h1:qu/W9HXQbbQ4+1+JcZp0ZNPV31ym537ZJN+fiS7Ti8E= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.6 h1:3zu537oLmsPfDMyjnUS2g+F2vITgy5pB74tHI+JBNoM= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.6/go.mod h1:WJSZH2ZvepM6t6jwu4w/Z45Eoi75lPN7DcydSRtJg6Y= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 h1:K0OQAsDywb0ltlFrZm0JHPY3yZp/S9OaoLU33S7vPS8= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5/go.mod h1:ORITg+fyuMoeiQFiVGoqB3OydVTLkClw/ljbblMq6Cc= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.1 h1:6SZUVRQNvExYlMLbHdlKB48x0fLbc2iVROyaNEwBHbU= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.1/go.mod h1:GqWyYCwLXnlUB1lOAXQyNSPqPLQJvmo8J0DWBzp9mtg= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7 h1:8eUsivBQzZHqe/3FE+cqwfH+0p5Jo8PFM/QYQSmeZ+M= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7/go.mod h1:kLPQvGUmxn/fqiCrDeohwG33bq2pQpGeY62yRO6Nrh0= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.8 h1:CvuUmnXI7ebaUAhbJcDy9YQx8wHR69eZ9I7q5hszt/g= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.8/go.mod h1:XDeGv1opzwm8ubxddF0cgqkZWsyOtw4lr6dxwmb6YQg= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7 h1:F2rBfNAL5UyswqoeWv9zs74N/NanhK16ydHW1pahX6E= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7/go.mod h1:JfyQ0g2JG8+Krq0EuZNnRwX0mU0HrwY/tG6JNfcqh4k= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.3 h1:Xgv/hyNgvLda/M9l9qxXc4UFSgppnRczLxlMs5Ae/QY= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.3/go.mod h1:5Gn+d+VaaRgsjewpMvGazt0WfcFO+Md4wLOuBfGR9Bc= github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= @@ -106,8 +106,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/formancehq/go-libs/v2 v2.0.1-0.20241121194732-b79c48b683f2 h1:JsacSDe6MYGCm04ZY/VJyYTUAWg8jhOBZm6AGyErF/I= github.com/formancehq/go-libs/v2 v2.0.1-0.20241121194732-b79c48b683f2/go.mod h1:ZGHVcAC54ZbPgeoGKy/d31CHy94RMhHQlX+Vui15ST8= -github.com/formancehq/numscript v0.0.9 h1:TJxA0dEmVSL0qA04WApgsrs/GDfwttieQkaIe5nd2Ao= -github.com/formancehq/numscript v0.0.9/go.mod h1:btuSv05cYwi9BvLRxVs5zrunU+O1vTgigG1T6UsawcY= +github.com/formancehq/numscript v0.0.10 h1:ElvYpoayUX5tHtCCR18ihJTjNlHzdkE4M0IqSm9aufg= +github.com/formancehq/numscript v0.0.10/go.mod h1:btuSv05cYwi9BvLRxVs5zrunU+O1vTgigG1T6UsawcY= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/gkampitakis/ciinfo v0.3.0 h1:gWZlOC2+RYYttL0hBqcoQhM7h1qNkVqvRCV1fOvpAv8= @@ -118,8 +118,8 @@ github.com/gkampitakis/go-snaps v0.5.4 h1:GX+dkKmVsRenz7SoTbdIEL4KQARZctkMiZ8ZKp github.com/gkampitakis/go-snaps v0.5.4/go.mod h1:ZABkO14uCuVxBHAXAfKG+bqNz+aa1bGPAg8jkI0Nk8Y= github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= -github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= -github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0= +github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= @@ -168,8 +168,8 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -216,7 +216,6 @@ github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqRO github.com/jeremija/gosubmit v0.2.7/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= @@ -233,8 +232,8 @@ github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczG github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -260,10 +259,10 @@ github.com/nats-io/jwt/v2 v2.7.0 h1:J+ZnaaMGQi3xSB8iOhVM5ipiWCDrQvgEoitTwWFyOYw= github.com/nats-io/jwt/v2 v2.7.0/go.mod h1:ZdWS1nZa6WMZfFwwgpEaqBV8EPGVgOTDHN/wTbz0Y5A= github.com/nats-io/nats-server/v2 v2.10.22 h1:Yt63BGu2c3DdMoBZNcR6pjGQwk/asrKU7VX846ibxDA= github.com/nats-io/nats-server/v2 v2.10.22/go.mod h1:X/m1ye9NYansUXYFrbcDwUi/blHkrgHh2rgCJaakonk= -github.com/nats-io/nats.go v1.37.0 h1:07rauXbVnnJvv1gfIyghFEo6lUcYRY0WXc3x7x0vUxE= -github.com/nats-io/nats.go v1.37.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= -github.com/nats-io/nkeys v0.4.8 h1:+wee30071y3vCZAYRsnrmIPaOe47A/SkK/UBDPdIV70= -github.com/nats-io/nkeys v0.4.8/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= +github.com/nats-io/nats.go v1.38.0 h1:A7P+g7Wjp4/NWqDOOP/K6hfhr54DvdDQUznt5JFg9XA= +github.com/nats-io/nats.go v1.38.0/go.mod h1:IGUM++TwokGnXPs82/wCuiHS02/aKrdYUQkU8If6yjw= +github.com/nats-io/nkeys v0.4.9 h1:qe9Faq2Gxwi6RZnZMXfmGMZkg3afLLOtrU+gDZJ35b0= +github.com/nats-io/nkeys v0.4.9/go.mod h1:jcMqs+FLG+W5YO36OX6wFIFcmpdAns+w1Wm6D3I/evE= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= @@ -282,8 +281,8 @@ github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNG github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI= github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= -github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= -github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -295,15 +294,15 @@ github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+ github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/riandyrn/otelchi v0.10.1 h1:x86f8M0pGvjW3tJUxpva4cpdNtMydLPnarIXHssYUy4= -github.com/riandyrn/otelchi v0.10.1/go.mod h1:SWarhA5rdeiCNq+Ygc4p59ZGM5AtYCiyPU/3Q5rzT0M= +github.com/riandyrn/otelchi v0.11.0 h1:x9MFoTgHcwCC2DdWkTEEZ2ZQFkbl6z7GXLQtTANN6Gk= +github.com/riandyrn/otelchi v0.11.0/go.mod h1:FlBYmG9fBQu0jFRvZZrATP4mDvLX2H5gwELfpZvNlxY= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shirou/gopsutil/v4 v4.24.10 h1:7VOzPtfw/5YDU+jLEoBwXwxJbQetULywoSV4RYY7HkM= -github.com/shirou/gopsutil/v4 v4.24.10/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8= +github.com/shirou/gopsutil/v4 v4.24.11 h1:WaU9xqGFKvFfsUv94SXcUPD7rCkU0vr/asVdQOBZNj8= +github.com/shirou/gopsutil/v4 v4.24.11/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8= github.com/shomali11/parallelizer v0.0.0-20220717173222-a6776fbf40a9/go.mod h1:QsLM53l8gzX0sQbOjVir85bzOUucuJEF8JgE39wD7w0= github.com/shomali11/util v0.0.0-20180607005212-e0f70fd665ff/go.mod h1:WWE2GJM9B5UpdOiwH2val10w/pvJ2cUUQOOA/4LgOng= github.com/shomali11/util v0.0.0-20220717175126-f0771b70947f h1:OM0LVaVycWC+/j5Qra7USyCg2sc+shg3KwygAA+pYvA= @@ -387,42 +386,44 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zitadel/oidc/v2 v2.12.2 h1:3kpckg4rurgw7w7aLJrq7yvRxb2pkNOtD08RH42vPEs= github.com/zitadel/oidc/v2 v2.12.2/go.mod h1:vhP26g1g4YVntcTi0amMYW3tJuid70nxqxf+kb6XKgg= -go.opentelemetry.io/contrib/instrumentation/host v0.57.0 h1:1gfzOyXEuCrrwCXF81LO3DQ4rll6YBKfAQHPl+03mik= -go.opentelemetry.io/contrib/instrumentation/host v0.57.0/go.mod h1:pHBt+1Rhz99VBX7AQVgwcKPf611zgD6pQy7VwBNMFmE= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94= -go.opentelemetry.io/contrib/instrumentation/runtime v0.57.0 h1:kJB5wMVorwre8QzEodzTAbzm9FOOah0zvG+V4abNlEE= -go.opentelemetry.io/contrib/instrumentation/runtime v0.57.0/go.mod h1:Nup4TgnOyEJWmVq9sf/ASH3ZJiAXwWHd5xZCHG7Sg9M= -go.opentelemetry.io/contrib/propagators/b3 v1.32.0 h1:MazJBz2Zf6HTN/nK/s3Ru1qme+VhWU5hm83QxEP+dvw= -go.opentelemetry.io/contrib/propagators/b3 v1.32.0/go.mod h1:B0s70QHYPrJwPOwD1o3V/R8vETNOG9N3qZf4LDYvA30= -go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= -go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 h1:t/Qur3vKSkUCcDVaSumWF2PKHt85pc7fRvFuoVT8qFU= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0/go.mod h1:Rl61tySSdcOJWoEgYZVtmnKdA0GeKrSqkHC1t+91CH8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 h1:9kV11HXBHZAvuPUZxmMWrH8hZn/6UnHX4K0mu36vNsU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0/go.mod h1:JyA0FHXe22E1NeNiHmVp7kFHglnexDQ7uRWDiiJ1hKQ= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 h1:SZmDnHcgp3zwlPBS2JX2urGYe/jBKEIT6ZedHRUyCz8= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0/go.mod h1:fdWW0HtZJ7+jNpTKUR0GpMEDP69nR8YBJQxNiVCE3jk= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s= -go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk= -go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8= -go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= -go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= -go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= -go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= -go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= -go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= -go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= -go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= -go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= -go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/host v0.58.0 h1:vstBQcCXLI4Q98dK0Ijw3PPRD+Lq9kTzK46wloSB3uk= +go.opentelemetry.io/contrib/instrumentation/host v0.58.0/go.mod h1:D628SeDOkn0JL2Y0Pl212TDIQzmGroBuW+CYDF4mLSA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= +go.opentelemetry.io/contrib/instrumentation/runtime v0.58.0 h1:GrcF8ABgnBHQFgp4zu5/jTSqLkoJ9uiDz2e7eKkjq+w= +go.opentelemetry.io/contrib/instrumentation/runtime v0.58.0/go.mod h1:+kxR5prZLoFAJVXJWZKWO2e4PY2dYyXIRNklBuOyzpM= +go.opentelemetry.io/contrib/propagators/b3 v1.33.0 h1:ig/IsHyyoQ1F1d6FUDIIW5oYpsuTVtN16AyGOgdjAHQ= +go.opentelemetry.io/contrib/propagators/b3 v1.33.0/go.mod h1:EsVYoNy+Eol5znb6wwN3XQTILyjl040gUpEnUSNZfsk= +go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= +go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0 h1:7F29RDmnlqk6B5d+sUqemt8TBfDqxryYW5gX6L74RFA= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0/go.mod h1:ZiGDq7xwDMKmWDrN1XsXAj0iC7hns+2DhxBFSncNHSE= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.33.0 h1:bSjzTvsXZbLSWU8hnZXcKmEVaJjjnandxD0PxThhVU8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.33.0/go.mod h1:aj2rilHL8WjXY1I5V+ra+z8FELtk681deydgYT8ikxU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0 h1:FiOTYABOX4tdzi8A0+mtzcsTmi6WBOxk66u0f1Mj9Gs= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0/go.mod h1:xyo5rS8DgzV0Jtsht+LCEMwyiDbjpsxBpWETwFRF0/4= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0 h1:W5AWUn/IVe8RFb5pZx1Uh9Laf/4+Qmm4kJL5zPuvR+0= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0/go.mod h1:mzKxJywMNBdEX8TSJais3NnsVZUaJ+bAy6UxPTng2vk= +go.opentelemetry.io/otel/log v0.9.0 h1:0OiWRefqJ2QszpCiqwGO0u9ajMPe17q6IscQvvp3czY= +go.opentelemetry.io/otel/log v0.9.0/go.mod h1:WPP4OJ+RBkQ416jrFCQFuFKtXKD6mOoYCQm6ykK8VaU= +go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= +go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= +go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= +go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= +go.opentelemetry.io/otel/sdk/metric v1.33.0 h1:Gs5VK9/WUJhNXZgn8MR6ITatvAmKeIuCtNbsP3JkNqU= +go.opentelemetry.io/otel/sdk/metric v1.33.0/go.mod h1:dL5ykHZmm1B1nVRk9dDjChwDmt81MjVp3gLkQRwKf/Q= +go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= +go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= +go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= +go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw= go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg= @@ -440,10 +441,10 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= -golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= -golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= -golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo= +golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -456,16 +457,16 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= -golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -487,8 +488,8 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -497,8 +498,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -506,20 +507,20 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= -golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= +golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= +golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 h1:pgr/4QbFyktUv9CtQ/Fq4gzEE6/Xs7iCXbktaGzLHbQ= -google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go.mod h1:+D9ySVjN8nY8YCVjc5O7PZDIdZporIDY3KaGfJunh88= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= -google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= -google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= -google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= -google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/genproto/googleapis/api v0.0.0-20241219192143-6b3ec007d9bb h1:B7GIB7sr443wZ/EAEl7VZjmh1V6qzkt5V+RYcUYtS1U= +google.golang.org/genproto/googleapis/api v0.0.0-20241219192143-6b3ec007d9bb/go.mod h1:E5//3O5ZIG2l71Xnt+P/CYUY8Bxs8E7WMoZ9tlcMbAY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241219192143-6b3ec007d9bb h1:3oy2tynMOP1QbTC0MsNNAV+Se8M2Bd0A5+x1QHyw+pI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241219192143-6b3ec007d9bb/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= +google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU= +google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ= +google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/openapi.yaml b/openapi.yaml index f91ba4288..43b85c962 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -3301,7 +3301,6 @@ components: - id - metadata - reverted - - insertedAt V2PostTransaction: type: object required: diff --git a/openapi/v2.yaml b/openapi/v2.yaml index 4d879e322..ee11f4902 100644 --- a/openapi/v2.yaml +++ b/openapi/v2.yaml @@ -1585,7 +1585,6 @@ components: - id - metadata - reverted - - insertedAt V2PostTransaction: type: object required: diff --git a/pkg/client/.speakeasy/gen.lock b/pkg/client/.speakeasy/gen.lock index 69d84c282..2ceb89946 100644 --- a/pkg/client/.speakeasy/gen.lock +++ b/pkg/client/.speakeasy/gen.lock @@ -1,12 +1,12 @@ lockVersion: 2.0.0 id: a9ac79e1-e429-4ee3-96c4-ec973f19bec3 management: - docChecksum: 1596e540a51d2b443378a8de0698b2f4 + docChecksum: a75fbcee4705d8d6603062086b542987 docVersion: v1 speakeasyVersion: 1.351.0 generationVersion: 2.384.1 - releaseVersion: 0.5.0 - configChecksum: 598ba44ea6e4a0fa32d0a3e02d870ace + releaseVersion: 0.5.1 + configChecksum: c054a60832022bfc610a27df523d0e92 features: go: additionalDependencies: 0.1.0 diff --git a/pkg/client/docs/models/components/v2errorsenum.md b/pkg/client/docs/models/components/v2errorsenum.md index e6959db96..357b94f39 100644 --- a/pkg/client/docs/models/components/v2errorsenum.md +++ b/pkg/client/docs/models/components/v2errorsenum.md @@ -22,4 +22,4 @@ | `V2ErrorsEnumInterpreterParse` | INTERPRETER_PARSE | | `V2ErrorsEnumInterpreterRuntime` | INTERPRETER_RUNTIME | | `V2ErrorsEnumLedgerAlreadyExists` | LEDGER_ALREADY_EXISTS | -| `V2ErrorsEnumBucketOutdated` | BUCKET_OUTDATED | \ No newline at end of file +| `V2ErrorsEnumOutdatedSchema` | OUTDATED_SCHEMA | \ No newline at end of file diff --git a/pkg/client/docs/models/components/v2transaction.md b/pkg/client/docs/models/components/v2transaction.md index d09ce0ce0..341d08068 100644 --- a/pkg/client/docs/models/components/v2transaction.md +++ b/pkg/client/docs/models/components/v2transaction.md @@ -5,7 +5,7 @@ | Field | Type | Required | Description | Example | | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -| `InsertedAt` | [time.Time](https://pkg.go.dev/time#Time) | :heavy_check_mark: | N/A | | +| `InsertedAt` | [*time.Time](https://pkg.go.dev/time#Time) | :heavy_minus_sign: | N/A | | | `Timestamp` | [time.Time](https://pkg.go.dev/time#Time) | :heavy_check_mark: | N/A | | | `Postings` | [][components.V2Posting](../../models/components/v2posting.md) | :heavy_check_mark: | N/A | | | `Reference` | **string* | :heavy_minus_sign: | N/A | ref:001 | diff --git a/pkg/client/models/components/v2errorsenum.go b/pkg/client/models/components/v2errorsenum.go index be953b76e..1c90aaf4d 100644 --- a/pkg/client/models/components/v2errorsenum.go +++ b/pkg/client/models/components/v2errorsenum.go @@ -27,7 +27,7 @@ const ( V2ErrorsEnumInterpreterParse V2ErrorsEnum = "INTERPRETER_PARSE" V2ErrorsEnumInterpreterRuntime V2ErrorsEnum = "INTERPRETER_RUNTIME" V2ErrorsEnumLedgerAlreadyExists V2ErrorsEnum = "LEDGER_ALREADY_EXISTS" - V2ErrorsEnumBucketOutdated V2ErrorsEnum = "BUCKET_OUTDATED" + V2ErrorsEnumOutdatedSchema V2ErrorsEnum = "OUTDATED_SCHEMA" ) func (e V2ErrorsEnum) ToPointer() *V2ErrorsEnum { @@ -73,7 +73,7 @@ func (e *V2ErrorsEnum) UnmarshalJSON(data []byte) error { fallthrough case "LEDGER_ALREADY_EXISTS": fallthrough - case "BUCKET_OUTDATED": + case "OUTDATED_SCHEMA": *e = V2ErrorsEnum(v) return nil default: diff --git a/pkg/client/models/components/v2transaction.go b/pkg/client/models/components/v2transaction.go index 61c5af7ef..d9910a223 100644 --- a/pkg/client/models/components/v2transaction.go +++ b/pkg/client/models/components/v2transaction.go @@ -9,7 +9,7 @@ import ( ) type V2Transaction struct { - InsertedAt time.Time `json:"insertedAt"` + InsertedAt *time.Time `json:"insertedAt,omitempty"` Timestamp time.Time `json:"timestamp"` Postings []V2Posting `json:"postings"` Reference *string `json:"reference,omitempty"` @@ -34,9 +34,9 @@ func (v *V2Transaction) UnmarshalJSON(data []byte) error { return nil } -func (o *V2Transaction) GetInsertedAt() time.Time { +func (o *V2Transaction) GetInsertedAt() *time.Time { if o == nil { - return time.Time{} + return nil } return o.InsertedAt } diff --git a/pkg/testserver/helpers.go b/pkg/testserver/helpers.go index 2e611eddd..0ba9f320e 100644 --- a/pkg/testserver/helpers.go +++ b/pkg/testserver/helpers.go @@ -26,7 +26,7 @@ func ConvertSDKTxToCoreTX(tx *components.V2Transaction) ledger.Transaction { TransactionData: ledger.TransactionData{ Postings: collectionutils.Map(tx.Postings, ConvertSDKPostingToCorePosting), Timestamp: time.New(tx.Timestamp), - InsertedAt: time.New(tx.InsertedAt), + InsertedAt: time.New(*tx.InsertedAt), Metadata: tx.Metadata, Reference: func() string { if tx.Reference == nil { diff --git a/test/e2e/api_balances_aggregated_test.go b/test/e2e/api_balances_aggregated_test.go index 375444698..e57d86292 100644 --- a/test/e2e/api_balances_aggregated_test.go +++ b/test/e2e/api_balances_aggregated_test.go @@ -99,7 +99,7 @@ var _ = Context("Ledger engine tests", func() { }) Expect(err).To(Succeed()) - firstTransactionsInsertedAt = ret[2].V2BulkElementResultCreateTransaction.Data.InsertedAt + firstTransactionsInsertedAt = *ret[2].V2BulkElementResultCreateTransaction.Data.InsertedAt _, err = CreateBulk(ctx, testServer.GetValue(), operations.V2CreateBulkRequest{ RequestBody: []components.V2BulkElement{ diff --git a/tools/generator/go.mod b/tools/generator/go.mod index cf92d0371..12fa366ed 100644 --- a/tools/generator/go.mod +++ b/tools/generator/go.mod @@ -32,8 +32,8 @@ require ( github.com/dop251/goja v0.0.0-20241009100908-5f46f2705ca3 // indirect github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731 // indirect github.com/fatih/color v1.18.0 // indirect - github.com/formancehq/numscript v0.0.9 // indirect - github.com/go-chi/chi/v5 v5.1.0 // indirect + github.com/formancehq/numscript v0.0.10 // indirect + github.com/go-chi/chi/v5 v5.2.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect @@ -50,14 +50,13 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/lithammer/shortuuid/v3 v3.0.7 // indirect github.com/logrusorgru/aurora v2.0.3+incompatible // indirect - github.com/mailru/easyjson v0.7.7 // indirect + github.com/mailru/easyjson v0.9.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect - github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect @@ -67,19 +66,20 @@ require ( github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240816141633-0a40785b4f41 // indirect - go.opentelemetry.io/otel v1.32.0 // indirect - go.opentelemetry.io/otel/log v0.8.0 // indirect - go.opentelemetry.io/otel/metric v1.32.0 // indirect - go.opentelemetry.io/otel/trace v1.32.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.33.0 // indirect + go.opentelemetry.io/otel/log v0.9.0 // indirect + go.opentelemetry.io/otel/metric v1.33.0 // indirect + go.opentelemetry.io/otel/trace v1.33.0 // indirect go.uber.org/dig v1.18.0 // indirect go.uber.org/fx v1.23.0 // indirect go.uber.org/mock v0.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.29.0 // indirect - golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect - golang.org/x/sync v0.9.0 // indirect - golang.org/x/sys v0.27.0 // indirect - golang.org/x/text v0.20.0 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/tools/generator/go.sum b/tools/generator/go.sum index 53074ed2c..29c1e3556 100644 --- a/tools/generator/go.sum +++ b/tools/generator/go.sum @@ -30,32 +30,32 @@ github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYW github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/aws/aws-msk-iam-sasl-signer-go v1.0.0 h1:UyjtGmO0Uwl/K+zpzPwLoXzMhcN9xmnR2nrqJoBrg3c= github.com/aws/aws-msk-iam-sasl-signer-go v1.0.0/go.mod h1:TJAXuFs2HcMib3sN5L0gUC+Q01Qvy3DemvA55WuC+iA= -github.com/aws/aws-sdk-go-v2 v1.32.5 h1:U8vdWJuY7ruAkzaOdD7guwJjD06YSKmnKCJs7s3IkIo= -github.com/aws/aws-sdk-go-v2 v1.32.5/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= -github.com/aws/aws-sdk-go-v2/config v1.28.5 h1:Za41twdCXbuyyWv9LndXxZZv3QhTG1DinqlFsSuvtI0= -github.com/aws/aws-sdk-go-v2/config v1.28.5/go.mod h1:4VsPbHP8JdcdUDmbTVgNL/8w9SqOkM5jyY8ljIxLO3o= -github.com/aws/aws-sdk-go-v2/credentials v1.17.46 h1:AU7RcriIo2lXjUfHFnFKYsLCwgbz1E7Mm95ieIRDNUg= -github.com/aws/aws-sdk-go-v2/credentials v1.17.46/go.mod h1:1FmYyLGL08KQXQ6mcTlifyFXfJVCNJTVGuQP4m0d/UA= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 h1:sDSXIrlsFSFJtWKLQS4PUWRvrT580rrnuLydJrCQ/yA= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20/go.mod h1:WZ/c+w0ofps+/OUqMwWgnfrgzZH1DZO1RIkktICsqnY= -github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.24 h1:HfLyPCysN3MqXSQIP83f/0fNTvb8ELXBv76Jaa3LvCs= -github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.24/go.mod h1:WNDtzVHjS5Ct1HJLcVaclQivrWvK3lQWmQkaT7tzr4M= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 h1:4usbeaes3yJnCFC7kfeyhkdkPtoRYPa/hTmCqMpKpLI= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24/go.mod h1:5CI1JemjVwde8m2WG3cz23qHKPOxbpkq0HaoreEgLIY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 h1:N1zsICrQglfzaBnrfM0Ys00860C+QFwu6u/5+LomP+o= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24/go.mod h1:dCn9HbJ8+K31i8IQ8EWmWj0EiIk0+vKiHNMxTTYveAg= +github.com/aws/aws-sdk-go-v2 v1.32.7 h1:ky5o35oENWi0JYWUZkB7WYvVPP+bcRF5/Iq7JWSb5Rw= +github.com/aws/aws-sdk-go-v2 v1.32.7/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= +github.com/aws/aws-sdk-go-v2/config v1.28.7 h1:GduUnoTXlhkgnxTD93g1nv4tVPILbdNQOzav+Wpg7AE= +github.com/aws/aws-sdk-go-v2/config v1.28.7/go.mod h1:vZGX6GVkIE8uECSUHB6MWAUsd4ZcG2Yq/dMa4refR3M= +github.com/aws/aws-sdk-go-v2/credentials v1.17.48 h1:IYdLD1qTJ0zanRavulofmqut4afs45mOWEI+MzZtTfQ= +github.com/aws/aws-sdk-go-v2/credentials v1.17.48/go.mod h1:tOscxHN3CGmuX9idQ3+qbkzrjVIx32lqDSU1/0d/qXs= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.22 h1:kqOrpojG71DxJm/KDPO+Z/y1phm1JlC8/iT+5XRmAn8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.22/go.mod h1:NtSFajXVVL8TA2QNngagVZmUtXciyrHOt7xgz4faS/M= +github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.5.2 h1:fo+GuZNME9oGDc7VY+EBT+oCrco6RjRgUp1bKTcaHrU= +github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.5.2/go.mod h1:fnqb94UO6YCjBIic4WaqDYkNVAEFWOWiReVHitBBWW0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26 h1:I/5wmGMffY4happ8NOCuIUEWGUvvFp5NSeQcXl9RHcI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26/go.mod h1:FR8f4turZtNy6baO0KJ5FJUmXH/cSkI9fOngs0yl6mA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26 h1:zXFLuEuMMUOvEARXFUVJdfqZ4bvvSgdGRq/ATcrQxzM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26/go.mod h1:3o2Wpy0bogG1kyOPrgkXA8pgIfEEv0+m19O9D5+W8y8= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 h1:wtpJ4zcwrSbwhECWQoI/g6WM9zqCcSpHDJIWSbMLOu4= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5/go.mod h1:qu/W9HXQbbQ4+1+JcZp0ZNPV31ym537ZJN+fiS7Ti8E= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.6 h1:3zu537oLmsPfDMyjnUS2g+F2vITgy5pB74tHI+JBNoM= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.6/go.mod h1:WJSZH2ZvepM6t6jwu4w/Z45Eoi75lPN7DcydSRtJg6Y= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 h1:K0OQAsDywb0ltlFrZm0JHPY3yZp/S9OaoLU33S7vPS8= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5/go.mod h1:ORITg+fyuMoeiQFiVGoqB3OydVTLkClw/ljbblMq6Cc= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.1 h1:6SZUVRQNvExYlMLbHdlKB48x0fLbc2iVROyaNEwBHbU= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.1/go.mod h1:GqWyYCwLXnlUB1lOAXQyNSPqPLQJvmo8J0DWBzp9mtg= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7 h1:8eUsivBQzZHqe/3FE+cqwfH+0p5Jo8PFM/QYQSmeZ+M= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7/go.mod h1:kLPQvGUmxn/fqiCrDeohwG33bq2pQpGeY62yRO6Nrh0= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.8 h1:CvuUmnXI7ebaUAhbJcDy9YQx8wHR69eZ9I7q5hszt/g= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.8/go.mod h1:XDeGv1opzwm8ubxddF0cgqkZWsyOtw4lr6dxwmb6YQg= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7 h1:F2rBfNAL5UyswqoeWv9zs74N/NanhK16ydHW1pahX6E= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7/go.mod h1:JfyQ0g2JG8+Krq0EuZNnRwX0mU0HrwY/tG6JNfcqh4k= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.3 h1:Xgv/hyNgvLda/M9l9qxXc4UFSgppnRczLxlMs5Ae/QY= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.3/go.mod h1:5Gn+d+VaaRgsjewpMvGazt0WfcFO+Md4wLOuBfGR9Bc= github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= @@ -104,8 +104,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/formancehq/go-libs/v2 v2.0.1-0.20241121194732-b79c48b683f2 h1:JsacSDe6MYGCm04ZY/VJyYTUAWg8jhOBZm6AGyErF/I= github.com/formancehq/go-libs/v2 v2.0.1-0.20241121194732-b79c48b683f2/go.mod h1:ZGHVcAC54ZbPgeoGKy/d31CHy94RMhHQlX+Vui15ST8= -github.com/formancehq/numscript v0.0.9 h1:TJxA0dEmVSL0qA04WApgsrs/GDfwttieQkaIe5nd2Ao= -github.com/formancehq/numscript v0.0.9/go.mod h1:btuSv05cYwi9BvLRxVs5zrunU+O1vTgigG1T6UsawcY= +github.com/formancehq/numscript v0.0.10 h1:ElvYpoayUX5tHtCCR18ihJTjNlHzdkE4M0IqSm9aufg= +github.com/formancehq/numscript v0.0.10/go.mod h1:btuSv05cYwi9BvLRxVs5zrunU+O1vTgigG1T6UsawcY= github.com/gkampitakis/ciinfo v0.3.0 h1:gWZlOC2+RYYttL0hBqcoQhM7h1qNkVqvRCV1fOvpAv8= github.com/gkampitakis/ciinfo v0.3.0/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= @@ -114,8 +114,8 @@ github.com/gkampitakis/go-snaps v0.5.4 h1:GX+dkKmVsRenz7SoTbdIEL4KQARZctkMiZ8ZKp github.com/gkampitakis/go-snaps v0.5.4/go.mod h1:ZABkO14uCuVxBHAXAfKG+bqNz+aa1bGPAg8jkI0Nk8Y= github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= -github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= -github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0= +github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= @@ -154,8 +154,8 @@ github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= @@ -194,7 +194,6 @@ github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZ github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -207,8 +206,8 @@ github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczG github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -228,10 +227,10 @@ github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY= github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0= -github.com/nats-io/nats.go v1.37.0 h1:07rauXbVnnJvv1gfIyghFEo6lUcYRY0WXc3x7x0vUxE= -github.com/nats-io/nats.go v1.37.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= -github.com/nats-io/nkeys v0.4.8 h1:+wee30071y3vCZAYRsnrmIPaOe47A/SkK/UBDPdIV70= -github.com/nats-io/nkeys v0.4.8/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= +github.com/nats-io/nats.go v1.38.0 h1:A7P+g7Wjp4/NWqDOOP/K6hfhr54DvdDQUznt5JFg9XA= +github.com/nats-io/nats.go v1.38.0/go.mod h1:IGUM++TwokGnXPs82/wCuiHS02/aKrdYUQkU8If6yjw= +github.com/nats-io/nkeys v0.4.9 h1:qe9Faq2Gxwi6RZnZMXfmGMZkg3afLLOtrU+gDZJ35b0= +github.com/nats-io/nkeys v0.4.9/go.mod h1:jcMqs+FLG+W5YO36OX6wFIFcmpdAns+w1Wm6D3I/evE= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= @@ -248,8 +247,8 @@ github.com/opencontainers/runc v1.1.14 h1:rgSuzbmgz5DUJjeSnw337TxDbRuqjs6iqQck/2 github.com/opencontainers/runc v1.1.14/go.mod h1:E4C2z+7BxR7GHXp0hAY53mek+x49X1LjPNeMTfRGvOA= github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA= github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI= -github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= -github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -261,15 +260,15 @@ github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+ github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/riandyrn/otelchi v0.10.1 h1:x86f8M0pGvjW3tJUxpva4cpdNtMydLPnarIXHssYUy4= -github.com/riandyrn/otelchi v0.10.1/go.mod h1:SWarhA5rdeiCNq+Ygc4p59ZGM5AtYCiyPU/3Q5rzT0M= +github.com/riandyrn/otelchi v0.11.0 h1:x9MFoTgHcwCC2DdWkTEEZ2ZQFkbl6z7GXLQtTANN6Gk= +github.com/riandyrn/otelchi v0.11.0/go.mod h1:FlBYmG9fBQu0jFRvZZrATP4mDvLX2H5gwELfpZvNlxY= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shirou/gopsutil/v4 v4.24.10 h1:7VOzPtfw/5YDU+jLEoBwXwxJbQetULywoSV4RYY7HkM= -github.com/shirou/gopsutil/v4 v4.24.10/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8= +github.com/shirou/gopsutil/v4 v4.24.11 h1:WaU9xqGFKvFfsUv94SXcUPD7rCkU0vr/asVdQOBZNj8= +github.com/shirou/gopsutil/v4 v4.24.11/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8= github.com/shomali11/util v0.0.0-20220717175126-f0771b70947f h1:OM0LVaVycWC+/j5Qra7USyCg2sc+shg3KwygAA+pYvA= github.com/shomali11/util v0.0.0-20220717175126-f0771b70947f/go.mod h1:9POpw/crb6YrseaYBOwraL0lAYy0aOW79eU3bvMxgbM= github.com/shomali11/xsql v0.0.0-20190608141458-bf76292144df h1:SVCDTuzM3KEk8WBwSSw7RTPLw9ajzBaXDg39Bo6xIeU= @@ -340,42 +339,44 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zitadel/oidc/v2 v2.12.2 h1:3kpckg4rurgw7w7aLJrq7yvRxb2pkNOtD08RH42vPEs= github.com/zitadel/oidc/v2 v2.12.2/go.mod h1:vhP26g1g4YVntcTi0amMYW3tJuid70nxqxf+kb6XKgg= -go.opentelemetry.io/contrib/instrumentation/host v0.57.0 h1:1gfzOyXEuCrrwCXF81LO3DQ4rll6YBKfAQHPl+03mik= -go.opentelemetry.io/contrib/instrumentation/host v0.57.0/go.mod h1:pHBt+1Rhz99VBX7AQVgwcKPf611zgD6pQy7VwBNMFmE= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94= -go.opentelemetry.io/contrib/instrumentation/runtime v0.57.0 h1:kJB5wMVorwre8QzEodzTAbzm9FOOah0zvG+V4abNlEE= -go.opentelemetry.io/contrib/instrumentation/runtime v0.57.0/go.mod h1:Nup4TgnOyEJWmVq9sf/ASH3ZJiAXwWHd5xZCHG7Sg9M= -go.opentelemetry.io/contrib/propagators/b3 v1.32.0 h1:MazJBz2Zf6HTN/nK/s3Ru1qme+VhWU5hm83QxEP+dvw= -go.opentelemetry.io/contrib/propagators/b3 v1.32.0/go.mod h1:B0s70QHYPrJwPOwD1o3V/R8vETNOG9N3qZf4LDYvA30= -go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= -go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 h1:t/Qur3vKSkUCcDVaSumWF2PKHt85pc7fRvFuoVT8qFU= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0/go.mod h1:Rl61tySSdcOJWoEgYZVtmnKdA0GeKrSqkHC1t+91CH8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 h1:9kV11HXBHZAvuPUZxmMWrH8hZn/6UnHX4K0mu36vNsU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0/go.mod h1:JyA0FHXe22E1NeNiHmVp7kFHglnexDQ7uRWDiiJ1hKQ= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 h1:SZmDnHcgp3zwlPBS2JX2urGYe/jBKEIT6ZedHRUyCz8= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0/go.mod h1:fdWW0HtZJ7+jNpTKUR0GpMEDP69nR8YBJQxNiVCE3jk= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s= -go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk= -go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8= -go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= -go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= -go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= -go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= -go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= -go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= -go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= -go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= -go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= -go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/host v0.58.0 h1:vstBQcCXLI4Q98dK0Ijw3PPRD+Lq9kTzK46wloSB3uk= +go.opentelemetry.io/contrib/instrumentation/host v0.58.0/go.mod h1:D628SeDOkn0JL2Y0Pl212TDIQzmGroBuW+CYDF4mLSA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= +go.opentelemetry.io/contrib/instrumentation/runtime v0.58.0 h1:GrcF8ABgnBHQFgp4zu5/jTSqLkoJ9uiDz2e7eKkjq+w= +go.opentelemetry.io/contrib/instrumentation/runtime v0.58.0/go.mod h1:+kxR5prZLoFAJVXJWZKWO2e4PY2dYyXIRNklBuOyzpM= +go.opentelemetry.io/contrib/propagators/b3 v1.33.0 h1:ig/IsHyyoQ1F1d6FUDIIW5oYpsuTVtN16AyGOgdjAHQ= +go.opentelemetry.io/contrib/propagators/b3 v1.33.0/go.mod h1:EsVYoNy+Eol5znb6wwN3XQTILyjl040gUpEnUSNZfsk= +go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= +go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0 h1:7F29RDmnlqk6B5d+sUqemt8TBfDqxryYW5gX6L74RFA= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0/go.mod h1:ZiGDq7xwDMKmWDrN1XsXAj0iC7hns+2DhxBFSncNHSE= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.33.0 h1:bSjzTvsXZbLSWU8hnZXcKmEVaJjjnandxD0PxThhVU8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.33.0/go.mod h1:aj2rilHL8WjXY1I5V+ra+z8FELtk681deydgYT8ikxU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0 h1:FiOTYABOX4tdzi8A0+mtzcsTmi6WBOxk66u0f1Mj9Gs= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0/go.mod h1:xyo5rS8DgzV0Jtsht+LCEMwyiDbjpsxBpWETwFRF0/4= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0 h1:W5AWUn/IVe8RFb5pZx1Uh9Laf/4+Qmm4kJL5zPuvR+0= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0/go.mod h1:mzKxJywMNBdEX8TSJais3NnsVZUaJ+bAy6UxPTng2vk= +go.opentelemetry.io/otel/log v0.9.0 h1:0OiWRefqJ2QszpCiqwGO0u9ajMPe17q6IscQvvp3czY= +go.opentelemetry.io/otel/log v0.9.0/go.mod h1:WPP4OJ+RBkQ416jrFCQFuFKtXKD6mOoYCQm6ykK8VaU= +go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= +go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= +go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= +go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= +go.opentelemetry.io/otel/sdk/metric v1.33.0 h1:Gs5VK9/WUJhNXZgn8MR6ITatvAmKeIuCtNbsP3JkNqU= +go.opentelemetry.io/otel/sdk/metric v1.33.0/go.mod h1:dL5ykHZmm1B1nVRk9dDjChwDmt81MjVp3gLkQRwKf/Q= +go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= +go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= +go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= +go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw= go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg= @@ -388,16 +389,16 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= -golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= -golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= -golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= -golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= -golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo= +golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -406,20 +407,20 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= -golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= -golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= -google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 h1:pgr/4QbFyktUv9CtQ/Fq4gzEE6/Xs7iCXbktaGzLHbQ= -google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go.mod h1:+D9ySVjN8nY8YCVjc5O7PZDIdZporIDY3KaGfJunh88= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= -google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= -google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= -google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= -google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= +golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= +google.golang.org/genproto/googleapis/api v0.0.0-20241219192143-6b3ec007d9bb h1:B7GIB7sr443wZ/EAEl7VZjmh1V6qzkt5V+RYcUYtS1U= +google.golang.org/genproto/googleapis/api v0.0.0-20241219192143-6b3ec007d9bb/go.mod h1:E5//3O5ZIG2l71Xnt+P/CYUY8Bxs8E7WMoZ9tlcMbAY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241219192143-6b3ec007d9bb h1:3oy2tynMOP1QbTC0MsNNAV+Se8M2Bd0A5+x1QHyw+pI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241219192143-6b3ec007d9bb/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= +google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU= +google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ= +google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From b1cb3cf42ff748ac951291100bf3463c52911d81 Mon Sep 17 00:00:00 2001 From: Ragot Geoffrey Date: Thu, 2 Jan 2025 11:30:22 +0100 Subject: [PATCH 66/71] feat(api): add configurable page sizes (#628) --- cmd/serve.go | 33 ++++++-- internal/api/common/pagination.go | 6 ++ internal/api/module.go | 9 +- internal/api/router.go | 15 +++- internal/api/v2/common.go | 17 +++- internal/api/v2/controllers_accounts_list.go | 39 ++++----- .../api/v2/controllers_accounts_list_test.go | 22 ++--- internal/api/v2/controllers_ledgers_list.go | 7 +- .../api/v2/controllers_ledgers_list_test.go | 4 +- internal/api/v2/controllers_logs_list.go | 42 +++++----- internal/api/v2/controllers_logs_list_test.go | 14 ++-- .../api/v2/controllers_transactions_list.go | 47 ++++++----- .../v2/controllers_transactions_list_test.go | 26 +++--- internal/api/v2/controllers_volumes.go | 83 +++++++++---------- internal/api/v2/controllers_volumes_test.go | 12 +-- internal/api/v2/query.go | 8 -- internal/api/v2/routes.go | 22 +++-- pkg/testserver/server.go | 9 +- test/e2e/api_ledgers_list_test.go | 9 +- 19 files changed, 247 insertions(+), 177 deletions(-) create mode 100644 internal/api/common/pagination.go diff --git a/cmd/serve.go b/cmd/serve.go index c4a050763..a1f6431ef 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -2,6 +2,7 @@ package cmd import ( "github.com/formancehq/go-libs/v2/logging" + "github.com/formancehq/ledger/internal/api/common" systemstore "github.com/formancehq/ledger/internal/storage/system" "net/http" "net/http/pprof" @@ -43,6 +44,8 @@ const ( BulkMaxSizeFlag = "bulk-max-size" BulkParallelFlag = "bulk-parallel" NumscriptInterpreterFlag = "experimental-numscript-interpreter" + DefaultPageSizeFlag = "default-page-size" + MaxPageSizeFlag = "max-page-size" ) func NewServeCommand() *cobra.Command { @@ -73,6 +76,16 @@ func NewServeCommand() *cobra.Command { return err } + maxPageSize, err := cmd.Flags().GetUint64(MaxPageSizeFlag) + if err != nil { + return err + } + + defaultPageSize, err := cmd.Flags().GetUint64(DefaultPageSizeFlag) + if err != nil { + return err + } + options := []fx.Option{ fx.NopLogger, otlp.FXModuleFromFlags(cmd), @@ -102,18 +115,22 @@ func NewServeCommand() *cobra.Command { MaxSize: bulkMaxSize, Parallel: bulkParallel, }, + Pagination: common.PaginationConfig{ + MaxPageSize: maxPageSize, + DefaultPageSize: defaultPageSize, + }, }), fx.Decorate(func( params struct { - fx.In + fx.In - Handler chi.Router - HealthController *health.HealthController - Logger logging.Logger + Handler chi.Router + HealthController *health.HealthController + Logger logging.Logger - MeterProvider *metric.MeterProvider `optional:"true"` - Exporter *otlpmetrics.InMemoryExporter `optional:"true"` - }, + MeterProvider *metric.MeterProvider `optional:"true"` + Exporter *otlpmetrics.InMemoryExporter `optional:"true"` + }, ) chi.Router { return assembleFinalRouter( service.IsDebug(cmd), @@ -140,6 +157,8 @@ func NewServeCommand() *cobra.Command { cmd.Flags().Int(BulkMaxSizeFlag, api.DefaultBulkMaxSize, "Bulk max size (default 100)") cmd.Flags().Int(BulkParallelFlag, 10, "Bulk max parallelism") cmd.Flags().Bool(NumscriptInterpreterFlag, false, "Enable experimental numscript rewrite") + cmd.Flags().Uint64(MaxPageSizeFlag, 100, "Max page size") + cmd.Flags().Uint64(DefaultPageSizeFlag, 15, "Default page size") service.AddFlags(cmd.Flags()) bunconnect.AddFlags(cmd.Flags()) diff --git a/internal/api/common/pagination.go b/internal/api/common/pagination.go new file mode 100644 index 000000000..163a29616 --- /dev/null +++ b/internal/api/common/pagination.go @@ -0,0 +1,6 @@ +package common + +type PaginationConfig struct { + MaxPageSize uint64 + DefaultPageSize uint64 +} diff --git a/internal/api/module.go b/internal/api/module.go index d3e54c6b9..d9e6bad7f 100644 --- a/internal/api/module.go +++ b/internal/api/module.go @@ -5,6 +5,7 @@ import ( "github.com/formancehq/go-libs/v2/auth" "github.com/formancehq/go-libs/v2/health" "github.com/formancehq/ledger/internal/api/bulking" + "github.com/formancehq/ledger/internal/api/common" "github.com/formancehq/ledger/internal/controller/system" "github.com/go-chi/chi/v5" "go.opentelemetry.io/otel/trace" @@ -17,9 +18,10 @@ type BulkConfig struct { } type Config struct { - Version string - Debug bool - Bulk BulkConfig + Version string + Debug bool + Bulk BulkConfig + Pagination common.PaginationConfig } func Module(cfg Config) fx.Option { @@ -40,6 +42,7 @@ func Module(cfg Config) fx.Option { bulking.WithParallelism(cfg.Bulk.Parallel), bulking.WithTracer(tracerProvider.Tracer("api.bulking")), )), + WithPaginationConfiguration(cfg.Pagination), ) }), health.Module(), diff --git a/internal/api/router.go b/internal/api/router.go index 940ac8bf4..220339515 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -2,6 +2,7 @@ package api import ( "github.com/formancehq/go-libs/v2/api" + "github.com/formancehq/go-libs/v2/bun/bunpaginate" "github.com/formancehq/ledger/internal/api/bulking" "github.com/formancehq/ledger/internal/controller/system" "go.opentelemetry.io/otel/attribute" @@ -71,6 +72,7 @@ func NewRouter( "application/json": bulking.NewJSONBulkHandlerFactory(routerOptions.bulkMaxSize), "application/vnd.formance.ledger.api.v2.bulk+script-stream": bulking.NewScriptStreamBulkHandlerFactory(), }), + v2.WithPaginationConfig(routerOptions.paginationConfig), ) mux.Handle("/v2*", http.StripPrefix("/v2", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { chi.RouteContext(r.Context()).Reset() @@ -91,7 +93,8 @@ func NewRouter( type routerOptions struct { tracer trace.Tracer bulkMaxSize int - bulkerFactory bulking.BulkerFactory + bulkerFactory bulking.BulkerFactory + paginationConfig common.PaginationConfig } type RouterOption func(ro *routerOptions) @@ -114,9 +117,19 @@ func WithBulkerFactory(bf bulking.BulkerFactory) RouterOption { } } +func WithPaginationConfiguration(paginationConfig common.PaginationConfig) RouterOption { + return func(ro *routerOptions) { + ro.paginationConfig = paginationConfig + } +} + var defaultRouterOptions = []RouterOption{ WithTracer(nooptracer.Tracer{}), WithBulkMaxSize(DefaultBulkMaxSize), + WithPaginationConfiguration(common.PaginationConfig{ + MaxPageSize: bunpaginate.MaxPageSize, + DefaultPageSize: bunpaginate.QueryDefaultPageSize, + }), } const DefaultBulkMaxSize = 100 diff --git a/internal/api/v2/common.go b/internal/api/v2/common.go index 6d7c94185..18b90c97e 100644 --- a/internal/api/v2/common.go +++ b/internal/api/v2/common.go @@ -2,6 +2,7 @@ package v2 import ( . "github.com/formancehq/go-libs/v2/collectionutils" + "github.com/formancehq/ledger/internal/api/common" ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" "io" "net/http" @@ -61,14 +62,18 @@ func getExpand(r *http.Request) []string { ) } -func getOffsetPaginatedQuery[v any](r *http.Request, modifiers ...func(*v) error) (*ledgercontroller.OffsetPaginatedQuery[v], error) { +func getOffsetPaginatedQuery[v any](r *http.Request, paginationConfig common.PaginationConfig, modifiers ...func(*v) error) (*ledgercontroller.OffsetPaginatedQuery[v], error) { return bunpaginate.Extract[ledgercontroller.OffsetPaginatedQuery[v]](r, func() (*ledgercontroller.OffsetPaginatedQuery[v], error) { rq, err := getResourceQuery[v](r, modifiers...) if err != nil { return nil, err } - pageSize, err := bunpaginate.GetPageSize(r, bunpaginate.WithMaxPageSize(MaxPageSize), bunpaginate.WithDefaultPageSize(DefaultPageSize)) + pageSize, err := bunpaginate.GetPageSize( + r, + bunpaginate.WithMaxPageSize(paginationConfig.MaxPageSize), + bunpaginate.WithDefaultPageSize(paginationConfig.DefaultPageSize), + ) if err != nil { return nil, err } @@ -80,14 +85,18 @@ func getOffsetPaginatedQuery[v any](r *http.Request, modifiers ...func(*v) error }) } -func getColumnPaginatedQuery[v any](r *http.Request, defaultPaginationColumn string, order bunpaginate.Order, modifiers ...func(*v) error) (*ledgercontroller.ColumnPaginatedQuery[v], error) { +func getColumnPaginatedQuery[v any](r *http.Request, paginationConfig common.PaginationConfig, defaultPaginationColumn string, order bunpaginate.Order, modifiers ...func(*v) error) (*ledgercontroller.ColumnPaginatedQuery[v], error) { return bunpaginate.Extract[ledgercontroller.ColumnPaginatedQuery[v]](r, func() (*ledgercontroller.ColumnPaginatedQuery[v], error) { rq, err := getResourceQuery[v](r, modifiers...) if err != nil { return nil, err } - pageSize, err := bunpaginate.GetPageSize(r, bunpaginate.WithMaxPageSize(MaxPageSize), bunpaginate.WithDefaultPageSize(DefaultPageSize)) + pageSize, err := bunpaginate.GetPageSize( + r, + bunpaginate.WithMaxPageSize(paginationConfig.MaxPageSize), + bunpaginate.WithDefaultPageSize(paginationConfig.DefaultPageSize), + ) if err != nil { return nil, err } diff --git a/internal/api/v2/controllers_accounts_list.go b/internal/api/v2/controllers_accounts_list.go index 0c1e171fd..190f7f5d1 100644 --- a/internal/api/v2/controllers_accounts_list.go +++ b/internal/api/v2/controllers_accounts_list.go @@ -1,33 +1,34 @@ package v2 import ( - "net/http" - "errors" "github.com/formancehq/go-libs/v2/api" "github.com/formancehq/ledger/internal/api/common" ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" + "net/http" ) -func listAccounts(w http.ResponseWriter, r *http.Request) { - l := common.LedgerFromContext(r.Context()) - - query, err := getOffsetPaginatedQuery[any](r) - if err != nil { - api.BadRequest(w, common.ErrValidation, err) - return - } +func listAccounts(paginationConfig common.PaginationConfig) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + l := common.LedgerFromContext(r.Context()) - cursor, err := l.ListAccounts(r.Context(), *query) - if err != nil { - switch { - case errors.Is(err, ledgercontroller.ErrInvalidQuery{}) || errors.Is(err, ledgercontroller.ErrMissingFeature{}): + query, err := getOffsetPaginatedQuery[any](r, paginationConfig) + if err != nil { api.BadRequest(w, common.ErrValidation, err) - default: - common.HandleCommonErrors(w, r, err) + return + } + + cursor, err := l.ListAccounts(r.Context(), *query) + if err != nil { + switch { + case errors.Is(err, ledgercontroller.ErrInvalidQuery{}) || errors.Is(err, ledgercontroller.ErrMissingFeature{}): + api.BadRequest(w, common.ErrValidation, err) + default: + common.HandleCommonErrors(w, r, err) + } + return } - return - } - api.RenderCursor(w, *cursor) + api.RenderCursor(w, *cursor) + } } diff --git a/internal/api/v2/controllers_accounts_list_test.go b/internal/api/v2/controllers_accounts_list_test.go index 3dff1b1de..ae0252e64 100644 --- a/internal/api/v2/controllers_accounts_list_test.go +++ b/internal/api/v2/controllers_accounts_list_test.go @@ -41,7 +41,7 @@ func TestAccountsList(t *testing.T) { { name: "nominal", expectQuery: ledgercontroller.OffsetPaginatedQuery[any]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Options: ledgercontroller.ResourceQuery[any]{ PIT: &before, Expand: make([]string, 0), @@ -54,7 +54,7 @@ func TestAccountsList(t *testing.T) { body: `{"$match": { "metadata[roles]": "admin" }}`, expectBackendCall: true, expectQuery: ledgercontroller.OffsetPaginatedQuery[any]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Options: ledgercontroller.ResourceQuery[any]{ PIT: &before, Builder: query.Match("metadata[roles]", "admin"), @@ -67,7 +67,7 @@ func TestAccountsList(t *testing.T) { body: `{"$match": { "address": "foo" }}`, expectBackendCall: true, expectQuery: ledgercontroller.OffsetPaginatedQuery[any]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Options: ledgercontroller.ResourceQuery[any]{ PIT: &before, Builder: query.Match("address", "foo"), @@ -80,12 +80,12 @@ func TestAccountsList(t *testing.T) { expectBackendCall: true, queryParams: url.Values{ "cursor": []string{bunpaginate.EncodeCursor(ledgercontroller.OffsetPaginatedQuery[any]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Options: ledgercontroller.ResourceQuery[any]{}, })}, }, expectQuery: ledgercontroller.OffsetPaginatedQuery[any]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Options: ledgercontroller.ResourceQuery[any]{}, }, }, @@ -112,7 +112,7 @@ func TestAccountsList(t *testing.T) { "pageSize": []string{"1000000"}, }, expectQuery: ledgercontroller.OffsetPaginatedQuery[any]{ - PageSize: MaxPageSize, + PageSize: bunpaginate.MaxPageSize, Options: ledgercontroller.ResourceQuery[any]{ PIT: &before, Expand: make([]string, 0), @@ -124,7 +124,7 @@ func TestAccountsList(t *testing.T) { expectBackendCall: true, body: `{"$lt": { "balance[USD/2]": 100 }}`, expectQuery: ledgercontroller.OffsetPaginatedQuery[any]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Options: ledgercontroller.ResourceQuery[any]{ PIT: &before, Builder: query.Lt("balance[USD/2]", float64(100)), @@ -137,7 +137,7 @@ func TestAccountsList(t *testing.T) { expectBackendCall: true, body: `{"$exists": { "metadata": "foo" }}`, expectQuery: ledgercontroller.OffsetPaginatedQuery[any]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Options: ledgercontroller.ResourceQuery[any]{ PIT: &before, Builder: query.Exists("metadata", "foo"), @@ -158,7 +158,7 @@ func TestAccountsList(t *testing.T) { expectBackendCall: true, returnErr: ledgercontroller.ErrInvalidQuery{}, expectQuery: ledgercontroller.OffsetPaginatedQuery[any]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Options: ledgercontroller.ResourceQuery[any]{ PIT: &before, Expand: make([]string, 0), @@ -172,7 +172,7 @@ func TestAccountsList(t *testing.T) { expectBackendCall: true, returnErr: ledgercontroller.ErrMissingFeature{}, expectQuery: ledgercontroller.OffsetPaginatedQuery[any]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Options: ledgercontroller.ResourceQuery[any]{ PIT: &before, Expand: make([]string, 0), @@ -186,7 +186,7 @@ func TestAccountsList(t *testing.T) { expectBackendCall: true, returnErr: errors.New("undefined error"), expectQuery: ledgercontroller.OffsetPaginatedQuery[any]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Options: ledgercontroller.ResourceQuery[any]{ PIT: &before, Expand: make([]string, 0), diff --git a/internal/api/v2/controllers_ledgers_list.go b/internal/api/v2/controllers_ledgers_list.go index 8f2a70343..dd0bd3d30 100644 --- a/internal/api/v2/controllers_ledgers_list.go +++ b/internal/api/v2/controllers_ledgers_list.go @@ -12,11 +12,14 @@ import ( "github.com/formancehq/ledger/internal/controller/system" ) -func listLedgers(b system.Controller) http.HandlerFunc { +func listLedgers(b system.Controller, paginationConfig common.PaginationConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { query, err := bunpaginate.Extract[ledgercontroller.ListLedgersQuery](r, func() (*ledgercontroller.ListLedgersQuery, error) { - pageSize, err := bunpaginate.GetPageSize(r) + pageSize, err := bunpaginate.GetPageSize(r, + bunpaginate.WithMaxPageSize(paginationConfig.MaxPageSize), + bunpaginate.WithDefaultPageSize(paginationConfig.DefaultPageSize), + ) if err != nil { return nil, err } diff --git a/internal/api/v2/controllers_ledgers_list_test.go b/internal/api/v2/controllers_ledgers_list_test.go index b72785af5..4ffbadd27 100644 --- a/internal/api/v2/controllers_ledgers_list_test.go +++ b/internal/api/v2/controllers_ledgers_list_test.go @@ -71,7 +71,7 @@ func TestListLedgers(t *testing.T) { expectedErrorCode: common.ErrValidation, expectBackendCall: true, returnErr: ledgercontroller.ErrInvalidQuery{}, - expectQuery: ledgercontroller.NewListLedgersQuery(DefaultPageSize), + expectQuery: ledgercontroller.NewListLedgersQuery(bunpaginate.QueryDefaultPageSize), }, { name: "with missing feature", @@ -79,7 +79,7 @@ func TestListLedgers(t *testing.T) { expectedErrorCode: common.ErrValidation, expectBackendCall: true, returnErr: ledgercontroller.ErrMissingFeature{}, - expectQuery: ledgercontroller.NewListLedgersQuery(DefaultPageSize), + expectQuery: ledgercontroller.NewListLedgersQuery(bunpaginate.QueryDefaultPageSize), }, } { t.Run(tc.name, func(t *testing.T) { diff --git a/internal/api/v2/controllers_logs_list.go b/internal/api/v2/controllers_logs_list.go index 56c82236a..439b0bdb9 100644 --- a/internal/api/v2/controllers_logs_list.go +++ b/internal/api/v2/controllers_logs_list.go @@ -1,35 +1,35 @@ package v2 import ( - "net/http" - "errors" - ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" - "github.com/formancehq/go-libs/v2/api" "github.com/formancehq/go-libs/v2/bun/bunpaginate" "github.com/formancehq/ledger/internal/api/common" + ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" + "net/http" ) -func listLogs(w http.ResponseWriter, r *http.Request) { - l := common.LedgerFromContext(r.Context()) +func listLogs(paginationConfig common.PaginationConfig) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + l := common.LedgerFromContext(r.Context()) - rq, err := getColumnPaginatedQuery[any](r, "id", bunpaginate.OrderDesc) - if err != nil { - api.BadRequest(w, common.ErrValidation, err) - return - } - - cursor, err := l.ListLogs(r.Context(), *rq) - if err != nil { - switch { - case errors.Is(err, ledgercontroller.ErrInvalidQuery{}): + rq, err := getColumnPaginatedQuery[any](r, paginationConfig, "id", bunpaginate.OrderDesc) + if err != nil { api.BadRequest(w, common.ErrValidation, err) - default: - common.HandleCommonErrors(w, r, err) + return } - return - } - api.RenderCursor(w, *cursor) + cursor, err := l.ListLogs(r.Context(), *rq) + if err != nil { + switch { + case errors.Is(err, ledgercontroller.ErrInvalidQuery{}): + api.BadRequest(w, common.ErrValidation, err) + default: + common.HandleCommonErrors(w, r, err) + } + return + } + + api.RenderCursor(w, *cursor) + } } diff --git a/internal/api/v2/controllers_logs_list_test.go b/internal/api/v2/controllers_logs_list_test.go index 2e931f7f3..340f84b2d 100644 --- a/internal/api/v2/controllers_logs_list_test.go +++ b/internal/api/v2/controllers_logs_list_test.go @@ -43,7 +43,7 @@ func TestGetLogs(t *testing.T) { { name: "nominal", expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Column: "id", Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), Options: ledgercontroller.ResourceQuery[any]{ @@ -56,7 +56,7 @@ func TestGetLogs(t *testing.T) { name: "using start time", body: fmt.Sprintf(`{"$gte": {"date": "%s"}}`, now.Format(time.DateFormat)), expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Column: "id", Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), Options: ledgercontroller.ResourceQuery[any]{ @@ -70,7 +70,7 @@ func TestGetLogs(t *testing.T) { name: "using end time", body: fmt.Sprintf(`{"$lt": {"date": "%s"}}`, now.Format(time.DateFormat)), expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Column: "id", Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), Options: ledgercontroller.ResourceQuery[any]{ @@ -84,13 +84,13 @@ func TestGetLogs(t *testing.T) { name: "using empty cursor", queryParams: url.Values{ "cursor": []string{bunpaginate.EncodeCursor(ledgercontroller.ColumnPaginatedQuery[any]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Column: "id", Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), })}, }, expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Column: "id", Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), }, @@ -122,7 +122,7 @@ func TestGetLogs(t *testing.T) { name: "with invalid query", expectStatusCode: http.StatusBadRequest, expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Column: "id", Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), Options: ledgercontroller.ResourceQuery[any]{ @@ -137,7 +137,7 @@ func TestGetLogs(t *testing.T) { name: "with unexpected error", expectStatusCode: http.StatusInternalServerError, expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Column: "id", Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), Options: ledgercontroller.ResourceQuery[any]{ diff --git a/internal/api/v2/controllers_transactions_list.go b/internal/api/v2/controllers_transactions_list.go index 2705a514e..c11baa859 100644 --- a/internal/api/v2/controllers_transactions_list.go +++ b/internal/api/v2/controllers_transactions_list.go @@ -1,39 +1,40 @@ package v2 import ( - "net/http" - "errors" "github.com/formancehq/go-libs/v2/api" "github.com/formancehq/go-libs/v2/bun/bunpaginate" "github.com/formancehq/ledger/internal/api/common" ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" + "net/http" ) -func listTransactions(w http.ResponseWriter, r *http.Request) { - l := common.LedgerFromContext(r.Context()) +func listTransactions(paginationConfig common.PaginationConfig) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + l := common.LedgerFromContext(r.Context()) - paginationColumn := "id" - if r.URL.Query().Get("order") == "effective" { - paginationColumn = "timestamp" - } - - rq, err := getColumnPaginatedQuery[any](r, paginationColumn, bunpaginate.OrderDesc) - if err != nil { - api.BadRequest(w, common.ErrValidation, err) - return - } + paginationColumn := "id" + if r.URL.Query().Get("order") == "effective" { + paginationColumn = "timestamp" + } - cursor, err := l.ListTransactions(r.Context(), *rq) - if err != nil { - switch { - case errors.Is(err, ledgercontroller.ErrInvalidQuery{}) || errors.Is(err, ledgercontroller.ErrMissingFeature{}): + rq, err := getColumnPaginatedQuery[any](r, paginationConfig, paginationColumn, bunpaginate.OrderDesc) + if err != nil { api.BadRequest(w, common.ErrValidation, err) - default: - common.HandleCommonErrors(w, r, err) + return } - return - } - api.RenderCursor(w, *cursor) + cursor, err := l.ListTransactions(r.Context(), *rq) + if err != nil { + switch { + case errors.Is(err, ledgercontroller.ErrInvalidQuery{}) || errors.Is(err, ledgercontroller.ErrMissingFeature{}): + api.BadRequest(w, common.ErrValidation, err) + default: + common.HandleCommonErrors(w, r, err) + } + return + } + + api.RenderCursor(w, *cursor) + } } diff --git a/internal/api/v2/controllers_transactions_list_test.go b/internal/api/v2/controllers_transactions_list_test.go index c64489bb7..087faf521 100644 --- a/internal/api/v2/controllers_transactions_list_test.go +++ b/internal/api/v2/controllers_transactions_list_test.go @@ -40,7 +40,7 @@ func TestTransactionsList(t *testing.T) { { name: "nominal", expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Column: "id", Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), Options: ledgercontroller.ResourceQuery[any]{ @@ -53,7 +53,7 @@ func TestTransactionsList(t *testing.T) { name: "using metadata", body: `{"$match": {"metadata[roles]": "admin"}}`, expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Column: "id", Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), Options: ledgercontroller.ResourceQuery[any]{ @@ -67,7 +67,7 @@ func TestTransactionsList(t *testing.T) { name: "using startTime", body: fmt.Sprintf(`{"$gte": {"start_time": "%s"}}`, now.Format(time.DateFormat)), expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Column: "id", Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), Options: ledgercontroller.ResourceQuery[any]{ @@ -81,7 +81,7 @@ func TestTransactionsList(t *testing.T) { name: "using endTime", body: fmt.Sprintf(`{"$lte": {"end_time": "%s"}}`, now.Format(time.DateFormat)), expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Column: "id", Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), Options: ledgercontroller.ResourceQuery[any]{ @@ -95,7 +95,7 @@ func TestTransactionsList(t *testing.T) { name: "using account", body: `{"$match": {"account": "xxx"}}`, expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Column: "id", Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), Options: ledgercontroller.ResourceQuery[any]{ @@ -109,7 +109,7 @@ func TestTransactionsList(t *testing.T) { name: "using reference", body: `{"$match": {"reference": "xxx"}}`, expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Column: "id", Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), Options: ledgercontroller.ResourceQuery[any]{ @@ -123,7 +123,7 @@ func TestTransactionsList(t *testing.T) { name: "using destination", body: `{"$match": {"destination": "xxx"}}`, expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Column: "id", Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), Options: ledgercontroller.ResourceQuery[any]{ @@ -137,7 +137,7 @@ func TestTransactionsList(t *testing.T) { name: "using source", body: `{"$match": {"source": "xxx"}}`, expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Column: "id", Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), Options: ledgercontroller.ResourceQuery[any]{ @@ -176,7 +176,7 @@ func TestTransactionsList(t *testing.T) { "pageSize": []string{"1000000"}, }, expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ - PageSize: MaxPageSize, + PageSize: bunpaginate.MaxPageSize, Column: "id", Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), Options: ledgercontroller.ResourceQuery[any]{ @@ -190,7 +190,7 @@ func TestTransactionsList(t *testing.T) { queryParams: url.Values{ "cursor": []string{func() string { return bunpaginate.EncodeCursor(ledgercontroller.ColumnPaginatedQuery[any]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Column: "id", Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), Options: ledgercontroller.ResourceQuery[any]{ @@ -200,7 +200,7 @@ func TestTransactionsList(t *testing.T) { }()}, }, expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Column: "id", Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), Options: ledgercontroller.ResourceQuery[any]{ @@ -212,7 +212,7 @@ func TestTransactionsList(t *testing.T) { name: "using $exists metadata filter", body: `{"$exists": {"metadata": "foo"}}`, expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Column: "id", Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), Options: ledgercontroller.ResourceQuery[any]{ @@ -226,7 +226,7 @@ func TestTransactionsList(t *testing.T) { name: "paginate using effective order", queryParams: map[string][]string{"order": {"effective"}}, expectQuery: ledgercontroller.ColumnPaginatedQuery[any]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Column: "timestamp", Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)), Options: ledgercontroller.ResourceQuery[any]{ diff --git a/internal/api/v2/controllers_volumes.go b/internal/api/v2/controllers_volumes.go index 8c94765cb..e5b443e75 100644 --- a/internal/api/v2/controllers_volumes.go +++ b/internal/api/v2/controllers_volumes.go @@ -1,66 +1,65 @@ package v2 import ( - "net/http" - "strconv" - "errors" - ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" - "github.com/formancehq/go-libs/v2/api" "github.com/formancehq/ledger/internal/api/common" + ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" + "net/http" + "strconv" ) -func readVolumes(w http.ResponseWriter, r *http.Request) { +func readVolumes(paginationConfig common.PaginationConfig) http.HandlerFunc { - l := common.LedgerFromContext(r.Context()) + return func(w http.ResponseWriter, r *http.Request) { + l := common.LedgerFromContext(r.Context()) - rq, err := getOffsetPaginatedQuery[ledgercontroller.GetVolumesOptions](r, func(opts *ledgercontroller.GetVolumesOptions) error { - groupBy := r.URL.Query().Get("groupBy") - if groupBy != "" { - v, err := strconv.ParseInt(groupBy, 10, 64) - if err != nil { - return err + rq, err := getOffsetPaginatedQuery[ledgercontroller.GetVolumesOptions](r, paginationConfig, func(opts *ledgercontroller.GetVolumesOptions) error { + groupBy := r.URL.Query().Get("groupBy") + if groupBy != "" { + v, err := strconv.ParseInt(groupBy, 10, 64) + if err != nil { + return err + } + opts.GroupLvl = int(v) } - opts.GroupLvl = int(v) - } - opts.UseInsertionDate = api.QueryParamBool(r, "insertionDate") + opts.UseInsertionDate = api.QueryParamBool(r, "insertionDate") - return nil - }) - if err != nil { - api.BadRequest(w, common.ErrValidation, err) - return - } - - if r.URL.Query().Get("endTime") != "" { - rq.Options.PIT, err = getDate(r, "endTime") + return nil + }) if err != nil { api.BadRequest(w, common.ErrValidation, err) return } - } - if r.URL.Query().Get("startTime") != "" { - rq.Options.OOT, err = getDate(r, "startTime") - if err != nil { - api.BadRequest(w, common.ErrValidation, err) - return + if r.URL.Query().Get("endTime") != "" { + rq.Options.PIT, err = getDate(r, "endTime") + if err != nil { + api.BadRequest(w, common.ErrValidation, err) + return + } } - } - cursor, err := l.GetVolumesWithBalances(r.Context(), *rq) - if err != nil { - switch { - case errors.Is(err, ledgercontroller.ErrInvalidQuery{}) || errors.Is(err, ledgercontroller.ErrMissingFeature{}): - api.BadRequest(w, common.ErrValidation, err) - default: - common.HandleCommonErrors(w, r, err) + if r.URL.Query().Get("startTime") != "" { + rq.Options.OOT, err = getDate(r, "startTime") + if err != nil { + api.BadRequest(w, common.ErrValidation, err) + return + } } - return - } - api.RenderCursor(w, *cursor) + cursor, err := l.GetVolumesWithBalances(r.Context(), *rq) + if err != nil { + switch { + case errors.Is(err, ledgercontroller.ErrInvalidQuery{}) || errors.Is(err, ledgercontroller.ErrMissingFeature{}): + api.BadRequest(w, common.ErrValidation, err) + default: + common.HandleCommonErrors(w, r, err) + } + return + } + api.RenderCursor(w, *cursor) + } } diff --git a/internal/api/v2/controllers_volumes_test.go b/internal/api/v2/controllers_volumes_test.go index b570bae9b..0b9a06593 100644 --- a/internal/api/v2/controllers_volumes_test.go +++ b/internal/api/v2/controllers_volumes_test.go @@ -41,7 +41,7 @@ func TestGetVolumes(t *testing.T) { { name: "basic", expectQuery: ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ PIT: &before, Expand: make([]string, 0), @@ -52,7 +52,7 @@ func TestGetVolumes(t *testing.T) { name: "using metadata", body: `{"$match": { "metadata[roles]": "admin" }}`, expectQuery: ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ PIT: &before, Builder: query.Match("metadata[roles]", "admin"), @@ -64,7 +64,7 @@ func TestGetVolumes(t *testing.T) { name: "using account", body: `{"$match": { "account": "foo" }}`, expectQuery: ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ PIT: &before, Builder: query.Match("account", "foo"), @@ -85,7 +85,7 @@ func TestGetVolumes(t *testing.T) { "groupBy": []string{"3"}, }, expectQuery: ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ PIT: &before, Expand: make([]string, 0), @@ -99,7 +99,7 @@ func TestGetVolumes(t *testing.T) { name: "using Exists metadata filter", body: `{"$exists": { "metadata": "foo" }}`, expectQuery: ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ PIT: &before, Builder: query.Exists("metadata", "foo"), @@ -111,7 +111,7 @@ func TestGetVolumes(t *testing.T) { name: "using balance filter", body: `{"$gte": { "balance[EUR]": 50 }}`, expectQuery: ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{ - PageSize: DefaultPageSize, + PageSize: bunpaginate.QueryDefaultPageSize, Options: ledgercontroller.ResourceQuery[ledgercontroller.GetVolumesOptions]{ PIT: &before, Builder: query.Gte("balance[EUR]", float64(50)), diff --git a/internal/api/v2/query.go b/internal/api/v2/query.go index 22cc3b868..913231aaf 100644 --- a/internal/api/v2/query.go +++ b/internal/api/v2/query.go @@ -6,14 +6,6 @@ import ( "github.com/formancehq/ledger/internal/controller/ledger" "github.com/formancehq/go-libs/v2/api" - "github.com/formancehq/go-libs/v2/bun/bunpaginate" -) - -const ( - MaxPageSize = bunpaginate.MaxPageSize - DefaultPageSize = bunpaginate.QueryDefaultPageSize - - QueryKeyCursor = "cursor" ) func getCommandParameters[INPUT any](r *http.Request, input INPUT) ledger.Parameters[INPUT] { diff --git a/internal/api/v2/routes.go b/internal/api/v2/routes.go index 5cd635fbc..22b93e4ac 100644 --- a/internal/api/v2/routes.go +++ b/internal/api/v2/routes.go @@ -1,6 +1,7 @@ package v2 import ( + "github.com/formancehq/go-libs/v2/bun/bunpaginate" "github.com/formancehq/ledger/internal/api/bulking" nooptracer "go.opentelemetry.io/otel/trace/noop" "net/http" @@ -35,7 +36,7 @@ func NewRouter( router.Use(auth.Middleware(authenticator)) router.Use(service.OTLPMiddleware("ledger", debug)) - router.Get("/", listLedgers(systemController)) + router.Get("/", listLedgers(systemController, routerOptions.paginationConfig)) router.Route("/{ledger}", func(router chi.Router) { router.Use(func(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -61,19 +62,19 @@ func NewRouter( // LedgerController router.Get("/_info", getLedgerInfo) router.Get("/stats", readStats) - router.Get("/logs", listLogs) + router.Get("/logs", listLogs(routerOptions.paginationConfig)) router.Post("/logs/import", importLogs) router.Post("/logs/export", exportLogs) // AccountController - router.Get("/accounts", listAccounts) + router.Get("/accounts", listAccounts(routerOptions.paginationConfig)) router.Head("/accounts", countAccounts) router.Get("/accounts/{address}", readAccount) router.Post("/accounts/{address}/metadata", addAccountMetadata) router.Delete("/accounts/{address}/metadata/{key}", deleteAccountMetadata) // TransactionController - router.Get("/transactions", listTransactions) + router.Get("/transactions", listTransactions(routerOptions.paginationConfig)) router.Head("/transactions", countTransactions) router.Post("/transactions", createTransaction) @@ -85,7 +86,7 @@ func NewRouter( router.Get("/aggregate/balances", readBalancesAggregated) - router.Get("/volumes", readVolumes) + router.Get("/volumes", readVolumes(routerOptions.paginationConfig)) }) }) }) @@ -98,6 +99,7 @@ type routerOptions struct { middlewares []func(http.Handler) http.Handler bulkerFactory bulking.BulkerFactory bulkHandlerFactories map[string]bulking.HandlerFactory + paginationConfig common.PaginationConfig } type RouterOption func(ro *routerOptions) @@ -126,6 +128,12 @@ func WithBulkerFactory(bulkerFactory bulking.BulkerFactory) RouterOption { } } +func WithPaginationConfig(paginationConfig common.PaginationConfig) RouterOption { + return func(ro *routerOptions) { + ro.paginationConfig = paginationConfig + } +} + var defaultRouterOptions = []RouterOption{ WithTracer(nooptracer.Tracer{}), WithBulkerFactory(bulking.NewDefaultBulkerFactory()), @@ -133,4 +141,8 @@ var defaultRouterOptions = []RouterOption{ "application/json": bulking.NewJSONBulkHandlerFactory(100), "application/vnd.formance.ledger.api.v2.bulk+script-stream": bulking.NewScriptStreamBulkHandlerFactory(), }), + WithPaginationConfig(common.PaginationConfig{ + DefaultPageSize: bunpaginate.QueryDefaultPageSize, + MaxPageSize: bunpaginate.MaxPageSize, + }), } diff --git a/pkg/testserver/server.go b/pkg/testserver/server.go index d6dae85ab..d35cd7441 100644 --- a/pkg/testserver/server.go +++ b/pkg/testserver/server.go @@ -48,6 +48,8 @@ type Configuration struct { DisableAutoUpgrade bool BulkMaxSize int ExperimentalNumscriptRewrite bool + MaxPageSize uint64 + DefaultPageSize uint64 } type Logger interface { @@ -177,7 +179,12 @@ func (s *Server) Start() error { args = append(args, "--"+otlp.OtelServiceNameFlag, s.configuration.OTLPConfig.BaseConfig.ServiceName) } } - + if s.configuration.MaxPageSize != 0 { + args = append(args, "--"+cmd.MaxPageSizeFlag, fmt.Sprint(s.configuration.MaxPageSize)) + } + if s.configuration.DefaultPageSize != 0 { + args = append(args, "--"+cmd.DefaultPageSizeFlag, fmt.Sprint(s.configuration.DefaultPageSize)) + } if s.configuration.Debug { args = append(args, "--"+service.DebugFlag) } diff --git a/test/e2e/api_ledgers_list_test.go b/test/e2e/api_ledgers_list_test.go index 34a473118..1bd3deb34 100644 --- a/test/e2e/api_ledgers_list_test.go +++ b/test/e2e/api_ledgers_list_test.go @@ -5,6 +5,7 @@ package test_suite import ( "fmt" "github.com/formancehq/go-libs/v2/logging" + "github.com/formancehq/go-libs/v2/pointer" "github.com/formancehq/ledger/pkg/client/models/operations" . "github.com/formancehq/ledger/pkg/testserver" . "github.com/onsi/ginkgo/v2" @@ -23,6 +24,8 @@ var _ = Context("Ledger engine tests", func() { Output: GinkgoWriter, Debug: debug, NatsURL: natsServer.GetValue().ClientURL(), + MaxPageSize: 5, + DefaultPageSize: 5, } }) @@ -36,9 +39,11 @@ var _ = Context("Ledger engine tests", func() { } }) It("should be listable", func() { - ledgers, err := ListLedgers(ctx, testServer.GetValue(), operations.V2ListLedgersRequest{}) + ledgers, err := ListLedgers(ctx, testServer.GetValue(), operations.V2ListLedgersRequest{ + PageSize: pointer.For(int64(100)), + }) Expect(err).To(BeNil()) - Expect(ledgers.Data).To(HaveLen(10)) + Expect(ledgers.Data).To(HaveLen(5)) }) }) }) From 9073243f84ac49f36330b6ab1d23e7927ce2215d Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Thu, 2 Jan 2025 16:47:11 +0100 Subject: [PATCH 67/71] feat: Use Nix & Justfile (#619) * feat: Add Flake * ci: Rework tests - Add ginkgo to the list of dependencies in flake.nix - Remove unnecessary CACHE directives in Earthfile - Simplify 'compile' section in Earthfile - Simplify 'tests' section in Earthfile - Simplify 'lint' section in Earthfile - Simplify 'pre-commit' section in Earthfile - Simplify 'pre-commit-nix' section in Earthfile - Simplify 'tidy' section in Earthfile - Simplify 'export-docs-events' section in Earthfile feat/nix * ci: Rework config * ci: Rework config Reworked config to use 'nix develop' with 'earthly' command. Added 'pre-commit-nix' hook. Removed unnecessary flags and secrets. (feat/nix) * ci: Rework config Reworked config to use 'nix develop' with 'earthly' command. Added 'pre-commit-nix' hook. Removed unnecessary flags and secrets. (feat/nix) * ci: Rework config Reworked config to use 'nix develop' with 'earthly' command. Added 'pre-commit-nix' hook. Removed unnecessary flags and secrets. (feat/nix) * ci: Rework config Reworked config to use 'nix develop' with 'earthly' command. Added 'pre-commit-nix' hook. Removed unnecessary flags and secrets. (feat/nix) * ci: Rework config Reworked config to use 'nix develop' with 'earthly' command. Added 'pre-commit-nix' hook. Removed unnecessary flags and secrets. (feat/nix) * ci: Rework config Reworked config to use 'nix develop' with 'earthly' command. Added 'pre-commit-nix' hook. Removed unnecessary flags and secrets. (feat/nix) * ci: Rework config Reworked config to use 'nix develop' with 'earthly' command. Added 'pre-commit-nix' hook. Removed unnecessary flags and secrets. (feat/nix) * ci: Rework config Reworked config to use 'nix develop' with 'earthly' command. Added 'pre-commit-nix' hook. Removed unnecessary flags and secrets. (feat/nix) * feat(ledger): optimize release script in Earthfile (issue #123) * Add .envrc and update .gitignore to track it Added a new .envrc file with configuration for direnv and updated .gitignore to include .envrc while still ignoring .env files. This ensures proper local environment setup using direnv without accidentally committing sensitive .env files. * Add missing `isgomock` struct to mock test files This change ensures all mock test files include the `isgomock` struct, standardizing the structure of generated mocks. This addition aligns with gomock's conventions and facilitates internal consistency across mock definitions. * chore: migrate from Earthly to Just for build and workflow automation This change replaces Earthly with Just by removing Earthfiles and introducing a Justfile. Benefits include streamlining the dependency management, simplifying workflow automation, and improving build process readability. Build and CI workflows (e.g., pre-commit, tests, linting) have been adapted to utilize Just commands, ensuring consistency across development processes. * build(workflow): update CI to simplify release commands Replaced complex earthly-based release command with a `just` target in the CI workflow for better maintainability. Added new `release` commands to the Justfile for local, CI, and standard releases. This streamlines the release process and enhances clarity. * refactor: simplify and streamline build and lint processes - Removed unused 'group' annotation in Justfile to enhance clarity. - Consolidated COPY commands in Earthfile for better maintainability. - Updated 'compile' stage in Earthfile to use 'sources' for consistency. These changes reduce redundancy, improve readability, and ensure less complexity in maintenance. * fix(justfile): reorder pre-commit tasks for consistent execution flow Reordered tasks in the pre-commit command to ensure `generate` and `earthly` execute before `tidy`, `lint`, and `export-docs-events`. This improves maintainability and ensures prerequisites are met. * chore(ci): update release workflow to use nix and just for releases Replaced `earthly` with `nix` and `just` in the release workflow. This improves build consistency and leverages flakes for reproducibility. * test(mock): add `isgomock` struct to mock types Added `isgomock` struct to mock types in tests to enhance clarity and identification of GoMock-generated code structure. --- .envrc | 2 + .github/workflows/main.yml | 26 +--- .github/workflows/releases.yml | 11 +- .gitignore | 2 + Earthfile | 119 +----------------- Justfile | 47 +++++++ deployments/pulumi/Earthfile | 41 ------ flake.lock | 90 +++++++++++++ flake.nix | 48 +++++++ .../bulking/mocks_ledger_controller_test.go | 3 + .../common/mocks_ledger_controller_test.go | 3 + .../common/mocks_system_controller_test.go | 3 + .../api/v1/mocks_ledger_controller_test.go | 3 + .../api/v1/mocks_system_controller_test.go | 3 + .../api/v2/mocks_ledger_controller_test.go | 3 + .../api/v2/mocks_system_controller_test.go | 3 + .../ledger/controller_generated_test.go | 3 + ...too_many_client_handling_generated_test.go | 3 + .../ledger/listener_generated_test.go | 3 + .../ledger/numscript_parser_generated_test.go | 3 + .../numscript_runtime_generated_test.go | 3 + .../controller/ledger/store_generated_test.go | 5 + .../storage/driver/buckets_generated_test.go | 4 + .../storage/driver/ledger_generated_test.go | 3 + .../storage/driver/system_generated_test.go | 3 + tools/generator/Earthfile | 38 +----- 26 files changed, 252 insertions(+), 223 deletions(-) create mode 100644 .envrc create mode 100644 Justfile delete mode 100644 deployments/pulumi/Earthfile create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.envrc b/.envrc new file mode 100644 index 000000000..17be98bda --- /dev/null +++ b/.envrc @@ -0,0 +1,2 @@ +use flake --impure +dotenv \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ea60d8b64..a851a0edc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -35,11 +35,8 @@ jobs: with: token: ${{ secrets.NUMARY_GITHUB_TOKEN }} - run: > - earthly - --allow-privileged - --secret SPEAKEASY_API_KEY=$SPEAKEASY_API_KEY - ${{ contains(github.event.pull_request.labels.*.name, 'no-cache') && '--no-cache' || '' }} - +pre-commit + /nix/var/nix/profiles/default/bin/nix --extra-experimental-features "nix-command" --extra-experimental-features "flakes" + develop --impure --command just pre-commit env: SPEAKEASY_API_KEY: ${{ secrets.SPEAKEASY_API_KEY }} - name: Get changed files @@ -65,12 +62,8 @@ jobs: with: token: ${{ secrets.NUMARY_GITHUB_TOKEN }} - run: > - earthly - --no-output - --allow-privileged - --secret SPEAKEASY_API_KEY=$SPEAKEASY_API_KEY - ${{ contains(github.event.pull_request.labels.*.name, 'no-cache') && '--no-cache' || '' }} - +tests --coverage=true + /nix/var/nix/profiles/default/bin/nix --extra-experimental-features "nix-command" --extra-experimental-features "flakes" + develop --impure --command just tests env: SPEAKEASY_API_KEY: ${{ secrets.SPEAKEASY_API_KEY }} - name: Upload coverage reports to Codecov with GitHub Action @@ -102,15 +95,8 @@ jobs: username: "NumaryBot" password: ${{ secrets.NUMARY_GITHUB_TOKEN }} - run: > - earthly - --no-output - --allow-privileged - --secret SPEAKEASY_API_KEY=$SPEAKEASY_API_KEY - --secret GITHUB_TOKEN=$GITHUB_TOKEN - --secret FURY_TOKEN=$FURY_TOKEN - --secret GORELEASER_KEY=$GORELEASER_KEY - ${{ contains(github.event.pull_request.labels.*.name, 'no-cache') && '--no-cache' || '' }} - +release --mode=ci + /nix/var/nix/profiles/default/bin/nix --extra-experimental-features "nix-command" --extra-experimental-features "flakes" + develop --impure --command just release-ci env: GITHUB_TOKEN: ${{ secrets.NUMARY_GITHUB_TOKEN }} SPEAKEASY_API_KEY: ${{ secrets.SPEAKEASY_API_KEY }} diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index 1bd3d9646..cf195e8ff 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -29,15 +29,8 @@ jobs: username: "NumaryBot" password: ${{ secrets.NUMARY_GITHUB_TOKEN }} - run: > - earthly - --no-output - --allow-privileged - --secret SPEAKEASY_API_KEY=$SPEAKEASY_API_KEY - --secret GITHUB_TOKEN=$GITHUB_TOKEN - --secret FURY_TOKEN=$FURY_TOKEN - --secret GORELEASER_KEY=$GORELEASER_KEY - ${{ contains(github.event.pull_request.labels.*.name, 'no-cache') && '--no-cache' || '' }} - +release --mode=release + /nix/var/nix/profiles/default/bin/nix --extra-experimental-features "nix-command" --extra-experimental-features "flakes" + develop --impure --command just release env: GITHUB_TOKEN: ${{ secrets.NUMARY_GITHUB_TOKEN }} SPEAKEASY_API_KEY: ${{ secrets.SPEAKEASY_API_KEY }} diff --git a/.gitignore b/.gitignore index 69d9b77e5..8b084264d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ dist vendor worktrees dumps +.env +!.envrc \ No newline at end of file diff --git a/Earthfile b/Earthfile index 4f36f999c..f9f723380 100644 --- a/Earthfile +++ b/Earthfile @@ -3,12 +3,6 @@ PROJECT FormanceHQ/ledger IMPORT github.com/formancehq/earthly:tags/v0.19.0 AS core -FROM core+base-image - -CACHE --sharing=shared --id go-mod-cache /go/pkg/mod -CACHE --sharing=shared --id golangci-cache /root/.cache/golangci-lint -CACHE --sharing=shared --id go-cache /root/.cache/go-build - postgres: FROM postgres:15-alpine @@ -26,27 +20,8 @@ sources: COPY main.go . SAVE ARTIFACT /src -generate: - FROM core+builder-image - CACHE --id go-mod-cache /go/pkg/mod - CACHE --id go-cache /root/.cache/go-build - RUN apk update && apk add openjdk11 - RUN go install go.uber.org/mock/mockgen@v0.4.0 - RUN go install github.com/princjef/gomarkdoc/cmd/gomarkdoc@latest - COPY (+tidy/*) /src/ - COPY --dir (+sources/src/*) /src/ - - WORKDIR /src - RUN go generate ./... - SAVE ARTIFACT internal AS LOCAL internal - SAVE ARTIFACT pkg AS LOCAL pkg - SAVE ARTIFACT cmd AS LOCAL cmd - compile: - FROM +sources - CACHE --id go-mod-cache /go/pkg/mod - CACHE --id go-cache /root/.cache/go-build - WORKDIR /src + LOCALLY ARG VERSION=latest RUN go build -o main -ldflags="-X ${GIT_PATH}/cmd.Version=${VERSION} \ -X ${GIT_PATH}/cmd.BuildDate=$(date +%s) \ @@ -62,50 +37,6 @@ build-image: ARG tag=latest DO --pass-args core+SAVE_IMAGE --COMPONENT=ledger --REPOSITORY=${REPOSITORY} --TAG=$tag -tests: - FROM +tidy - CACHE --id go-mod-cache /go/pkg/mod - CACHE --id go-cache /root/.cache/go-build - RUN go install github.com/onsi/ginkgo/v2/ginkgo@latest - RUN apk add gcc musl-dev - - COPY --dir --pass-args (+generate/*) . - - ARG includeIntegrationTests="true" - ARG coverage="" - ARG debug=false - ARG additionalArgs="" - - ENV DEBUG=$debug - ENV CGO_ENABLED=1 # required for -race - - LET goFlags="-race" - IF [ "$coverage" = "true" ] - SET goFlags="$goFlags -covermode=atomic" - SET goFlags="$goFlags -coverpkg=github.com/formancehq/ledger/internal/..." - SET goFlags="$goFlags,github.com/formancehq/ledger/pkg/events/..." - SET goFlags="$goFlags,github.com/formancehq/ledger/pkg/accounts/..." - SET goFlags="$goFlags,github.com/formancehq/ledger/pkg/assets/..." - SET goFlags="$goFlags,github.com/formancehq/ledger/cmd/..." - SET goFlags="$goFlags -coverprofile coverage.txt" - END - - IF [ "$includeIntegrationTests" = "true" ] - SET goFlags="$goFlags -tags it" - WITH DOCKER --load=postgres:15-alpine=+postgres - RUN go test $goFlags $additionalArgs ./... - END - ELSE - RUN go test $goFlags $additionalArgs ./... - END - IF [ "$coverage" = "true" ] - # as special case, exclude files suffixed by debug.go - # toremovelater: exclude machine code as it will be updated soon - RUN cat coverage.txt | grep -v debug.go | grep -v "/machine/" > coverage2.txt - RUN mv coverage2.txt coverage.txt - SAVE ARTIFACT coverage.txt AS LOCAL coverage.txt - END - deploy: COPY (+sources/*) /src LET tag=$(tar cf - /src | sha1sum | awk '{print $1}') @@ -118,32 +49,10 @@ deploy: deploy-staging: BUILD --pass-args core+deploy-staging -lint: - #todo: get config from core - FROM +tidy - CACHE --id go-mod-cache /go/pkg/mod - CACHE --id go-cache /root/.cache/go-build - CACHE --id golangci-cache /root/.cache/golangci-lint - - RUN golangci-lint run --fix --build-tags it --timeout 5m - - SAVE ARTIFACT cmd AS LOCAL cmd - SAVE ARTIFACT internal AS LOCAL internal - SAVE ARTIFACT pkg AS LOCAL pkg - SAVE ARTIFACT test AS LOCAL test - SAVE ARTIFACT main.go AS LOCAL main.go - pre-commit: - BUILD +tidy - BUILD +lint BUILD +openapi BUILD +openapi-markdown - BUILD +generate BUILD +generate-client - BUILD +export-docs-events - - BUILD ./tools/*+pre-commit - BUILD ./deployments/*+pre-commit openapi: FROM node:20-alpine @@ -162,23 +71,6 @@ openapi-markdown: RUN widdershins openapi.yaml -o README.md --search false --language_tabs 'http:HTTP' --summary --omitHeader SAVE ARTIFACT README.md AS LOCAL docs/api/README.md -tidy: - FROM +sources - CACHE --id go-mod-cache /go/pkg/mod - CACHE --id go-cache /root/.cache/go-build - WORKDIR /src - COPY --dir test . - RUN go mod tidy - - SAVE ARTIFACT go.mod AS LOCAL go.mod - SAVE ARTIFACT go.sum AS LOCAL go.sum - -release: - FROM core+builder-image - ARG mode=local - COPY --dir . /src - DO core+GORELEASER --mode=$mode - generate-client: FROM node:20-alpine RUN apk update && apk add yq jq @@ -215,12 +107,3 @@ export-database-schema: END SAVE ARTIFACT docs/database/_system/diagrams AS LOCAL docs/database/_system/diagrams SAVE ARTIFACT docs/database/_default/diagrams AS LOCAL docs/database/_default/diagrams - -export-docs-events: - FROM +tidy - CACHE --id go-mod-cache /go/pkg/mod - CACHE --id go-cache /root/.cache/go-build - - RUN go run . docs events --write-dir docs/events - - SAVE ARTIFACT docs/events AS LOCAL docs/events \ No newline at end of file diff --git a/Justfile b/Justfile new file mode 100644 index 000000000..8157d63df --- /dev/null +++ b/Justfile @@ -0,0 +1,47 @@ +set dotenv-load + +default: + @just --list + +pre-commit: generate earthly tidy lint export-docs-events + +earthly: + @earthly --no-output +pre-commit + +lint: + @golangci-lint run --fix --build-tags it --timeout 5m + @cd {{justfile_directory()}}/tools/generator && golangci-lint run --fix --build-tags it --timeout 5m + @cd {{justfile_directory()}}/deployments/pulumi && golangci-lint run --fix --build-tags it --timeout 5m + +tidy: + @go mod tidy + @cd {{justfile_directory()}}/tools/generator && go mod tidy + @cd {{justfile_directory()}}/deployments/pulumi && go mod tidy + +generate: + @go generate ./... + +export-docs-events: + @go run . docs events --write-dir docs/events + +tests: + @go test -race -covermode=atomic \ + -coverpkg=github.com/formancehq/ledger/internal/... \ + -coverpkg=github.com/formancehq/ledger/pkg/events/... \ + -coverpkg=github.com/formancehq/ledger/pkg/accounts/... \ + -coverpkg=github.com/formancehq/ledger/pkg/assets/... \ + -coverpkg=github.com/formancehq/ledger/cmd/... \ + -coverprofile coverage.txt \ + -tags it \ + ./... + @cat coverage.txt | grep -v debug.go | grep -v "/machine/" > coverage2.txt + @mv coverage2.txt coverage.txt + +release-local: + @goreleaser release --nightly --skip=publish --clean + +release-ci: + @goreleaser release --nightly --clean + +release: + @goreleaser release --clean diff --git a/deployments/pulumi/Earthfile b/deployments/pulumi/Earthfile deleted file mode 100644 index 6c750b957..000000000 --- a/deployments/pulumi/Earthfile +++ /dev/null @@ -1,41 +0,0 @@ -VERSION 0.8 -PROJECT FormanceHQ/ledger - -IMPORT github.com/formancehq/earthly:tags/v0.19.0 AS core - -FROM core+base-image - -CACHE --sharing=shared --id go-mod-cache /go/pkg/mod -CACHE --sharing=shared --id go-cache /root/.cache/go-build -CACHE --sharing=shared --id golangci-cache /root/.cache/golangci-lint - -sources: - FROM core+builder-image - WORKDIR /src - COPY *.go go.* Pulumi.yaml . - COPY --dir pkg . - SAVE ARTIFACT /src - -tidy: - FROM +sources - CACHE --id go-mod-cache /go/pkg/mod - CACHE --id go-cache /root/.cache/go-build - RUN go mod tidy - - SAVE ARTIFACT go.mod AS LOCAL go.mod - SAVE ARTIFACT go.sum AS LOCAL go.sum - -lint: - FROM +tidy - CACHE --id go-mod-cache /go/pkg/mod - CACHE --id go-cache /root/.cache/go-build - CACHE --id golangci-cache /root/.cache/golangci-lint - - RUN golangci-lint run --fix --build-tags it --timeout 5m - - SAVE ARTIFACT main.go AS LOCAL main.go - SAVE ARTIFACT pkg AS LOCAL pkg - -pre-commit: - BUILD +tidy - BUILD +lint \ No newline at end of file diff --git a/flake.lock b/flake.lock new file mode 100644 index 000000000..46f2e8f85 --- /dev/null +++ b/flake.lock @@ -0,0 +1,90 @@ +{ + "nodes": { + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "nur", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1733312601, + "narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1734119587, + "narHash": "sha256-AKU6qqskl0yf2+JdRdD0cfxX4b9x3KKV5RqA6wijmPM=", + "rev": "3566ab7246670a43abd2ffa913cc62dad9cdf7d5", + "revCount": 721821, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.721821%2Brev-3566ab7246670a43abd2ffa913cc62dad9cdf7d5/0193cb18-1103-723d-8c38-29b3e808b002/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/NixOS/nixpkgs/0.1.%2A.tar.gz" + } + }, + "nur": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": [ + "nixpkgs" + ], + "treefmt-nix": "treefmt-nix" + }, + "locked": { + "lastModified": 1734461636, + "narHash": "sha256-ibv85eiHoqXTBdJ/iUCfq8odhhRozdZteQi1ZEO9g0o=", + "owner": "nix-community", + "repo": "NUR", + "rev": "d4aba0b137b5a6b444c3a4f4052f2b3f29097c4b", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "NUR", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "nur": "nur" + } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "nur", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1733222881, + "narHash": "sha256-JIPcz1PrpXUCbaccEnrcUS8jjEb/1vJbZz5KkobyFdM=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "49717b5af6f80172275d47a418c9719a31a78b53", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000..c7f4bd5cf --- /dev/null +++ b/flake.nix @@ -0,0 +1,48 @@ +{ + description = "A Nix-flake-based Go 1.23 development environment"; + + inputs = { + nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.1.*.tar.gz"; + nur = { + url = "github:nix-community/NUR"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { self, nixpkgs, nur }: + let + goVersion = 23; # Change this to update the whole stack + + supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; + forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f { + pkgs = import nixpkgs { + inherit system; + overlays = [ self.overlays.default nur.overlays.default ]; + config.allowUnfree = true; + }; + }); + in + { + overlays.default = final: prev: { + go = final."go_1_${toString goVersion}"; + }; + + devShells = forEachSupportedSystem ({ pkgs }: { + default = pkgs.mkShell { + packages = with pkgs; [ + go + gotools + golangci-lint + ginkgo + yq + jq + pkgs.nur.repos.goreleaser.goreleaser-pro + mockgen + gomarkdoc + jdk11 + just + ]; + }; + }); + }; +} \ No newline at end of file diff --git a/internal/api/bulking/mocks_ledger_controller_test.go b/internal/api/bulking/mocks_ledger_controller_test.go index 2cede2100..cbf90fc33 100644 --- a/internal/api/bulking/mocks_ledger_controller_test.go +++ b/internal/api/bulking/mocks_ledger_controller_test.go @@ -3,6 +3,8 @@ // Generated by this command: // // mockgen -write_source_comment=false -write_package_comment=false -source ../../controller/ledger/controller.go -destination mocks_ledger_controller_test.go -package bulking --mock_names Controller=LedgerController . Controller +// + package bulking import ( @@ -21,6 +23,7 @@ import ( type LedgerController struct { ctrl *gomock.Controller recorder *LedgerControllerMockRecorder + isgomock struct{} } // LedgerControllerMockRecorder is the mock recorder for LedgerController. diff --git a/internal/api/common/mocks_ledger_controller_test.go b/internal/api/common/mocks_ledger_controller_test.go index c263cfa1f..01e775d3f 100644 --- a/internal/api/common/mocks_ledger_controller_test.go +++ b/internal/api/common/mocks_ledger_controller_test.go @@ -3,6 +3,8 @@ // Generated by this command: // // mockgen -write_source_comment=false -write_package_comment=false -source ../../controller/ledger/controller.go -destination mocks_ledger_controller_test.go -package common --mock_names Controller=LedgerController . Controller +// + package common import ( @@ -21,6 +23,7 @@ import ( type LedgerController struct { ctrl *gomock.Controller recorder *LedgerControllerMockRecorder + isgomock struct{} } // LedgerControllerMockRecorder is the mock recorder for LedgerController. diff --git a/internal/api/common/mocks_system_controller_test.go b/internal/api/common/mocks_system_controller_test.go index b0fbeaea8..0c85dc246 100644 --- a/internal/api/common/mocks_system_controller_test.go +++ b/internal/api/common/mocks_system_controller_test.go @@ -3,6 +3,8 @@ // Generated by this command: // // mockgen -write_source_comment=false -write_package_comment=false -source ../../controller/system/controller.go -destination mocks_system_controller_test.go -package common --mock_names Controller=SystemController . Controller +// + package common import ( @@ -19,6 +21,7 @@ import ( type SystemController struct { ctrl *gomock.Controller recorder *SystemControllerMockRecorder + isgomock struct{} } // SystemControllerMockRecorder is the mock recorder for SystemController. diff --git a/internal/api/v1/mocks_ledger_controller_test.go b/internal/api/v1/mocks_ledger_controller_test.go index f89439826..2f3a686e1 100644 --- a/internal/api/v1/mocks_ledger_controller_test.go +++ b/internal/api/v1/mocks_ledger_controller_test.go @@ -3,6 +3,8 @@ // Generated by this command: // // mockgen -write_source_comment=false -write_package_comment=false -source ../../controller/ledger/controller.go -destination mocks_ledger_controller_test.go -package v1 --mock_names Controller=LedgerController . Controller +// + package v1 import ( @@ -21,6 +23,7 @@ import ( type LedgerController struct { ctrl *gomock.Controller recorder *LedgerControllerMockRecorder + isgomock struct{} } // LedgerControllerMockRecorder is the mock recorder for LedgerController. diff --git a/internal/api/v1/mocks_system_controller_test.go b/internal/api/v1/mocks_system_controller_test.go index 1ad57614e..f3f19b232 100644 --- a/internal/api/v1/mocks_system_controller_test.go +++ b/internal/api/v1/mocks_system_controller_test.go @@ -3,6 +3,8 @@ // Generated by this command: // // mockgen -write_source_comment=false -write_package_comment=false -source ../../controller/system/controller.go -destination mocks_system_controller_test.go -package v1 --mock_names Controller=SystemController . Controller +// + package v1 import ( @@ -19,6 +21,7 @@ import ( type SystemController struct { ctrl *gomock.Controller recorder *SystemControllerMockRecorder + isgomock struct{} } // SystemControllerMockRecorder is the mock recorder for SystemController. diff --git a/internal/api/v2/mocks_ledger_controller_test.go b/internal/api/v2/mocks_ledger_controller_test.go index 2cbbfee4a..a0d043ca4 100644 --- a/internal/api/v2/mocks_ledger_controller_test.go +++ b/internal/api/v2/mocks_ledger_controller_test.go @@ -3,6 +3,8 @@ // Generated by this command: // // mockgen -write_source_comment=false -write_package_comment=false -source ../../controller/ledger/controller.go -destination mocks_ledger_controller_test.go -package v2 --mock_names Controller=LedgerController . Controller +// + package v2 import ( @@ -21,6 +23,7 @@ import ( type LedgerController struct { ctrl *gomock.Controller recorder *LedgerControllerMockRecorder + isgomock struct{} } // LedgerControllerMockRecorder is the mock recorder for LedgerController. diff --git a/internal/api/v2/mocks_system_controller_test.go b/internal/api/v2/mocks_system_controller_test.go index 45d1eaaa5..c4d8c215a 100644 --- a/internal/api/v2/mocks_system_controller_test.go +++ b/internal/api/v2/mocks_system_controller_test.go @@ -3,6 +3,8 @@ // Generated by this command: // // mockgen -write_source_comment=false -write_package_comment=false -source ../../controller/system/controller.go -destination mocks_system_controller_test.go -package v2 --mock_names Controller=SystemController . Controller +// + package v2 import ( @@ -19,6 +21,7 @@ import ( type SystemController struct { ctrl *gomock.Controller recorder *SystemControllerMockRecorder + isgomock struct{} } // SystemControllerMockRecorder is the mock recorder for SystemController. diff --git a/internal/controller/ledger/controller_generated_test.go b/internal/controller/ledger/controller_generated_test.go index 7e6601231..1495b8c74 100644 --- a/internal/controller/ledger/controller_generated_test.go +++ b/internal/controller/ledger/controller_generated_test.go @@ -3,6 +3,8 @@ // Generated by this command: // // mockgen -write_source_comment=false -write_package_comment=false -source controller.go -destination controller_generated_test.go -package ledger . Controller +// + package ledger import ( @@ -20,6 +22,7 @@ import ( type MockController struct { ctrl *gomock.Controller recorder *MockControllerMockRecorder + isgomock struct{} } // MockControllerMockRecorder is the mock recorder for MockController. diff --git a/internal/controller/ledger/controller_with_too_many_client_handling_generated_test.go b/internal/controller/ledger/controller_with_too_many_client_handling_generated_test.go index 2f0c421cb..9f397752e 100644 --- a/internal/controller/ledger/controller_with_too_many_client_handling_generated_test.go +++ b/internal/controller/ledger/controller_with_too_many_client_handling_generated_test.go @@ -3,6 +3,8 @@ // Generated by this command: // // 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 +// + package ledger import ( @@ -16,6 +18,7 @@ import ( type MockDelayCalculator struct { ctrl *gomock.Controller recorder *MockDelayCalculatorMockRecorder + isgomock struct{} } // MockDelayCalculatorMockRecorder is the mock recorder for MockDelayCalculator. diff --git a/internal/controller/ledger/listener_generated_test.go b/internal/controller/ledger/listener_generated_test.go index 44df6a6a7..e0e7e584c 100644 --- a/internal/controller/ledger/listener_generated_test.go +++ b/internal/controller/ledger/listener_generated_test.go @@ -3,6 +3,8 @@ // Generated by this command: // // mockgen -write_source_comment=false -write_package_comment=false -source listener.go -destination listener_generated_test.go -package ledger . Listener +// + package ledger import ( @@ -18,6 +20,7 @@ import ( type MockListener struct { ctrl *gomock.Controller recorder *MockListenerMockRecorder + isgomock struct{} } // MockListenerMockRecorder is the mock recorder for MockListener. diff --git a/internal/controller/ledger/numscript_parser_generated_test.go b/internal/controller/ledger/numscript_parser_generated_test.go index f319d367d..5219a92e2 100644 --- a/internal/controller/ledger/numscript_parser_generated_test.go +++ b/internal/controller/ledger/numscript_parser_generated_test.go @@ -3,6 +3,8 @@ // Generated by this command: // // mockgen -write_source_comment=false -write_package_comment=false -source numscript_parser.go -destination numscript_parser_generated_test.go -package ledger . NumscriptParser +// + package ledger import ( @@ -15,6 +17,7 @@ import ( type MockNumscriptParser struct { ctrl *gomock.Controller recorder *MockNumscriptParserMockRecorder + isgomock struct{} } // MockNumscriptParserMockRecorder is the mock recorder for MockNumscriptParser. diff --git a/internal/controller/ledger/numscript_runtime_generated_test.go b/internal/controller/ledger/numscript_runtime_generated_test.go index 254a78556..8a6343843 100644 --- a/internal/controller/ledger/numscript_runtime_generated_test.go +++ b/internal/controller/ledger/numscript_runtime_generated_test.go @@ -3,6 +3,8 @@ // Generated by this command: // // mockgen -write_source_comment=false -write_package_comment=false -source numscript_runtime.go -destination numscript_runtime_generated_test.go -package ledger . NumscriptRuntime +// + package ledger import ( @@ -16,6 +18,7 @@ import ( type MockNumscriptRuntime struct { ctrl *gomock.Controller recorder *MockNumscriptRuntimeMockRecorder + isgomock struct{} } // MockNumscriptRuntimeMockRecorder is the mock recorder for MockNumscriptRuntime. diff --git a/internal/controller/ledger/store_generated_test.go b/internal/controller/ledger/store_generated_test.go index 7a677d58e..accb45087 100644 --- a/internal/controller/ledger/store_generated_test.go +++ b/internal/controller/ledger/store_generated_test.go @@ -3,6 +3,8 @@ // Generated by this command: // // mockgen -write_source_comment=false -write_package_comment=false -source store.go -destination store_generated_test.go -package ledger . PaginatedResource +// + package ledger import ( @@ -23,6 +25,7 @@ import ( type MockStore struct { ctrl *gomock.Controller recorder *MockStoreMockRecorder + isgomock struct{} } // MockStoreMockRecorder is the mock recorder for MockStore. @@ -370,6 +373,7 @@ func (mr *MockStoreMockRecorder) Volumes() *gomock.Call { type MockResource[ResourceType any, OptionsType any] struct { ctrl *gomock.Controller recorder *MockResourceMockRecorder[ResourceType, OptionsType] + isgomock struct{} } // MockResourceMockRecorder is the mock recorder for MockResource. @@ -423,6 +427,7 @@ func (mr *MockResourceMockRecorder[ResourceType, OptionsType]) GetOne(ctx, query type MockPaginatedResource[ResourceType any, OptionsType any, PaginationQueryType PaginatedQuery[OptionsType]] struct { ctrl *gomock.Controller recorder *MockPaginatedResourceMockRecorder[ResourceType, OptionsType, PaginationQueryType] + isgomock struct{} } // MockPaginatedResourceMockRecorder is the mock recorder for MockPaginatedResource. diff --git a/internal/storage/driver/buckets_generated_test.go b/internal/storage/driver/buckets_generated_test.go index b71780813..61635f327 100644 --- a/internal/storage/driver/buckets_generated_test.go +++ b/internal/storage/driver/buckets_generated_test.go @@ -3,6 +3,8 @@ // Generated by this command: // // mockgen -write_source_comment=false -write_package_comment=false -source ../bucket/bucket.go -destination buckets_generated_test.go -package driver --mock_names Factory=BucketFactory . Factory +// + package driver import ( @@ -19,6 +21,7 @@ import ( type MockBucket struct { ctrl *gomock.Controller recorder *MockBucketMockRecorder + isgomock struct{} } // MockBucketMockRecorder is the mock recorder for MockBucket. @@ -135,6 +138,7 @@ func (mr *MockBucketMockRecorder) Migrate(ctx any, opts ...any) *gomock.Call { type BucketFactory struct { ctrl *gomock.Controller recorder *BucketFactoryMockRecorder + isgomock struct{} } // BucketFactoryMockRecorder is the mock recorder for BucketFactory. diff --git a/internal/storage/driver/ledger_generated_test.go b/internal/storage/driver/ledger_generated_test.go index fb2f8a6ab..b940e41b3 100644 --- a/internal/storage/driver/ledger_generated_test.go +++ b/internal/storage/driver/ledger_generated_test.go @@ -3,6 +3,8 @@ // Generated by this command: // // mockgen -write_source_comment=false -write_package_comment=false -source ../ledger/factory.go -destination ledger_generated_test.go -package driver --mock_names Factory=LedgerStoreFactory . Factory +// + package driver import ( @@ -18,6 +20,7 @@ import ( type LedgerStoreFactory struct { ctrl *gomock.Controller recorder *LedgerStoreFactoryMockRecorder + isgomock struct{} } // LedgerStoreFactoryMockRecorder is the mock recorder for LedgerStoreFactory. diff --git a/internal/storage/driver/system_generated_test.go b/internal/storage/driver/system_generated_test.go index d6afce573..6ce339a3f 100644 --- a/internal/storage/driver/system_generated_test.go +++ b/internal/storage/driver/system_generated_test.go @@ -3,6 +3,8 @@ // Generated by this command: // // mockgen -write_source_comment=false -write_package_comment=false -source ../system/store.go -destination system_generated_test.go -package driver --mock_names Store=SystemStore . Store +// + package driver import ( @@ -21,6 +23,7 @@ import ( type SystemStore struct { ctrl *gomock.Controller recorder *SystemStoreMockRecorder + isgomock struct{} } // SystemStoreMockRecorder is the mock recorder for SystemStore. diff --git a/tools/generator/Earthfile b/tools/generator/Earthfile index 974a4983b..d70de7213 100644 --- a/tools/generator/Earthfile +++ b/tools/generator/Earthfile @@ -10,31 +10,14 @@ CACHE --sharing=shared --id go-cache /root/.cache/go-build sources: FROM core+builder-image - - COPY ../..+lint/pkg /src/pkg - COPY ../..+lint/internal /src/internal - COPY ../..+lint/cmd /src/cmd - COPY ../..+lint/*.go /src/ - COPY ../..+tidy/go.mod /src/ - COPY ../..+tidy/go.sum /src/ - + COPY (../..+sources/*) /src WORKDIR /src/tools/generator COPY --dir cmd examples . COPY go.* *.go . - SAVE ARTIFACT /src -tidy: - FROM +sources - CACHE --id go-mod-cache /go/pkg/mod - CACHE --id go-cache /root/.cache/go-build - RUN go mod tidy - - SAVE ARTIFACT go.mod AS LOCAL go.mod - SAVE ARTIFACT go.sum AS LOCAL go.sum - compile: - FROM +tidy + FROM +sources CACHE --id go-mod-cache /go/pkg/mod CACHE --id go-cache /root/.cache/go-build RUN go build -o main @@ -47,19 +30,4 @@ build-image: COPY examples /examples ARG REPOSITORY=ghcr.io ARG tag=latest - DO --pass-args core+SAVE_IMAGE --COMPONENT=ledger-generator --REPOSITORY=${REPOSITORY} --TAG=$tag - -lint: - FROM +tidy - CACHE --id go-mod-cache /go/pkg/mod - CACHE --id go-cache /root/.cache/go-build - CACHE --id golangci-cache /root/.cache/golangci-lint - - RUN golangci-lint run --fix --build-tags it --timeout 5m - - SAVE ARTIFACT cmd AS LOCAL cmd - SAVE ARTIFACT main.go AS LOCAL main.go - -pre-commit: - BUILD +tidy - BUILD +lint \ No newline at end of file + DO --pass-args core+SAVE_IMAGE --COMPONENT=ledger-generator --REPOSITORY=${REPOSITORY} --TAG=$tag \ No newline at end of file From 98707ec486b3077b54442c8086aa803a9ba0a713 Mon Sep 17 00:00:00 2001 From: Ragot Geoffrey Date: Thu, 2 Jan 2025 16:50:24 +0100 Subject: [PATCH 68/71] fix: release workflow (#635) --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a851a0edc..b1dd2d3b2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,7 +4,7 @@ on: push: branches: - main - - releases/* + - release/* pull_request: types: [ assigned, opened, synchronize, reopened, labeled ] From 7e4611b1d3c51672c3a47ea455551225236cf086 Mon Sep 17 00:00:00 2001 From: Ragot Geoffrey Date: Fri, 3 Jan 2025 11:57:11 +0100 Subject: [PATCH 69/71] chore: clean compat layer from v2.1 (#518) --- go.mod | 2 +- internal/README.md | 2 +- internal/ledger.go | 2 +- internal/storage/bucket/default_bucket.go | 10 +- .../notes.yaml | 1 + .../26-accounts-recreate-unique-index/up.sql | 9 + .../migrations/27-clean-database/notes.yaml | 1 + .../migrations/27-clean-database/up.sql | 124 ++++ internal/storage/driver/adapters.go | 10 +- internal/storage/driver/driver.go | 4 - internal/storage/ledger/adapters.go | 51 ++ internal/storage/ledger/balances.go | 67 +- internal/storage/ledger/balances_test.go | 77 +- internal/storage/ledger/legacy/accounts.go | 198 ----- .../storage/ledger/legacy/accounts_test.go | 338 --------- internal/storage/ledger/legacy/adapters.go | 128 ---- internal/storage/ledger/legacy/balances.go | 146 ---- .../storage/ledger/legacy/balances_test.go | 175 ----- internal/storage/ledger/legacy/debug.go | 42 -- internal/storage/ledger/legacy/errors.go | 30 - internal/storage/ledger/legacy/logs.go | 50 -- internal/storage/ledger/legacy/logs_test.go | 62 -- internal/storage/ledger/legacy/main_test.go | 79 -- internal/storage/ledger/legacy/queries.go | 159 ----- internal/storage/ledger/legacy/store.go | 43 -- .../storage/ledger/legacy/transactions.go | 206 ------ .../ledger/legacy/transactions_test.go | 281 -------- internal/storage/ledger/legacy/utils.go | 185 ----- internal/storage/ledger/legacy/volumes.go | 188 ----- .../storage/ledger/legacy/volumes_test.go | 675 ------------------ .../ledger/resource_aggregated_balances.go | 2 +- internal/storage/system/migrations.go | 12 + 32 files changed, 250 insertions(+), 3109 deletions(-) create mode 100644 internal/storage/bucket/migrations/26-accounts-recreate-unique-index/notes.yaml create mode 100644 internal/storage/bucket/migrations/26-accounts-recreate-unique-index/up.sql create mode 100644 internal/storage/bucket/migrations/27-clean-database/notes.yaml create mode 100644 internal/storage/bucket/migrations/27-clean-database/up.sql create mode 100644 internal/storage/ledger/adapters.go delete mode 100644 internal/storage/ledger/legacy/accounts.go delete mode 100644 internal/storage/ledger/legacy/accounts_test.go delete mode 100644 internal/storage/ledger/legacy/adapters.go delete mode 100644 internal/storage/ledger/legacy/balances.go delete mode 100644 internal/storage/ledger/legacy/balances_test.go delete mode 100644 internal/storage/ledger/legacy/debug.go delete mode 100644 internal/storage/ledger/legacy/errors.go delete mode 100644 internal/storage/ledger/legacy/logs.go delete mode 100644 internal/storage/ledger/legacy/logs_test.go delete mode 100644 internal/storage/ledger/legacy/main_test.go delete mode 100644 internal/storage/ledger/legacy/queries.go delete mode 100644 internal/storage/ledger/legacy/store.go delete mode 100644 internal/storage/ledger/legacy/transactions.go delete mode 100644 internal/storage/ledger/legacy/transactions_test.go delete mode 100644 internal/storage/ledger/legacy/utils.go delete mode 100644 internal/storage/ledger/legacy/volumes.go delete mode 100644 internal/storage/ledger/legacy/volumes_test.go diff --git a/go.mod b/go.mod index fece61599..cc9c8f740 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,6 @@ require ( github.com/onsi/gomega v1.35.1 github.com/ory/dockertest/v3 v3.11.0 github.com/pborman/uuid v1.2.1 - github.com/pkg/errors v0.9.1 github.com/shomali11/xsql v0.0.0-20190608141458-bf76292144df github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 @@ -55,6 +54,7 @@ require gopkg.in/yaml.v3 v3.0.1 // indirect require ( github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/jackc/pgxlisten v0.0.0-20241106001234-1d6f6656415c // indirect + github.com/pkg/errors v0.9.1 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect ) diff --git a/internal/README.md b/internal/README.md index 42282d7d3..616e988bf 100644 --- a/internal/README.md +++ b/internal/README.md @@ -245,7 +245,7 @@ type BalancesByAssetsByAccounts map[string]BalancesByAssets ```go type Configuration struct { Bucket string `json:"bucket" bun:"bucket,type:varchar(255)"` - Metadata metadata.Metadata `json:"metadata" bun:"metadata,type:jsonb"` + Metadata metadata.Metadata `json:"metadata" bun:"metadata,type:jsonb,nullzero"` Features features.FeatureSet `json:"features" bun:"features,type:jsonb"` } ``` diff --git a/internal/ledger.go b/internal/ledger.go index f50708fd2..1fcb0b7e2 100644 --- a/internal/ledger.go +++ b/internal/ledger.go @@ -84,7 +84,7 @@ var ( type Configuration struct { Bucket string `json:"bucket" bun:"bucket,type:varchar(255)"` - Metadata metadata.Metadata `json:"metadata" bun:"metadata,type:jsonb"` + Metadata metadata.Metadata `json:"metadata" bun:"metadata,type:jsonb,nullzero"` Features features.FeatureSet `json:"features" bun:"features,type:jsonb"` } diff --git a/internal/storage/bucket/default_bucket.go b/internal/storage/bucket/default_bucket.go index 13e3ca8af..2267a2ca8 100644 --- a/internal/storage/bucket/default_bucket.go +++ b/internal/storage/bucket/default_bucket.go @@ -15,11 +15,11 @@ import ( ) // stateless version (+1 regarding directory name, as migrations start from 1 in the lib) -const MinimalSchemaVersion = 12 +const MinimalSchemaVersion = 27 type DefaultBucket struct { - name string - db *bun.DB + name string + db *bun.DB tracer trace.Tracer } @@ -81,8 +81,8 @@ func (b *DefaultBucket) AddLedger(ctx context.Context, l ledger.Ledger) error { func NewDefault(db *bun.DB, tracer trace.Tracer, name string) *DefaultBucket { return &DefaultBucket{ - db: db, - name: name, + db: db, + name: name, tracer: tracer, } } diff --git a/internal/storage/bucket/migrations/26-accounts-recreate-unique-index/notes.yaml b/internal/storage/bucket/migrations/26-accounts-recreate-unique-index/notes.yaml new file mode 100644 index 000000000..4b7c24021 --- /dev/null +++ b/internal/storage/bucket/migrations/26-accounts-recreate-unique-index/notes.yaml @@ -0,0 +1 @@ +name: Recreate accounts unique index diff --git a/internal/storage/bucket/migrations/26-accounts-recreate-unique-index/up.sql b/internal/storage/bucket/migrations/26-accounts-recreate-unique-index/up.sql new file mode 100644 index 000000000..3b8ad12bd --- /dev/null +++ b/internal/storage/bucket/migrations/26-accounts-recreate-unique-index/up.sql @@ -0,0 +1,9 @@ +-- There is already a covering index on accounts table (including seq column). +-- As we will remove the seq column in next migration, we have to create a new index without it (PG will remove it automatically in background). +-- Also, we create the index concurrently to avoid locking the table. +-- And, as there is already an index on this table, the index creation should not fail. +-- +-- We create this index in a dedicated as, as the doc mentions it (https://www.postgresql.org/docs/current/protocol-flow.html#PROTOCOL-FLOW-MULTI-STATEMENT) +-- multi statements queries are automatically wrapped inside transaction block, and it's forbidden +-- to create index concurrently inside a transaction block. +create unique index concurrently accounts_ledger2 on "{{.Schema}}".accounts (ledger, address) \ No newline at end of file diff --git a/internal/storage/bucket/migrations/27-clean-database/notes.yaml b/internal/storage/bucket/migrations/27-clean-database/notes.yaml new file mode 100644 index 000000000..759af9421 --- /dev/null +++ b/internal/storage/bucket/migrations/27-clean-database/notes.yaml @@ -0,0 +1 @@ +name: Clean not used columns in database diff --git a/internal/storage/bucket/migrations/27-clean-database/up.sql b/internal/storage/bucket/migrations/27-clean-database/up.sql new file mode 100644 index 000000000..d120fb02d --- /dev/null +++ b/internal/storage/bucket/migrations/27-clean-database/up.sql @@ -0,0 +1,124 @@ +set search_path = '{{.Schema}}'; + +-- Clean all useless function/aggregates/indexes inherited from stateful version. +drop aggregate aggregate_objects(jsonb); +drop aggregate first(anyelement); + +drop function array_distinct(anyarray); +drop function insert_posting(_transaction_seq bigint, _ledger character varying, _insertion_date timestamp without time zone, _effective_date timestamp without time zone, posting jsonb, _account_metadata jsonb); +drop function upsert_account(_ledger character varying, _address character varying, _metadata jsonb, _date timestamp without time zone, _first_usage timestamp without time zone); +drop function get_latest_move_for_account_and_asset(_ledger character varying, _account_address character varying, _asset character varying, _before timestamp without time zone); +drop function update_transaction_metadata(_ledger character varying, _id numeric, _metadata jsonb, _date timestamp without time zone); +drop function delete_account_metadata(_ledger character varying, _address character varying, _key character varying, _date timestamp without time zone); +drop function delete_transaction_metadata(_ledger character varying, _id numeric, _key character varying, _date timestamp without time zone); +drop function balance_from_volumes(v volumes); +drop function get_all_account_volumes(_ledger character varying, _account character varying, _before timestamp without time zone); +drop function first_agg(anyelement, anyelement); +drop function volumes_to_jsonb(v volumes_with_asset); +drop function get_account_aggregated_effective_volumes(_ledger character varying, _account_address character varying, _before timestamp without time zone); +drop function handle_log(); +drop function get_account_aggregated_volumes(_ledger character varying, _account_address character varying, _before timestamp without time zone); +drop function get_aggregated_volumes_for_transaction(_ledger character varying, tx numeric); +drop function insert_move(_transactions_seq bigint, _ledger character varying, _insertion_date timestamp without time zone, _effective_date timestamp without time zone, _account_address character varying, _asset character varying, _amount numeric, _is_source boolean, _account_exists boolean); +drop function get_all_assets(_ledger character varying); +drop function insert_transaction(_ledger character varying, data jsonb, _date timestamp without time zone, _account_metadata jsonb); +drop function get_all_account_effective_volumes(_ledger character varying, _account character varying, _before timestamp without time zone); +drop function get_account_balance(_ledger character varying, _account character varying, _asset character varying, _before timestamp without time zone); +drop function get_aggregated_effective_volumes_for_transaction(_ledger character varying, tx numeric); +drop function aggregate_ledger_volumes(_ledger character varying, _before timestamp without time zone, _accounts character varying[], _assets character varying[] ); +drop function get_transaction(_ledger character varying, _id numeric, _before timestamp without time zone); +drop function revert_transaction(_ledger character varying, _id numeric, _date timestamp without time zone); + +drop index transactions_sources_arrays; +drop index transactions_destinations_arrays; +drop index transactions_sources; +drop index transactions_destinations; + +-- We will remove some triggers writing these columns (set_compat_xxx) later in this file. +-- When these triggers will be removed, there is a little moment where the columns will not be filled and constraints +-- still checked by the database. +-- So, we drop the not null constraint before removing the triggers. +-- Once the triggers removed, we will be able to drop the columns. +alter table moves +alter column transactions_seq drop not null, +alter column accounts_seq drop not null, +alter column accounts_address_array drop not null; + +alter table transactions_metadata +alter column transactions_seq drop not null; + +alter table accounts_metadata +alter column accounts_seq drop not null; + +-- Now, the columns are nullable, we can drop the trigger +drop trigger set_compat_on_move on moves; +drop trigger set_compat_on_accounts_metadata on accounts_metadata; +drop trigger set_compat_on_transactions_metadata on transactions_metadata; +drop function set_compat_on_move(); +drop function set_compat_on_accounts_metadata(); +drop function set_compat_on_transactions_metadata(); + +-- Finally remove the columns +alter table moves +drop column transactions_seq, +drop column accounts_seq, +drop column accounts_address_array; + +alter table transactions_metadata +drop column transactions_seq; + +alter table accounts_metadata +drop column accounts_seq; + +alter table transactions +drop column seq; + +alter table accounts +drop column seq; + +-- rename index create in previous migration, as the drop of the column seq of accounts table has automatically dropped the index accounts_ledger +alter index accounts_ledger2 +rename to accounts_ledger; + +create or replace function set_log_hash() + returns trigger + security definer + language plpgsql +as +$$ +declare + previousHash bytea; + marshalledAsJSON varchar; +begin + select hash into previousHash + from logs + where ledger = new.ledger + order by id desc + limit 1; + + -- select only fields participating in the hash on the backend and format json representation the same way + select '{' || + '"type":"' || new.type || '",' || + '"data":' || encode(new.memento, 'escape') || ',' || + '"date":"' || (to_json(new.date::timestamp)#>>'{}') || 'Z",' || + '"idempotencyKey":"' || coalesce(new.idempotency_key, '') || '",' || + '"id":0,' || + '"hash":null' || + '}' into marshalledAsJSON; + + new.hash = ( + select public.digest( + case + when previousHash is null + then marshalledAsJSON::bytea + else '"' || encode(previousHash::bytea, 'base64')::bytea || E'"\n' || convert_to(marshalledAsJSON, 'LATIN1')::bytea + end || E'\n', 'sha256'::text + ) + ); + + return new; +end; +$$ set search_path from current; + +alter table logs +drop column seq; \ No newline at end of file diff --git a/internal/storage/driver/adapters.go b/internal/storage/driver/adapters.go index 304d9b7f6..de9ef7341 100644 --- a/internal/storage/driver/adapters.go +++ b/internal/storage/driver/adapters.go @@ -2,8 +2,7 @@ package driver import ( "context" - "fmt" - ledgerstore "github.com/formancehq/ledger/internal/storage/ledger/legacy" + ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" ledger "github.com/formancehq/ledger/internal" ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" @@ -20,12 +19,7 @@ func (d *DefaultStorageDriverAdapter) OpenLedger(ctx context.Context, name strin return nil, nil, err } - isUpToDate, err := store.GetBucket().IsUpToDate(ctx) - if err != nil { - return nil, nil, fmt.Errorf("checking if bucket is up to date: %w", err) - } - - return ledgerstore.NewDefaultStoreAdapter(isUpToDate, store), l, nil + return ledgerstore.NewDefaultStoreAdapter(store), l, nil } func (d *DefaultStorageDriverAdapter) CreateLedger(ctx context.Context, l *ledger.Ledger) error { diff --git a/internal/storage/driver/driver.go b/internal/storage/driver/driver.go index 497bb406e..ab45d4dac 100644 --- a/internal/storage/driver/driver.go +++ b/internal/storage/driver/driver.go @@ -39,10 +39,6 @@ type Driver struct { func (d *Driver) CreateLedger(ctx context.Context, l *ledger.Ledger) (*ledgerstore.Store, error) { - if l.Metadata == nil { - l.Metadata = metadata.Metadata{} - } - b := d.bucketFactory.Create(l.Bucket) isInitialized, err := b.IsInitialized(ctx) if err != nil { diff --git a/internal/storage/ledger/adapters.go b/internal/storage/ledger/adapters.go new file mode 100644 index 000000000..17505d9a4 --- /dev/null +++ b/internal/storage/ledger/adapters.go @@ -0,0 +1,51 @@ +package ledger + +import ( + "context" + "database/sql" + ledger "github.com/formancehq/ledger/internal" + ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" +) + +type TX struct { + *Store +} + +type DefaultStoreAdapter struct { + *Store +} + +func (d *DefaultStoreAdapter) IsUpToDate(ctx context.Context) (bool, error) { + return d.HasMinimalVersion(ctx) +} + +func (d *DefaultStoreAdapter) BeginTX(ctx context.Context, opts *sql.TxOptions) (ledgercontroller.Store, error) { + store, err := d.Store.BeginTX(ctx, opts) + if err != nil { + return nil, err + } + + return &DefaultStoreAdapter{ + Store: store, + }, nil +} + +func (d *DefaultStoreAdapter) Commit() error { + return d.Store.Commit() +} + +func (d *DefaultStoreAdapter) Rollback() error { + return d.Store.Rollback() +} + +func (d *DefaultStoreAdapter) AggregatedBalances() ledgercontroller.Resource[ledger.AggregatedVolumes, ledgercontroller.GetAggregatedVolumesOptions] { + return d.AggregatedVolumes() +} + +func NewDefaultStoreAdapter(store *Store) *DefaultStoreAdapter { + return &DefaultStoreAdapter{ + Store: store, + } +} + +var _ ledgercontroller.Store = (*DefaultStoreAdapter)(nil) diff --git a/internal/storage/ledger/balances.go b/internal/storage/ledger/balances.go index d702f7135..6af3e6929 100644 --- a/internal/storage/ledger/balances.go +++ b/internal/storage/ledger/balances.go @@ -49,62 +49,25 @@ func (store *Store) GetBalances(ctx context.Context, query ledgercontroller.Bala } } - // Try to insert volumes using last move (to keep compat with previous version) or 0 values. - // This way, if the account has a 0 balance at this point, it will be locked as any other accounts. - // If the complete sql transaction fails, the account volumes will not be inserted. - selectMoves := store.db.NewSelect(). - ModelTableExpr(store.GetPrefixedRelationName("moves")). - DistinctOn("accounts_address, asset"). - Column("accounts_address", "asset"). - ColumnExpr("first_value(post_commit_volumes) over (partition by accounts_address, asset order by seq desc) as post_commit_volumes"). - ColumnExpr("first_value(ledger) over (partition by accounts_address, asset order by seq desc) as ledger"). - Where("("+strings.Join(conditions, ") OR (")+")", args...) - - zeroValuesAndMoves := store.db.NewSelect(). - TableExpr("(?) data", selectMoves). - Column("ledger", "accounts_address", "asset"). - ColumnExpr("(post_commit_volumes).inputs as input"). - ColumnExpr("(post_commit_volumes).outputs as output"). - UnionAll( - store.db.NewSelect(). - TableExpr( - "(?) data", - store.db.NewSelect().NewValues(&accountsVolumes), - ). - Column("*"), - ) - - zeroValueOrMoves := store.db.NewSelect(). - TableExpr("(?) data", zeroValuesAndMoves). - Column("ledger", "accounts_address", "asset", "input", "output"). - DistinctOn("ledger, accounts_address, asset") - - insertDefaultValue := store.db.NewInsert(). - TableExpr(store.GetPrefixedRelationName("accounts_volumes")). - TableExpr("(" + zeroValueOrMoves.String() + ") data"). - On("conflict (ledger, accounts_address, asset) do nothing"). - Returning("ledger, accounts_address, asset, input, output") - - selectExistingValues := store.db.NewSelect(). + err := store.db.NewSelect(). + With( + "ins", + // Try to insert volumes with 0 values. + // This way, if the account has a 0 balance at this point, it will be locked as any other accounts. + // It the complete sql transaction fail, the account volumes will not be inserted. + store.db.NewInsert(). + Model(&accountsVolumes). + ModelTableExpr(store.GetPrefixedRelationName("accounts_volumes")). + On("conflict do nothing"), + ). + Model(&accountsVolumes). ModelTableExpr(store.GetPrefixedRelationName("accounts_volumes")). - Column("ledger", "accounts_address", "asset", "input", "output"). + Column("accounts_address", "asset", "input", "output"). Where("("+strings.Join(conditions, ") OR (")+")", args...). For("update"). // notes(gfyrag): Keep order, it ensures consistent locking order and limit deadlocks - Order("accounts_address", "asset") - - finalQuery := store.db.NewSelect(). - With("inserted", insertDefaultValue). - With("existing", selectExistingValues). - ModelTableExpr( - "(?) accounts_volumes", - store.db.NewSelect(). - ModelTableExpr("inserted"). - UnionAll(store.db.NewSelect().ModelTableExpr("existing")), - ). - Model(&accountsVolumes) - - err := finalQuery.Scan(ctx) + Order("accounts_address", "asset"). + Scan(ctx) if err != nil { return nil, postgres.ResolveError(err) } diff --git a/internal/storage/ledger/balances_test.go b/internal/storage/ledger/balances_test.go index 3713294e9..8f299d5a3 100644 --- a/internal/storage/ledger/balances_test.go +++ b/internal/storage/ledger/balances_test.go @@ -4,7 +4,6 @@ package ledger_test import ( "database/sql" - "github.com/formancehq/go-libs/v2/bun/bunpaginate" "math/big" "testing" @@ -44,6 +43,32 @@ func TestBalancesGet(t *testing.T) { }) require.NoError(t, err) + t.Run("get balances of not existing account should create an empty row", func(t *testing.T) { + t.Parallel() + + balances, err := store.GetBalances(ctx, ledgercontroller.BalanceQuery{ + "orders:1234": []string{"USD"}, + }) + require.NoError(t, err) + require.Len(t, balances, 1) + require.NotNil(t, balances["orders:1234"]) + require.Len(t, balances["orders:1234"], 1) + require.Equal(t, big.NewInt(0), balances["orders:1234"]["USD"]) + + volumes := make([]*ledger.AccountsVolumes, 0) + + err = store.GetDB().NewSelect(). + Model(&volumes). + ModelTableExpr(store.GetPrefixedRelationName("accounts_volumes")). + Where("accounts_address = ?", "orders:1234"). + Scan(ctx) + require.NoError(t, err) + require.Len(t, volumes, 1) + require.Equal(t, "USD", volumes[0].Asset) + require.Equal(t, big.NewInt(0), volumes[0].Input) + require.Equal(t, big.NewInt(0), volumes[0].Output) + }) + t.Run("check concurrent access on same balance", func(t *testing.T) { t.Parallel() @@ -130,56 +155,6 @@ func TestBalancesGet(t *testing.T) { require.NoError(t, err) require.Equal(t, 2, count) }) - - t.Run("with balance from move", func(t *testing.T) { - t.Parallel() - - tx := ledger.NewTransaction().WithPostings( - ledger.NewPosting("world", "bank", "USD", big.NewInt(100)), - ) - err := store.InsertTransaction(ctx, &tx) - require.NoError(t, err) - - bankAccount := ledger.Account{ - Address: "bank", - FirstUsage: tx.InsertedAt, - InsertionDate: tx.InsertedAt, - UpdatedAt: tx.InsertedAt, - } - err = store.UpsertAccounts(ctx, &bankAccount) - require.NoError(t, err) - - err = store.InsertMoves(ctx, &ledger.Move{ - TransactionID: tx.ID, - IsSource: false, - Account: "bank", - Amount: (*bunpaginate.BigInt)(big.NewInt(100)), - Asset: "USD", - InsertionDate: tx.InsertedAt, - EffectiveDate: tx.InsertedAt, - PostCommitVolumes: pointer.For(ledger.NewVolumesInt64(100, 0)), - }) - require.NoError(t, err) - - balances, err := store.GetBalances(ctx, ledgercontroller.BalanceQuery{ - "bank": {"USD"}, - }) - require.NoError(t, err) - - require.NotNil(t, balances["bank"]) - RequireEqual(t, big.NewInt(100), balances["bank"]["USD"]) - - // Check a new line has been inserted into accounts_volumes table - volumes := &ledger.AccountsVolumes{} - err = store.GetDB().NewSelect(). - ModelTableExpr(store.GetPrefixedRelationName("accounts_volumes")). - Where("accounts_address = ? and ledger = ?", "bank", store.GetLedger().Name). - Scan(ctx, volumes) - require.NoError(t, err) - - RequireEqual(t, big.NewInt(100), volumes.Input) - RequireEqual(t, big.NewInt(0), volumes.Output) - }) } func TestBalancesAggregates(t *testing.T) { diff --git a/internal/storage/ledger/legacy/accounts.go b/internal/storage/ledger/legacy/accounts.go deleted file mode 100644 index ccadb0c77..000000000 --- a/internal/storage/ledger/legacy/accounts.go +++ /dev/null @@ -1,198 +0,0 @@ -package legacy - -import ( - "context" - "errors" - "fmt" - ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" - "regexp" - - "github.com/formancehq/go-libs/v2/bun/bunpaginate" - - "github.com/formancehq/go-libs/v2/query" - ledger "github.com/formancehq/ledger/internal" - "github.com/uptrace/bun" -) - -func (store *Store) buildAccountQuery(q PITFilterWithVolumes, query *bun.SelectQuery) *bun.SelectQuery { - - query = query. - Column("accounts.address", "accounts.first_usage"). - Where("accounts.ledger = ?", store.name). - Apply(filterPIT(q.PIT, "first_usage")). - Order("accounts.address"). - ModelTableExpr(store.GetPrefixedRelationName("accounts")) - - if q.PIT != nil && !q.PIT.IsZero() { - query = query. - Column("accounts.address"). - ColumnExpr(`coalesce(accounts_metadata.metadata, '{}'::jsonb) as metadata`). - Join(` - left join lateral ( - select metadata, accounts_seq - from `+store.GetPrefixedRelationName("accounts_metadata")+` - where accounts_metadata.accounts_seq = accounts.seq and accounts_metadata.date < ? - order by revision desc - limit 1 - ) accounts_metadata on true - `, q.PIT) - } else { - query = query.ColumnExpr("accounts.metadata") - } - - if q.ExpandVolumes { - query = query. - ColumnExpr("volumes.*"). - Join(`join `+store.GetPrefixedRelationName("get_account_aggregated_volumes")+`(?, accounts.address, ?) volumes on true`, store.name, q.PIT) - } - - if q.ExpandEffectiveVolumes { - query = query. - ColumnExpr("effective_volumes.*"). - Join(`join `+store.GetPrefixedRelationName("get_account_aggregated_effective_volumes")+`(?, accounts.address, ?) effective_volumes on true`, store.name, q.PIT) - } - - return query -} - -func (store *Store) accountQueryContext(qb query.Builder, q ListAccountsQuery) (string, []any, error) { - metadataRegex := regexp.MustCompile(`metadata\[(.+)]`) - balanceRegex := regexp.MustCompile(`balance\[(.*)]`) - - return qb.Build(query.ContextFn(func(key, operator string, value any) (string, []any, error) { - convertOperatorToSQL := func() string { - switch operator { - case "$match": - return "=" - case "$lt": - return "<" - case "$gt": - return ">" - case "$lte": - return "<=" - case "$gte": - return ">=" - } - panic("unreachable") - } - switch { - case key == "address": - if operator != "$match" { - return "", nil, errors.New("'address' column can only be used with $match") - } - switch address := value.(type) { - case string: - return filterAccountAddress(address, "accounts.address"), nil, nil - default: - return "", nil, newErrInvalidQuery("unexpected type %T for column 'address'", address) - } - case metadataRegex.Match([]byte(key)): - if operator != "$match" { - return "", nil, newErrInvalidQuery("'account' column can only be used with $match") - } - match := metadataRegex.FindAllStringSubmatch(key, 3) - - key := "metadata" - if q.Options.Options.PIT != nil && !q.Options.Options.PIT.IsZero() { - key = "accounts_metadata.metadata" - } - - return key + " @> ?", []any{map[string]any{ - match[0][1]: value, - }}, nil - case balanceRegex.Match([]byte(key)): - match := balanceRegex.FindAllStringSubmatch(key, 2) - - return fmt.Sprintf(`( - select `+store.GetPrefixedRelationName("balance_from_volumes")+`(post_commit_volumes) - from `+store.GetPrefixedRelationName("moves")+` - where asset = ? and accounts_address = accounts.address and ledger = ? - order by seq desc - limit 1 - ) %s ?`, convertOperatorToSQL()), []any{match[0][1], store.name, value}, nil - case key == "balance": - return fmt.Sprintf(`( - select `+store.GetPrefixedRelationName("balance_from_volumes")+`(post_commit_volumes) - from `+store.GetPrefixedRelationName("moves")+` - where accounts_address = accounts.address and ledger = ? - order by seq desc - limit 1 - ) %s ?`, convertOperatorToSQL()), []any{store.name, value}, nil - - case key == "metadata": - if operator != "$exists" { - return "", nil, newErrInvalidQuery("'metadata' key filter can only be used with $exists") - } - if q.Options.Options.PIT != nil && !q.Options.Options.PIT.IsZero() { - key = "accounts_metadata.metadata" - } - - return fmt.Sprintf("%s -> ? IS NOT NULL", key), []any{value}, nil - default: - return "", nil, newErrInvalidQuery("unknown key '%s' when building query", key) - } - })) -} - -func (store *Store) buildAccountListQuery(selectQuery *bun.SelectQuery, q ListAccountsQuery, where string, args []any) *bun.SelectQuery { - selectQuery = store.buildAccountQuery(q.Options.Options, selectQuery) - - if where != "" { - return selectQuery.Where(where, args...) - } - - return selectQuery -} - -func (store *Store) GetAccountsWithVolumes(ctx context.Context, q ListAccountsQuery) (*bunpaginate.Cursor[ledger.Account], error) { - var ( - where string - args []any - err error - ) - if q.Options.QueryBuilder != nil { - where, args, err = store.accountQueryContext(q.Options.QueryBuilder, q) - if err != nil { - return nil, err - } - } - - return paginateWithOffset[ledgercontroller.PaginatedQueryOptions[PITFilterWithVolumes], ledger.Account](store, ctx, - (*bunpaginate.OffsetPaginatedQuery[ledgercontroller.PaginatedQueryOptions[PITFilterWithVolumes]])(&q), - func(query *bun.SelectQuery) *bun.SelectQuery { - return store.buildAccountListQuery(query, q, where, args) - }, - ) -} - -func (store *Store) GetAccountWithVolumes(ctx context.Context, q GetAccountQuery) (*ledger.Account, error) { - account, err := fetch[*ledger.Account](store, true, ctx, func(query *bun.SelectQuery) *bun.SelectQuery { - query = store.buildAccountQuery(q.PITFilterWithVolumes, query). - Where("accounts.address = ?", q.Addr). - Limit(1) - - return query - }) - if err != nil { - return nil, err - } - return account, nil -} - -func (store *Store) CountAccounts(ctx context.Context, q ListAccountsQuery) (int, error) { - var ( - where string - args []any - err error - ) - if q.Options.QueryBuilder != nil { - where, args, err = store.accountQueryContext(q.Options.QueryBuilder, q) - if err != nil { - return 0, err - } - } - - return count[ledger.Account](store, true, ctx, func(query *bun.SelectQuery) *bun.SelectQuery { - return store.buildAccountListQuery(query, q, where, args) - }) -} diff --git a/internal/storage/ledger/legacy/accounts_test.go b/internal/storage/ledger/legacy/accounts_test.go deleted file mode 100644 index 62201eaa8..000000000 --- a/internal/storage/ledger/legacy/accounts_test.go +++ /dev/null @@ -1,338 +0,0 @@ -//go:build it - -package legacy_test - -import ( - "github.com/formancehq/go-libs/v2/pointer" - ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" - "github.com/formancehq/ledger/internal/storage/ledger/legacy" - "math/big" - "testing" - - "github.com/formancehq/go-libs/v2/time" - - "github.com/formancehq/go-libs/v2/logging" - - "github.com/formancehq/go-libs/v2/metadata" - "github.com/formancehq/go-libs/v2/query" - ledger "github.com/formancehq/ledger/internal" - ledgerstore "github.com/formancehq/ledger/internal/storage/ledger/legacy" - "github.com/stretchr/testify/require" -) - -func TestGetAccounts(t *testing.T) { - t.Parallel() - store := newLedgerStore(t) - now := time.Now() - ctx := logging.TestingContext() - - err := store.newStore.CommitTransaction(ctx, pointer.For(ledger.NewTransaction(). - WithPostings(ledger.NewPosting("world", "account:1", "USD", big.NewInt(100))). - WithTimestamp(now). - WithInsertedAt(now))) - require.NoError(t, err) - - require.NoError(t, store.newStore.UpdateAccountsMetadata(ctx, map[string]metadata.Metadata{ - "account:1": { - "category": "1", - }, - "account:2": { - "category": "2", - }, - "account:3": { - "category": "3", - }, - "orders:1": { - "foo": "bar", - }, - "orders:2": { - "foo": "bar", - }, - })) - - err = store.newStore.CommitTransaction(ctx, pointer.For(ledger.NewTransaction(). - WithPostings(ledger.NewPosting("world", "account:1", "USD", big.NewInt(100))). - WithTimestamp(now.Add(4*time.Minute)). - WithInsertedAt(now.Add(100*time.Millisecond)))) - require.NoError(t, err) - - err = store.newStore.CommitTransaction(ctx, pointer.For(ledger.NewTransaction(). - WithPostings(ledger.NewPosting("account:1", "bank", "USD", big.NewInt(50))). - WithTimestamp(now.Add(3*time.Minute)). - WithInsertedAt(now.Add(200*time.Millisecond)))) - require.NoError(t, err) - - err = store.newStore.CommitTransaction(ctx, pointer.For(ledger.NewTransaction(). - WithPostings(ledger.NewPosting("world", "account:1", "USD", big.NewInt(0))). - WithTimestamp(now.Add(-time.Minute)). - WithInsertedAt(now.Add(200*time.Millisecond)))) - require.NoError(t, err) - - t.Run("list all", func(t *testing.T) { - t.Parallel() - accounts, err := store.GetAccountsWithVolumes(ctx, ledgerstore.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}))) - require.NoError(t, err) - require.Len(t, accounts.Data, 7) - }) - - t.Run("list using metadata", func(t *testing.T) { - t.Parallel() - accounts, err := store.GetAccountsWithVolumes(ctx, ledgerstore.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). - WithQueryBuilder(query.Match("metadata[category]", "1")), - )) - require.NoError(t, err) - require.Len(t, accounts.Data, 1) - }) - - t.Run("list before date", func(t *testing.T) { - t.Parallel() - accounts, err := store.GetAccountsWithVolumes(ctx, ledgerstore.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{ - PITFilter: ledgerstore.PITFilter{ - PIT: &now, - }, - }))) - require.NoError(t, err) - require.Len(t, accounts.Data, 2) - }) - - t.Run("list with volumes", func(t *testing.T) { - t.Parallel() - - accounts, err := store.GetAccountsWithVolumes(ctx, ledgerstore.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{ - ExpandVolumes: true, - }).WithQueryBuilder(query.Match("address", "account:1")))) - require.NoError(t, err) - require.Len(t, accounts.Data, 1) - require.Equal(t, ledger.VolumesByAssets{ - "USD": ledger.NewVolumesInt64(200, 50), - }, accounts.Data[0].Volumes) - }) - - t.Run("list with volumes using PIT", func(t *testing.T) { - t.Parallel() - - accounts, err := store.GetAccountsWithVolumes(ctx, ledgerstore.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{ - PITFilter: ledgerstore.PITFilter{ - PIT: &now, - }, - ExpandVolumes: true, - }).WithQueryBuilder(query.Match("address", "account:1")))) - require.NoError(t, err) - require.Len(t, accounts.Data, 1) - require.Equal(t, ledger.VolumesByAssets{ - "USD": ledger.NewVolumesInt64(100, 0), - }, accounts.Data[0].Volumes) - }) - - t.Run("list with effective volumes", func(t *testing.T) { - t.Parallel() - accounts, err := store.GetAccountsWithVolumes(ctx, ledgerstore.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{ - ExpandEffectiveVolumes: true, - }).WithQueryBuilder(query.Match("address", "account:1")))) - require.NoError(t, err) - require.Len(t, accounts.Data, 1) - require.Equal(t, ledger.VolumesByAssets{ - "USD": ledger.NewVolumesInt64(200, 50), - }, accounts.Data[0].EffectiveVolumes) - }) - - t.Run("list with effective volumes using PIT", func(t *testing.T) { - t.Parallel() - accounts, err := store.GetAccountsWithVolumes(ctx, ledgerstore.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{ - PITFilter: ledgerstore.PITFilter{ - PIT: &now, - }, - ExpandEffectiveVolumes: true, - }).WithQueryBuilder(query.Match("address", "account:1")))) - require.NoError(t, err) - require.Len(t, accounts.Data, 1) - require.Equal(t, ledger.VolumesByAssets{ - "USD": ledger.NewVolumesInt64(100, 0), - }, accounts.Data[0].EffectiveVolumes) - }) - - t.Run("list using filter on address", func(t *testing.T) { - t.Parallel() - accounts, err := store.GetAccountsWithVolumes(ctx, ledgerstore.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). - WithQueryBuilder(query.Match("address", "account:")), - )) - require.NoError(t, err) - require.Len(t, accounts.Data, 3) - }) - t.Run("list using filter on multiple address", func(t *testing.T) { - t.Parallel() - accounts, err := store.GetAccountsWithVolumes(ctx, ledgerstore.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). - WithQueryBuilder( - query.Or( - query.Match("address", "account:1"), - query.Match("address", "orders:"), - ), - ), - )) - require.NoError(t, err) - require.Len(t, accounts.Data, 3) - }) - t.Run("list using filter on balances", func(t *testing.T) { - t.Parallel() - accounts, err := store.GetAccountsWithVolumes(ctx, ledgerstore.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). - WithQueryBuilder(query.Lt("balance[USD]", 0)), - )) - require.NoError(t, err) - require.Len(t, accounts.Data, 1) // world - - accounts, err = store.GetAccountsWithVolumes(ctx, ledgerstore.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). - WithQueryBuilder(query.Gt("balance[USD]", 0)), - )) - require.NoError(t, err) - require.Len(t, accounts.Data, 2) - require.Equal(t, "account:1", accounts.Data[0].Address) - require.Equal(t, "bank", accounts.Data[1].Address) - }) - - t.Run("list using filter on exists metadata", func(t *testing.T) { - t.Parallel() - accounts, err := store.GetAccountsWithVolumes(ctx, ledgerstore.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). - WithQueryBuilder(query.Exists("metadata", "foo")), - )) - require.NoError(t, err) - require.Len(t, accounts.Data, 2) - - accounts, err = store.GetAccountsWithVolumes(ctx, ledgerstore.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). - WithQueryBuilder(query.Exists("metadata", "category")), - )) - require.NoError(t, err) - require.Len(t, accounts.Data, 3) - }) - - t.Run("list using filter invalid field", func(t *testing.T) { - t.Parallel() - _, err := store.GetAccountsWithVolumes(ctx, ledgerstore.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). - WithQueryBuilder(query.Lt("invalid", 0)), - )) - require.Error(t, err) - require.True(t, legacy.IsErrInvalidQuery(err)) - }) -} - -func TestGetAccount(t *testing.T) { - t.Parallel() - store := newLedgerStore(t) - now := time.Now() - ctx := logging.TestingContext() - - err := store.newStore.CommitTransaction(ctx, pointer.For(ledger.NewTransaction().WithPostings( - ledger.NewPosting("world", "multi", "USD/2", big.NewInt(100)), - ).WithTimestamp(now))) - require.NoError(t, err) - - require.NoError(t, store.newStore.UpdateAccountsMetadata(ctx, map[string]metadata.Metadata{ - "multi": { - "category": "gold", - }, - })) - - err = store.newStore.CommitTransaction(ctx, pointer.For(ledger.NewTransaction().WithPostings( - ledger.NewPosting("world", "multi", "USD/2", big.NewInt(0)), - ).WithTimestamp(now.Add(-time.Minute)))) - require.NoError(t, err) - - t.Run("find account", func(t *testing.T) { - t.Parallel() - account, err := store.GetAccountWithVolumes(ctx, ledgerstore.NewGetAccountQuery("multi")) - require.NoError(t, err) - require.Equal(t, ledger.Account{ - Address: "multi", - Metadata: metadata.Metadata{ - "category": "gold", - }, - FirstUsage: now.Add(-time.Minute), - }, *account) - - account, err = store.GetAccountWithVolumes(ctx, ledgerstore.NewGetAccountQuery("world")) - require.NoError(t, err) - require.Equal(t, ledger.Account{ - Address: "world", - Metadata: metadata.Metadata{}, - FirstUsage: now.Add(-time.Minute), - }, *account) - }) - - t.Run("find account in past", func(t *testing.T) { - t.Parallel() - account, err := store.GetAccountWithVolumes(ctx, ledgerstore.NewGetAccountQuery("multi").WithPIT(now.Add(-30*time.Second))) - require.NoError(t, err) - require.Equal(t, ledger.Account{ - Address: "multi", - Metadata: metadata.Metadata{}, - FirstUsage: now.Add(-time.Minute), - }, *account) - }) - - t.Run("find account with volumes", func(t *testing.T) { - t.Parallel() - account, err := store.GetAccountWithVolumes(ctx, ledgerstore.NewGetAccountQuery("multi"). - WithExpandVolumes()) - require.NoError(t, err) - require.Equal(t, ledger.Account{ - Address: "multi", - Metadata: metadata.Metadata{ - "category": "gold", - }, - FirstUsage: now.Add(-time.Minute), - Volumes: ledger.VolumesByAssets{ - "USD/2": ledger.NewVolumesInt64(100, 0), - }, - }, *account) - }) - - t.Run("find account with effective volumes", func(t *testing.T) { - t.Parallel() - account, err := store.GetAccountWithVolumes(ctx, ledgerstore.NewGetAccountQuery("multi"). - WithExpandEffectiveVolumes()) - require.NoError(t, err) - require.Equal(t, ledger.Account{ - Address: "multi", - Metadata: metadata.Metadata{ - "category": "gold", - }, - FirstUsage: now.Add(-time.Minute), - - EffectiveVolumes: ledger.VolumesByAssets{ - "USD/2": ledger.NewVolumesInt64(100, 0), - }, - }, *account) - }) - - t.Run("find account using pit", func(t *testing.T) { - t.Parallel() - account, err := store.GetAccountWithVolumes(ctx, ledgerstore.NewGetAccountQuery("multi").WithPIT(now)) - require.NoError(t, err) - require.Equal(t, ledger.Account{ - Address: "multi", - Metadata: metadata.Metadata{}, - FirstUsage: now.Add(-time.Minute), - }, *account) - }) - - t.Run("not existent account", func(t *testing.T) { - t.Parallel() - _, err := store.GetAccountWithVolumes(ctx, ledgerstore.NewGetAccountQuery("account_not_existing")) - require.Error(t, err) - }) - -} - -func TestCountAccounts(t *testing.T) { - t.Parallel() - store := newLedgerStore(t) - ctx := logging.TestingContext() - - err := store.newStore.CommitTransaction(ctx, pointer.For(ledger.NewTransaction().WithPostings( - ledger.NewPosting("world", "central_bank", "USD/2", big.NewInt(100)), - ))) - require.NoError(t, err) - - countAccounts, err := store.CountAccounts(ctx, ledgerstore.NewListAccountsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}))) - require.NoError(t, err) - require.EqualValues(t, 2, countAccounts) // world + central_bank -} diff --git a/internal/storage/ledger/legacy/adapters.go b/internal/storage/ledger/legacy/adapters.go deleted file mode 100644 index 3ea005891..000000000 --- a/internal/storage/ledger/legacy/adapters.go +++ /dev/null @@ -1,128 +0,0 @@ -package legacy - -import ( - "context" - "database/sql" - "github.com/formancehq/go-libs/v2/metadata" - "github.com/formancehq/go-libs/v2/migrations" - "github.com/formancehq/go-libs/v2/time" - ledger "github.com/formancehq/ledger/internal" - ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" - ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" - "github.com/uptrace/bun" -) - -type DefaultStoreAdapter struct { - newStore *ledgerstore.Store - legacyStore *Store - isFullUpToDate bool -} - -// todo; handle compat with v1 -func (d *DefaultStoreAdapter) Accounts() ledgercontroller.PaginatedResource[ledger.Account, any, ledgercontroller.OffsetPaginatedQuery[any]] { - return d.newStore.Accounts() -} - -func (d *DefaultStoreAdapter) Logs() ledgercontroller.PaginatedResource[ledger.Log, any, ledgercontroller.ColumnPaginatedQuery[any]] { - return d.newStore.Logs() -} - -func (d *DefaultStoreAdapter) Transactions() ledgercontroller.PaginatedResource[ledger.Transaction, any, ledgercontroller.ColumnPaginatedQuery[any]] { - return d.newStore.Transactions() -} - -func (d *DefaultStoreAdapter) AggregatedBalances() ledgercontroller.Resource[ledger.AggregatedVolumes, ledgercontroller.GetAggregatedVolumesOptions] { - return d.newStore.AggregatedVolumes() -} - -func (d *DefaultStoreAdapter) Volumes() ledgercontroller.PaginatedResource[ledger.VolumesWithBalanceByAssetByAccount, ledgercontroller.GetVolumesOptions, ledgercontroller.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]] { - return d.newStore.Volumes() -} - -func (d *DefaultStoreAdapter) GetDB() bun.IDB { - return d.newStore.GetDB() -} - -func (d *DefaultStoreAdapter) GetBalances(ctx context.Context, query ledgercontroller.BalanceQuery) (ledgercontroller.Balances, error) { - return d.newStore.GetBalances(ctx, query) -} - -func (d *DefaultStoreAdapter) CommitTransaction(ctx context.Context, transaction *ledger.Transaction) error { - return d.newStore.CommitTransaction(ctx, transaction) -} - -func (d *DefaultStoreAdapter) RevertTransaction(ctx context.Context, id int, at time.Time) (*ledger.Transaction, bool, error) { - return d.newStore.RevertTransaction(ctx, id, at) -} - -func (d *DefaultStoreAdapter) UpdateTransactionMetadata(ctx context.Context, transactionID int, m metadata.Metadata) (*ledger.Transaction, bool, error) { - return d.newStore.UpdateTransactionMetadata(ctx, transactionID, m) -} - -func (d *DefaultStoreAdapter) DeleteTransactionMetadata(ctx context.Context, transactionID int, key string) (*ledger.Transaction, bool, error) { - return d.newStore.DeleteTransactionMetadata(ctx, transactionID, key) -} - -func (d *DefaultStoreAdapter) UpdateAccountsMetadata(ctx context.Context, m map[string]metadata.Metadata) error { - return d.newStore.UpdateAccountsMetadata(ctx, m) -} - -func (d *DefaultStoreAdapter) UpsertAccounts(ctx context.Context, accounts ...*ledger.Account) error { - return d.newStore.UpsertAccounts(ctx, accounts...) -} - -func (d *DefaultStoreAdapter) DeleteAccountMetadata(ctx context.Context, address, key string) error { - return d.newStore.DeleteAccountMetadata(ctx, address, key) -} - -func (d *DefaultStoreAdapter) InsertLog(ctx context.Context, log *ledger.Log) error { - return d.newStore.InsertLog(ctx, log) -} - -func (d *DefaultStoreAdapter) LockLedger(ctx context.Context) error { - return d.newStore.LockLedger(ctx) -} - -func (d *DefaultStoreAdapter) ReadLogWithIdempotencyKey(ctx context.Context, ik string) (*ledger.Log, error) { - return d.newStore.ReadLogWithIdempotencyKey(ctx, ik) -} - -func (d *DefaultStoreAdapter) IsUpToDate(ctx context.Context) (bool, error) { - return d.newStore.HasMinimalVersion(ctx) -} - -func (d *DefaultStoreAdapter) GetMigrationsInfo(ctx context.Context) ([]migrations.Info, error) { - return d.newStore.GetMigrationsInfo(ctx) -} - -func (d *DefaultStoreAdapter) BeginTX(ctx context.Context, opts *sql.TxOptions) (ledgercontroller.Store, error) { - store, err := d.newStore.BeginTX(ctx, opts) - if err != nil { - return nil, err - } - - legacyStore := d.legacyStore.WithDB(store.GetDB()) - - return &DefaultStoreAdapter{ - newStore: store, - legacyStore: legacyStore, - }, nil -} - -func (d *DefaultStoreAdapter) Commit() error { - return d.newStore.Commit() -} - -func (d *DefaultStoreAdapter) Rollback() error { - return d.newStore.Rollback() -} - -func NewDefaultStoreAdapter(isFullUpToDate bool, store *ledgerstore.Store) *DefaultStoreAdapter { - return &DefaultStoreAdapter{ - isFullUpToDate: isFullUpToDate, - newStore: store, - legacyStore: New(store.GetDB(), store.GetLedger().Bucket, store.GetLedger().Name), - } -} - -var _ ledgercontroller.Store = (*DefaultStoreAdapter)(nil) diff --git a/internal/storage/ledger/legacy/balances.go b/internal/storage/ledger/legacy/balances.go deleted file mode 100644 index 7a3b02cbd..000000000 --- a/internal/storage/ledger/legacy/balances.go +++ /dev/null @@ -1,146 +0,0 @@ -package legacy - -import ( - "context" - "errors" - "fmt" - "github.com/formancehq/go-libs/v2/platform/postgres" - "github.com/formancehq/go-libs/v2/query" - ledger "github.com/formancehq/ledger/internal" - "github.com/uptrace/bun" -) - -func (store *Store) GetAggregatedBalances(ctx context.Context, q GetAggregatedBalanceQuery) (ledger.BalancesByAssets, error) { - - var ( - needMetadata bool - subQuery string - args []any - err error - ) - if q.QueryBuilder != nil { - subQuery, args, err = q.QueryBuilder.Build(query.ContextFn(func(key, operator string, value any) (string, []any, error) { - switch { - case key == "address": - if operator != "$match" { - return "", nil, newErrInvalidQuery("'address' column can only be used with $match") - } - - switch address := value.(type) { - case string: - return filterAccountAddress(address, "accounts_address"), nil, nil - default: - return "", nil, newErrInvalidQuery("unexpected type %T for column 'address'", address) - } - case metadataRegex.Match([]byte(key)): - if operator != "$match" { - return "", nil, newErrInvalidQuery("'metadata' column can only be used with $match") - } - match := metadataRegex.FindAllStringSubmatch(key, 3) - needMetadata = true - key := "accounts.metadata" - if q.PIT != nil { - key = "am.metadata" - } - - return key + " @> ?", []any{map[string]any{ - match[0][1]: value, - }}, nil - - case key == "metadata": - if operator != "$exists" { - return "", nil, newErrInvalidQuery("'metadata' key filter can only be used with $exists") - } - needMetadata = true - key := "accounts.metadata" - if q.PIT != nil && !q.PIT.IsZero() { - key = "am.metadata" - } - - return fmt.Sprintf("%s -> ? IS NOT NULL", key), []any{value}, nil - default: - return "", nil, newErrInvalidQuery("unknown key '%s' when building query", key) - } - })) - if err != nil { - return nil, err - } - } - - type Temp struct { - Aggregated ledger.VolumesByAssets `bun:"aggregated,type:jsonb"` - } - ret, err := fetch[*Temp](store, false, ctx, - func(selectQuery *bun.SelectQuery) *bun.SelectQuery { - pitColumn := "effective_date" - if q.UseInsertionDate { - pitColumn = "insertion_date" - } - moves := store.db. - NewSelect(). - ModelTableExpr(store.GetPrefixedRelationName("moves")). - DistinctOn("moves.accounts_address, moves.asset"). - Where("moves.ledger = ?", store.name). - Apply(filterPIT(q.PIT, pitColumn)) - - if q.UseInsertionDate { - moves = moves. - ColumnExpr("accounts_address"). - ColumnExpr("asset"). - ColumnExpr("first_value(moves.post_commit_volumes) over (partition by moves.accounts_address, moves.asset order by seq desc) as post_commit_volumes") - } else { - moves = moves. - ColumnExpr("accounts_address"). - ColumnExpr("asset"). - ColumnExpr("first_value(moves.post_commit_effective_volumes) over (partition by moves.accounts_address, moves.asset order by effective_date desc, seq desc) as post_commit_effective_volumes") - } - - if needMetadata { - if q.PIT != nil { - moves = moves.Join(`join lateral ( - select metadata - from `+store.GetPrefixedRelationName("accounts_metadata")+` am - where am.accounts_seq = moves.accounts_seq and (? is null or date <= ?) - order by revision desc - limit 1 - ) am on true`, q.PIT, q.PIT) - } else { - moves = moves.Join(`join lateral ( - select metadata - from ` + store.GetPrefixedRelationName("accounts") + ` a - where a.seq = moves.accounts_seq - ) accounts on true`) - } - } - if subQuery != "" { - moves = moves.Where(subQuery, args...) - } - - volumesColumn := "post_commit_effective_volumes" - if q.UseInsertionDate { - volumesColumn = "post_commit_volumes" - } - - finalQuery := selectQuery. - With("moves", moves). - With( - "data", - selectQuery.NewSelect(). - TableExpr("moves"). - ColumnExpr(fmt.Sprintf(store.GetPrefixedRelationName("volumes_to_jsonb")+`((moves.asset, (sum((moves.%s).inputs), sum((moves.%s).outputs))::%s)) as aggregated`, volumesColumn, volumesColumn, store.GetPrefixedRelationName("volumes"))). - Group("moves.asset"), - ). - TableExpr("data"). - ColumnExpr("aggregate_objects(data.aggregated) as aggregated") - - return finalQuery - }) - if err != nil && !errors.Is(err, postgres.ErrNotFound) { - return nil, err - } - if errors.Is(err, postgres.ErrNotFound) { - return ledger.BalancesByAssets{}, nil - } - - return ret.Aggregated.Balances(), nil -} diff --git a/internal/storage/ledger/legacy/balances_test.go b/internal/storage/ledger/legacy/balances_test.go deleted file mode 100644 index e2adaac9d..000000000 --- a/internal/storage/ledger/legacy/balances_test.go +++ /dev/null @@ -1,175 +0,0 @@ -//go:build it - -package legacy_test - -import ( - ledgerstore "github.com/formancehq/ledger/internal/storage/ledger/legacy" - "github.com/google/go-cmp/cmp" - "math/big" - "testing" - - "github.com/formancehq/go-libs/v2/time" - - "github.com/formancehq/go-libs/v2/logging" - "github.com/formancehq/go-libs/v2/pointer" - - "github.com/formancehq/go-libs/v2/metadata" - "github.com/formancehq/go-libs/v2/query" - ledger "github.com/formancehq/ledger/internal" - "github.com/stretchr/testify/require" -) - -func TestGetBalancesAggregated(t *testing.T) { - t.Parallel() - store := newLedgerStore(t) - now := time.Now() - ctx := logging.TestingContext() - - bigInt, _ := big.NewInt(0).SetString("1000", 10) - smallInt := big.NewInt(100) - - tx1 := ledger.NewTransaction(). - WithPostings( - ledger.NewPosting("world", "users:1", "USD", bigInt), - ledger.NewPosting("world", "users:2", "USD", smallInt), - ). - WithTimestamp(now). - WithInsertedAt(now) - err := store.newStore.CommitTransaction(ctx, &tx1) - require.NoError(t, err) - - tx2 := ledger.NewTransaction(). - WithPostings( - ledger.NewPosting("world", "users:1", "USD", bigInt), - ledger.NewPosting("world", "users:2", "USD", smallInt), - ledger.NewPosting("world", "xxx", "EUR", smallInt), - ). - WithTimestamp(now.Add(-time.Minute)). - WithInsertedAt(now.Add(time.Minute)) - err = store.newStore.CommitTransaction(ctx, &tx2) - require.NoError(t, err) - - require.NoError(t, store.newStore.UpdateAccountsMetadata(ctx, map[string]metadata.Metadata{ - "users:1": { - "category": "premium", - }, - "users:2": { - "category": "premium", - }, - })) - - require.NoError(t, store.newStore.DeleteAccountMetadata(ctx, "users:2", "category")) - - require.NoError(t, store.newStore.UpdateAccountsMetadata(ctx, map[string]metadata.Metadata{ - "users:1": { - "category": "premium", - }, - "users:2": { - "category": "2", - }, - "world": { - "world": "bar", - }, - })) - - t.Run("aggregate on all", func(t *testing.T) { - t.Parallel() - cursor, err := store.GetAggregatedBalances(ctx, ledgerstore.NewGetAggregatedBalancesQuery(ledgerstore.PITFilter{}, nil, false)) - require.NoError(t, err) - RequireEqual(t, ledger.BalancesByAssets{ - "USD": big.NewInt(0), - "EUR": big.NewInt(0), - }, cursor) - }) - t.Run("filter on address", func(t *testing.T) { - t.Parallel() - ret, err := store.GetAggregatedBalances(ctx, ledgerstore.NewGetAggregatedBalancesQuery(ledgerstore.PITFilter{}, - query.Match("address", "users:"), false)) - require.NoError(t, err) - require.Equal(t, ledger.BalancesByAssets{ - "USD": big.NewInt(0).Add( - big.NewInt(0).Mul(bigInt, big.NewInt(2)), - big.NewInt(0).Mul(smallInt, big.NewInt(2)), - ), - }, ret) - }) - t.Run("using pit on effective date", func(t *testing.T) { - t.Parallel() - ret, err := store.GetAggregatedBalances(ctx, ledgerstore.NewGetAggregatedBalancesQuery(ledgerstore.PITFilter{ - PIT: pointer.For(now.Add(-time.Second)), - }, query.Match("address", "users:"), false)) - require.NoError(t, err) - require.Equal(t, ledger.BalancesByAssets{ - "USD": big.NewInt(0).Add( - bigInt, - smallInt, - ), - }, ret) - }) - t.Run("using pit on insertion date", func(t *testing.T) { - t.Parallel() - ret, err := store.GetAggregatedBalances(ctx, ledgerstore.NewGetAggregatedBalancesQuery(ledgerstore.PITFilter{ - PIT: pointer.For(now), - }, query.Match("address", "users:"), true)) - require.NoError(t, err) - require.Equal(t, ledger.BalancesByAssets{ - "USD": big.NewInt(0).Add( - bigInt, - smallInt, - ), - }, ret) - }) - t.Run("using a metadata and pit", func(t *testing.T) { - t.Parallel() - - ret, err := store.GetAggregatedBalances(ctx, ledgerstore.NewGetAggregatedBalancesQuery(ledgerstore.PITFilter{ - PIT: pointer.For(now.Add(time.Minute)), - }, query.Match("metadata[category]", "premium"), false)) - require.NoError(t, err) - require.Equal(t, ledger.BalancesByAssets{ - "USD": big.NewInt(0).Add( - big.NewInt(0).Mul(bigInt, big.NewInt(2)), - big.NewInt(0), - ), - }, ret) - }) - t.Run("using a metadata without pit", func(t *testing.T) { - t.Parallel() - ret, err := store.GetAggregatedBalances(ctx, ledgerstore.NewGetAggregatedBalancesQuery(ledgerstore.PITFilter{}, - query.Match("metadata[category]", "premium"), false)) - require.NoError(t, err) - require.Equal(t, ledger.BalancesByAssets{ - "USD": big.NewInt(0).Mul(bigInt, big.NewInt(2)), - }, ret) - }) - t.Run("when no matching", func(t *testing.T) { - t.Parallel() - ret, err := store.GetAggregatedBalances(ctx, ledgerstore.NewGetAggregatedBalancesQuery(ledgerstore.PITFilter{}, - query.Match("metadata[category]", "guest"), false)) - require.NoError(t, err) - require.Equal(t, ledger.BalancesByAssets{}, ret) - }) - - t.Run("using a filter exist on metadata", func(t *testing.T) { - t.Parallel() - ret, err := store.GetAggregatedBalances(ctx, ledgerstore.NewGetAggregatedBalancesQuery(ledgerstore.PITFilter{}, query.Exists("metadata", "category"), false)) - require.NoError(t, err) - require.Equal(t, ledger.BalancesByAssets{ - "USD": big.NewInt(0).Add( - big.NewInt(0).Mul(bigInt, big.NewInt(2)), - big.NewInt(0).Mul(smallInt, big.NewInt(2)), - ), - }, ret) - }) -} - -func RequireEqual(t *testing.T, expected, actual any) { - t.Helper() - if diff := cmp.Diff(expected, actual, cmp.Comparer(bigIntComparer)); diff != "" { - require.Failf(t, "Content not matching", diff) - } -} - -func bigIntComparer(v1 *big.Int, v2 *big.Int) bool { - return v1.String() == v2.String() -} diff --git a/internal/storage/ledger/legacy/debug.go b/internal/storage/ledger/legacy/debug.go deleted file mode 100644 index 64141226f..000000000 --- a/internal/storage/ledger/legacy/debug.go +++ /dev/null @@ -1,42 +0,0 @@ -package legacy - -import ( - "context" - "database/sql" - "fmt" - "github.com/shomali11/xsql" - "github.com/uptrace/bun" -) - -//nolint:unused -func (s *Store) DumpTables(ctx context.Context, tables ...string) { - for _, table := range tables { - s.DumpQuery( - ctx, - s.db.NewSelect(). - ModelTableExpr(s.GetPrefixedRelationName(table)), - ) - } -} - -//nolint:unused -func (s *Store) DumpQuery(ctx context.Context, query *bun.SelectQuery) { - fmt.Println(query) - rows, err := query.Rows(ctx) - if err != nil { - panic(err) - } - s.DumpRows(rows) -} - -//nolint:unused -func (s *Store) DumpRows(rows *sql.Rows) { - data, err := xsql.Pretty(rows) - if err != nil { - panic(err) - } - fmt.Println(data) - if err := rows.Close(); err != nil { - panic(err) - } -} diff --git a/internal/storage/ledger/legacy/errors.go b/internal/storage/ledger/legacy/errors.go deleted file mode 100644 index 41754951e..000000000 --- a/internal/storage/ledger/legacy/errors.go +++ /dev/null @@ -1,30 +0,0 @@ -package legacy - -import ( - "fmt" - - "github.com/pkg/errors" -) - -type errInvalidQuery struct { - msg string -} - -func (e *errInvalidQuery) Error() string { - return e.msg -} - -func (e *errInvalidQuery) Is(err error) bool { - _, ok := err.(*errInvalidQuery) - return ok -} - -func newErrInvalidQuery(msg string, args ...any) *errInvalidQuery { - return &errInvalidQuery{ - msg: fmt.Sprintf(msg, args...), - } -} - -func IsErrInvalidQuery(err error) bool { - return errors.Is(err, &errInvalidQuery{}) -} diff --git a/internal/storage/ledger/legacy/logs.go b/internal/storage/ledger/legacy/logs.go deleted file mode 100644 index a022aa789..000000000 --- a/internal/storage/ledger/legacy/logs.go +++ /dev/null @@ -1,50 +0,0 @@ -package legacy - -import ( - "context" - "fmt" - "github.com/formancehq/go-libs/v2/bun/bunpaginate" - ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" - ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" - - "github.com/formancehq/go-libs/v2/query" - ledger "github.com/formancehq/ledger/internal" - "github.com/uptrace/bun" -) - -func (store *Store) logsQueryBuilder(q ledgercontroller.PaginatedQueryOptions[any]) func(*bun.SelectQuery) *bun.SelectQuery { - return func(selectQuery *bun.SelectQuery) *bun.SelectQuery { - - selectQuery = selectQuery.Where("ledger = ?", store.name).ModelTableExpr(store.GetPrefixedRelationName("logs")) - if q.QueryBuilder != nil { - subQuery, args, err := q.QueryBuilder.Build(query.ContextFn(func(key, operator string, value any) (string, []any, error) { - switch { - case key == "date": - return fmt.Sprintf("%s %s ?", key, query.DefaultComparisonOperatorsMapping[operator]), []any{value}, nil - default: - return "", nil, fmt.Errorf("unknown key '%s' when building query", key) - } - })) - if err != nil { - panic(err) - } - selectQuery = selectQuery.Where(subQuery, args...) - } - - return selectQuery - } -} - -func (store *Store) GetLogs(ctx context.Context, q GetLogsQuery) (*bunpaginate.Cursor[ledger.Log], error) { - logs, err := paginateWithColumn[ledgercontroller.PaginatedQueryOptions[any], ledgerstore.Log](store, ctx, - (*bunpaginate.ColumnPaginatedQuery[ledgercontroller.PaginatedQueryOptions[any]])(&q), - store.logsQueryBuilder(q.Options), - ) - if err != nil { - return nil, err - } - - return bunpaginate.MapCursor(logs, func(from ledgerstore.Log) ledger.Log { - return from.ToCore() - }), nil -} diff --git a/internal/storage/ledger/legacy/logs_test.go b/internal/storage/ledger/legacy/logs_test.go deleted file mode 100644 index 6e6e298f9..000000000 --- a/internal/storage/ledger/legacy/logs_test.go +++ /dev/null @@ -1,62 +0,0 @@ -//go:build it - -package legacy_test - -import ( - ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" - ledgerstore "github.com/formancehq/ledger/internal/storage/ledger/legacy" - "testing" - - "github.com/formancehq/go-libs/v2/bun/bunpaginate" - "github.com/formancehq/go-libs/v2/time" - - "github.com/formancehq/go-libs/v2/logging" - - "github.com/formancehq/go-libs/v2/query" - ledger "github.com/formancehq/ledger/internal" - "github.com/stretchr/testify/require" -) - -func TestLogsList(t *testing.T) { - t.Parallel() - store := newLedgerStore(t) - now := time.Now() - ctx := logging.TestingContext() - - for i := 1; i <= 3; i++ { - newLog := ledger.NewLog(ledger.CreatedTransaction{ - Transaction: ledger.NewTransaction(), - AccountMetadata: ledger.AccountMetadata{}, - }) - newLog.Date = now.Add(-time.Duration(i) * time.Hour) - - err := store.newStore.InsertLog(ctx, &newLog) - require.NoError(t, err) - } - - cursor, err := store.GetLogs(ctx, ledgerstore.NewListLogsQuery(ledgercontroller.NewPaginatedQueryOptions[any](nil))) - require.NoError(t, err) - require.Equal(t, bunpaginate.QueryDefaultPageSize, cursor.PageSize) - - require.Equal(t, 3, len(cursor.Data)) - require.EqualValues(t, 3, cursor.Data[0].ID) - - cursor, err = store.GetLogs(ctx, ledgerstore.NewListLogsQuery(ledgercontroller.NewPaginatedQueryOptions[any](nil).WithPageSize(1))) - require.NoError(t, err) - // Should get only the first log. - require.Equal(t, 1, cursor.PageSize) - require.EqualValues(t, 3, cursor.Data[0].ID) - - cursor, err = store.GetLogs(ctx, ledgerstore.NewListLogsQuery(ledgercontroller.NewPaginatedQueryOptions[any](nil). - WithQueryBuilder(query.And( - query.Gte("date", now.Add(-2*time.Hour)), - query.Lt("date", now.Add(-time.Hour)), - )). - WithPageSize(10), - )) - require.NoError(t, err) - require.Equal(t, 10, cursor.PageSize) - // Should get only the second log, as StartTime is inclusive and EndTime exclusive. - require.Len(t, cursor.Data, 1) - require.EqualValues(t, 2, cursor.Data[0].ID) -} diff --git a/internal/storage/ledger/legacy/main_test.go b/internal/storage/ledger/legacy/main_test.go deleted file mode 100644 index 392ea3750..000000000 --- a/internal/storage/ledger/legacy/main_test.go +++ /dev/null @@ -1,79 +0,0 @@ -//go:build it - -package legacy_test - -import ( - "github.com/formancehq/go-libs/v2/bun/bundebug" - "github.com/formancehq/go-libs/v2/testing/docker" - "github.com/formancehq/go-libs/v2/testing/utils" - "github.com/formancehq/ledger/internal/storage/bucket" - ledgerstore "github.com/formancehq/ledger/internal/storage/ledger" - "github.com/formancehq/ledger/internal/storage/ledger/legacy" - systemstore "github.com/formancehq/ledger/internal/storage/system" - "go.opentelemetry.io/otel/trace/noop" - "os" - "testing" - - "github.com/formancehq/go-libs/v2/bun/bunconnect" - - "github.com/uptrace/bun" - - "github.com/formancehq/go-libs/v2/logging" - "github.com/formancehq/go-libs/v2/testing/platform/pgtesting" - ledger "github.com/formancehq/ledger/internal" - "github.com/google/uuid" - "github.com/stretchr/testify/require" -) - -var ( - srv *pgtesting.PostgresServer -) - -func TestMain(m *testing.M) { - utils.WithTestMain(func(t *utils.TestingTForMain) int { - srv = pgtesting.CreatePostgresServer(t, docker.NewPool(t, logging.Testing())) - - return m.Run() - }) -} - -type T interface { - require.TestingT - Helper() - Cleanup(func()) -} - -type testStore struct { - *legacy.Store - newStore *ledgerstore.Store -} - -func newLedgerStore(t T) *testStore { - t.Helper() - - ledgerName := uuid.NewString()[:8] - ctx := logging.TestingContext() - - pgDatabase := srv.NewDatabase(t) - - hooks := make([]bun.QueryHook, 0) - if os.Getenv("DEBUG") == "true" { - hooks = append(hooks, bundebug.NewQueryHook()) - } - - db, err := bunconnect.OpenSQLDB(ctx, pgDatabase.ConnectionOptions(), hooks...) - require.NoError(t, err) - - require.NoError(t, systemstore.Migrate(ctx, db)) - - l := ledger.MustNewWithDefault(ledgerName) - - b := bucket.NewDefault(db, noop.Tracer{}, ledger.DefaultBucket) - require.NoError(t, b.Migrate(ctx)) - require.NoError(t, b.AddLedger(ctx, l)) - - return &testStore{ - Store: legacy.New(db, ledger.DefaultBucket, l.Name), - newStore: ledgerstore.New(db, b, l), - } -} diff --git a/internal/storage/ledger/legacy/queries.go b/internal/storage/ledger/legacy/queries.go deleted file mode 100644 index ade76c7aa..000000000 --- a/internal/storage/ledger/legacy/queries.go +++ /dev/null @@ -1,159 +0,0 @@ -package legacy - -import ( - "github.com/formancehq/go-libs/v2/bun/bunpaginate" - "github.com/formancehq/go-libs/v2/pointer" - "github.com/formancehq/go-libs/v2/query" - "github.com/formancehq/go-libs/v2/time" - "github.com/formancehq/ledger/internal/controller/ledger" -) - -type PITFilter struct { - PIT *time.Time `json:"pit"` - OOT *time.Time `json:"oot"` -} - -type PITFilterWithVolumes struct { - PITFilter - ExpandVolumes bool `json:"volumes"` - ExpandEffectiveVolumes bool `json:"effectiveVolumes"` -} - -type FiltersForVolumes struct { - PITFilter - UseInsertionDate bool - GroupLvl int -} - -func NewGetVolumesWithBalancesQuery(opts ledger.PaginatedQueryOptions[FiltersForVolumes]) GetVolumesWithBalancesQuery { - return GetVolumesWithBalancesQuery{ - PageSize: opts.PageSize, - Order: bunpaginate.OrderAsc, - Options: opts, - } -} - -type ListTransactionsQuery bunpaginate.ColumnPaginatedQuery[ledger.PaginatedQueryOptions[PITFilterWithVolumes]] - -func (q ListTransactionsQuery) WithColumn(column string) ListTransactionsQuery { - ret := pointer.For((bunpaginate.ColumnPaginatedQuery[ledger.PaginatedQueryOptions[PITFilterWithVolumes]])(q)) - ret = ret.WithColumn(column) - - return ListTransactionsQuery(*ret) -} - -func NewListTransactionsQuery(options ledger.PaginatedQueryOptions[PITFilterWithVolumes]) ListTransactionsQuery { - return ListTransactionsQuery{ - PageSize: options.PageSize, - Column: "id", - Order: bunpaginate.OrderDesc, - Options: options, - } -} - -type GetTransactionQuery struct { - PITFilterWithVolumes - ID int -} - -func (q GetTransactionQuery) WithExpandVolumes() GetTransactionQuery { - q.ExpandVolumes = true - - return q -} - -func (q GetTransactionQuery) WithExpandEffectiveVolumes() GetTransactionQuery { - q.ExpandEffectiveVolumes = true - - return q -} - -func NewGetTransactionQuery(id int) GetTransactionQuery { - return GetTransactionQuery{ - PITFilterWithVolumes: PITFilterWithVolumes{}, - ID: id, - } -} - -type ListAccountsQuery bunpaginate.OffsetPaginatedQuery[ledger.PaginatedQueryOptions[PITFilterWithVolumes]] - -func (q ListAccountsQuery) WithExpandVolumes() ListAccountsQuery { - q.Options.Options.ExpandVolumes = true - - return q -} - -func (q ListAccountsQuery) WithExpandEffectiveVolumes() ListAccountsQuery { - q.Options.Options.ExpandEffectiveVolumes = true - - return q -} - -func NewListAccountsQuery(opts ledger.PaginatedQueryOptions[PITFilterWithVolumes]) ListAccountsQuery { - return ListAccountsQuery{ - PageSize: opts.PageSize, - Order: bunpaginate.OrderAsc, - Options: opts, - } -} - -type GetAccountQuery struct { - PITFilterWithVolumes - Addr string -} - -func (q GetAccountQuery) WithPIT(pit time.Time) GetAccountQuery { - q.PIT = &pit - - return q -} - -func (q GetAccountQuery) WithExpandVolumes() GetAccountQuery { - q.ExpandVolumes = true - - return q -} - -func (q GetAccountQuery) WithExpandEffectiveVolumes() GetAccountQuery { - q.ExpandEffectiveVolumes = true - - return q -} - -func NewGetAccountQuery(addr string) GetAccountQuery { - return GetAccountQuery{ - Addr: addr, - } -} - -type GetAggregatedBalanceQuery struct { - PITFilter - QueryBuilder query.Builder - UseInsertionDate bool -} - -func NewGetAggregatedBalancesQuery(filter PITFilter, qb query.Builder, useInsertionDate bool) GetAggregatedBalanceQuery { - return GetAggregatedBalanceQuery{ - PITFilter: filter, - QueryBuilder: qb, - UseInsertionDate: useInsertionDate, - } -} - -type GetVolumesWithBalancesQuery bunpaginate.OffsetPaginatedQuery[ledger.PaginatedQueryOptions[FiltersForVolumes]] - -type GetLogsQuery bunpaginate.ColumnPaginatedQuery[ledger.PaginatedQueryOptions[any]] - -func (q GetLogsQuery) WithOrder(order bunpaginate.Order) GetLogsQuery { - q.Order = order - return q -} - -func NewListLogsQuery(options ledger.PaginatedQueryOptions[any]) GetLogsQuery { - return GetLogsQuery{ - PageSize: options.PageSize, - Column: "id", - Order: bunpaginate.OrderDesc, - Options: options, - } -} diff --git a/internal/storage/ledger/legacy/store.go b/internal/storage/ledger/legacy/store.go deleted file mode 100644 index d3ae4cb62..000000000 --- a/internal/storage/ledger/legacy/store.go +++ /dev/null @@ -1,43 +0,0 @@ -package legacy - -import ( - "fmt" - _ "github.com/jackc/pgx/v5/stdlib" - "github.com/uptrace/bun" -) - -type Store struct { - db bun.IDB - - bucket string - name string -} - -func (store *Store) GetPrefixedRelationName(v string) string { - return fmt.Sprintf(`"%s".%s`, store.bucket, v) -} - -func (store *Store) Name() string { - return store.name -} - -func (store *Store) GetDB() bun.IDB { - return store.db -} - -func (store Store) WithDB(db bun.IDB) *Store { - store.db = db - return &store -} - -func New( - db bun.IDB, - bucket string, - name string, -) *Store { - return &Store{ - db: db, - bucket: bucket, - name: name, - } -} diff --git a/internal/storage/ledger/legacy/transactions.go b/internal/storage/ledger/legacy/transactions.go deleted file mode 100644 index b9bd8399a..000000000 --- a/internal/storage/ledger/legacy/transactions.go +++ /dev/null @@ -1,206 +0,0 @@ -package legacy - -import ( - "context" - "errors" - "fmt" - ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" - "regexp" - - "github.com/formancehq/go-libs/v2/time" - - "github.com/formancehq/go-libs/v2/bun/bunpaginate" - - "github.com/formancehq/go-libs/v2/query" - ledger "github.com/formancehq/ledger/internal" - "github.com/uptrace/bun" -) - -var ( - metadataRegex = regexp.MustCompile(`metadata\[(.+)]`) -) - -func (store *Store) buildTransactionQuery(p PITFilterWithVolumes, query *bun.SelectQuery) *bun.SelectQuery { - - selectMetadata := query.NewSelect(). - ModelTableExpr(store.GetPrefixedRelationName("transactions_metadata")). - Where("transactions.seq = transactions_metadata.transactions_seq"). - Order("revision desc"). - Limit(1) - - if p.PIT != nil && !p.PIT.IsZero() { - selectMetadata = selectMetadata.Where("date <= ?", p.PIT) - } - - query = query. - ModelTableExpr(store.GetPrefixedRelationName("transactions")). - Where("transactions.ledger = ?", store.name) - - if p.PIT != nil && !p.PIT.IsZero() { - query = query. - Where("timestamp <= ?", p.PIT). - Column("id", "inserted_at", "timestamp", "postings"). - Column("transactions_metadata.metadata"). - Join(fmt.Sprintf(`left join lateral (%s) as transactions_metadata on true`, selectMetadata.String())). - ColumnExpr(fmt.Sprintf("case when reverted_at is not null and reverted_at > '%s' then null else reverted_at end", p.PIT.Format(time.DateFormat))) - } else { - query = query.Column( - "transactions.metadata", - "transactions.id", - "transactions.inserted_at", - "transactions.timestamp", - "transactions.postings", - "transactions.reverted_at", - "transactions.reference", - ) - } - - if p.ExpandEffectiveVolumes { - query = query.ColumnExpr(store.GetPrefixedRelationName("get_aggregated_effective_volumes_for_transaction")+"(?, transactions.seq) as post_commit_effective_volumes", store.name) - } - if p.ExpandVolumes { - query = query.ColumnExpr(store.GetPrefixedRelationName("get_aggregated_volumes_for_transaction")+"(?, transactions.seq) as post_commit_volumes", store.name) - } - return query -} - -func (store *Store) transactionQueryContext(qb query.Builder, q ListTransactionsQuery) (string, []any, error) { - - return qb.Build(query.ContextFn(func(key, operator string, value any) (string, []any, error) { - switch { - case key == "reference" || key == "timestamp": - return fmt.Sprintf("%s %s ?", key, query.DefaultComparisonOperatorsMapping[operator]), []any{value}, nil - case key == "reverted": - if operator != "$match" { - return "", nil, newErrInvalidQuery("'reverted' column can only be used with $match") - } - switch value := value.(type) { - case bool: - ret := "reverted_at is" - if value { - ret += " not" - } - return ret + " null", nil, nil - default: - return "", nil, newErrInvalidQuery("'reverted' can only be used with bool value") - } - case key == "account": - if operator != "$match" { - return "", nil, newErrInvalidQuery("'account' column can only be used with $match") - } - switch address := value.(type) { - case string: - return filterAccountAddressOnTransactions(address, true, true), nil, nil - default: - return "", nil, newErrInvalidQuery("unexpected type %T for column 'account'", address) - } - case key == "source": - if operator != "$match" { - return "", nil, errors.New("'source' column can only be used with $match") - } - switch address := value.(type) { - case string: - return filterAccountAddressOnTransactions(address, true, false), nil, nil - default: - return "", nil, newErrInvalidQuery("unexpected type %T for column 'source'", address) - } - case key == "destination": - if operator != "$match" { - return "", nil, errors.New("'destination' column can only be used with $match") - } - switch address := value.(type) { - case string: - return filterAccountAddressOnTransactions(address, false, true), nil, nil - default: - return "", nil, newErrInvalidQuery("unexpected type %T for column 'destination'", address) - } - case metadataRegex.Match([]byte(key)): - if operator != "$match" { - return "", nil, newErrInvalidQuery("'account' column can only be used with $match") - } - match := metadataRegex.FindAllStringSubmatch(key, 3) - - key := "metadata" - if q.Options.Options.PIT != nil && !q.Options.Options.PIT.IsZero() { - key = "transactions_metadata.metadata" - } - - return key + " @> ?", []any{map[string]any{ - match[0][1]: value, - }}, nil - - case key == "metadata": - if operator != "$exists" { - return "", nil, newErrInvalidQuery("'metadata' key filter can only be used with $exists") - } - if q.Options.Options.PIT != nil && !q.Options.Options.PIT.IsZero() { - key = "transactions_metadata.metadata" - } - - return fmt.Sprintf("%s -> ? IS NOT NULL", key), []any{value}, nil - default: - return "", nil, newErrInvalidQuery("unknown key '%s' when building query", key) - } - })) -} - -func (store *Store) buildTransactionListQuery(selectQuery *bun.SelectQuery, q ledgercontroller.PaginatedQueryOptions[PITFilterWithVolumes], where string, args []any) *bun.SelectQuery { - - selectQuery = store.buildTransactionQuery(q.Options, selectQuery) - if where != "" { - return selectQuery.Where(where, args...) - } - - return selectQuery -} - -func (store *Store) GetTransactions(ctx context.Context, q ListTransactionsQuery) (*bunpaginate.Cursor[ledger.Transaction], error) { - - var ( - where string - args []any - err error - ) - if q.Options.QueryBuilder != nil { - where, args, err = store.transactionQueryContext(q.Options.QueryBuilder, q) - if err != nil { - return nil, err - } - } - - return paginateWithColumn[ledgercontroller.PaginatedQueryOptions[PITFilterWithVolumes], ledger.Transaction](store, ctx, - (*bunpaginate.ColumnPaginatedQuery[ledgercontroller.PaginatedQueryOptions[PITFilterWithVolumes]])(&q), - func(query *bun.SelectQuery) *bun.SelectQuery { - return store.buildTransactionListQuery(query, q.Options, where, args) - }, - ) -} - -func (store *Store) CountTransactions(ctx context.Context, q ListTransactionsQuery) (int, error) { - - var ( - where string - args []any - err error - ) - - if q.Options.QueryBuilder != nil { - where, args, err = store.transactionQueryContext(q.Options.QueryBuilder, q) - if err != nil { - return 0, err - } - } - - return count[ledger.Transaction](store, true, ctx, func(query *bun.SelectQuery) *bun.SelectQuery { - return store.buildTransactionListQuery(query, q.Options, where, args) - }) -} - -func (store *Store) GetTransactionWithVolumes(ctx context.Context, filter GetTransactionQuery) (*ledger.Transaction, error) { - return fetch[*ledger.Transaction](store, true, ctx, - func(query *bun.SelectQuery) *bun.SelectQuery { - return store.buildTransactionQuery(filter.PITFilterWithVolumes, query). - Where("transactions.id = ?", filter.ID). - Limit(1) - }) -} diff --git a/internal/storage/ledger/legacy/transactions_test.go b/internal/storage/ledger/legacy/transactions_test.go deleted file mode 100644 index 23b778f5a..000000000 --- a/internal/storage/ledger/legacy/transactions_test.go +++ /dev/null @@ -1,281 +0,0 @@ -//go:build it - -package legacy_test - -import ( - "context" - "fmt" - ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" - ledgerstore "github.com/formancehq/ledger/internal/storage/ledger/legacy" - "math/big" - "testing" - - "github.com/formancehq/go-libs/v2/time" - - "github.com/pkg/errors" - - "github.com/formancehq/go-libs/v2/logging" - "github.com/formancehq/go-libs/v2/pointer" - - "github.com/formancehq/go-libs/v2/metadata" - "github.com/formancehq/go-libs/v2/query" - ledger "github.com/formancehq/ledger/internal" - "github.com/stretchr/testify/require" -) - -func TestGetTransactionWithVolumes(t *testing.T) { - t.Parallel() - store := newLedgerStore(t) - now := time.Now() - ctx := logging.TestingContext() - - tx1 := ledger.NewTransaction(). - WithPostings( - ledger.NewPosting("world", "central_bank", "USD", big.NewInt(100)), - ). - WithReference("tx1"). - WithTimestamp(now.Add(-3 * time.Hour)) - err := store.newStore.CommitTransaction(ctx, &tx1) - require.NoError(t, err) - - tx2 := ledger.NewTransaction(). - WithPostings( - ledger.NewPosting("world", "central_bank", "USD", big.NewInt(100)), - ). - WithReference("tx2"). - WithTimestamp(now.Add(-2 * time.Hour)) - err = store.newStore.CommitTransaction(ctx, &tx2) - require.NoError(t, err) - - tx, err := store.GetTransactionWithVolumes(ctx, ledgerstore.NewGetTransactionQuery(tx1.ID). - WithExpandVolumes(). - WithExpandEffectiveVolumes()) - require.NoError(t, err) - require.Equal(t, tx1.Postings, tx.Postings) - require.Equal(t, tx1.Reference, tx.Reference) - require.Equal(t, tx1.Timestamp, tx.Timestamp) - RequireEqual(t, ledger.PostCommitVolumes{ - "world": { - "USD": { - Input: big.NewInt(0), - Output: big.NewInt(100), - }, - }, - "central_bank": { - "USD": { - Input: big.NewInt(100), - Output: big.NewInt(0), - }, - }, - }, tx.PostCommitVolumes) - - tx, err = store.GetTransactionWithVolumes(ctx, ledgerstore.NewGetTransactionQuery(tx2.ID). - WithExpandVolumes(). - WithExpandEffectiveVolumes()) - require.NoError(t, err) - require.Equal(t, tx2.Postings, tx.Postings) - require.Equal(t, tx2.Reference, tx.Reference) - require.Equal(t, tx2.Timestamp, tx.Timestamp) - RequireEqual(t, ledger.PostCommitVolumes{ - "world": { - "USD": { - Input: big.NewInt(0), - Output: big.NewInt(200), - }, - }, - "central_bank": { - "USD": { - Input: big.NewInt(200), - Output: big.NewInt(0), - }, - }, - }, tx.PostCommitVolumes) -} - -func TestCountTransactions(t *testing.T) { - t.Parallel() - store := newLedgerStore(t) - ctx := logging.TestingContext() - - for i := 0; i < 3; i++ { - tx := ledger.NewTransaction().WithPostings( - ledger.NewPosting("world", fmt.Sprintf("account%d", i), "USD", big.NewInt(100)), - ) - err := store.newStore.CommitTransaction(ctx, &tx) - require.NoError(t, err) - } - - count, err := store.CountTransactions(context.Background(), ledgerstore.NewListTransactionsQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}))) - require.NoError(t, err, "counting transactions should not fail") - require.Equal(t, 3, count, "count should be equal") -} - -func TestGetTransactions(t *testing.T) { - t.Parallel() - store := newLedgerStore(t) - now := time.Now() - ctx := logging.TestingContext() - - tx1 := ledger.NewTransaction(). - WithPostings( - ledger.NewPosting("world", "alice", "USD", big.NewInt(100)), - ). - WithMetadata(metadata.Metadata{"category": "1"}). - WithTimestamp(now.Add(-3 * time.Hour)) - err := store.newStore.CommitTransaction(ctx, &tx1) - require.NoError(t, err) - - tx2 := ledger.NewTransaction(). - WithPostings( - ledger.NewPosting("world", "bob", "USD", big.NewInt(100)), - ). - WithMetadata(metadata.Metadata{"category": "2"}). - WithTimestamp(now.Add(-2 * time.Hour)) - err = store.newStore.CommitTransaction(ctx, &tx2) - require.NoError(t, err) - - tx3BeforeRevert := ledger.NewTransaction(). - WithPostings( - ledger.NewPosting("world", "users:marley", "USD", big.NewInt(100)), - ). - WithMetadata(metadata.Metadata{"category": "3"}). - WithTimestamp(now.Add(-time.Hour)) - err = store.newStore.CommitTransaction(ctx, &tx3BeforeRevert) - require.NoError(t, err) - - _, hasBeenReverted, err := store.newStore.RevertTransaction(ctx, tx3BeforeRevert.ID, time.Time{}) - require.NoError(t, err) - require.True(t, hasBeenReverted) - - tx4 := tx3BeforeRevert.Reverse().WithTimestamp(now) - err = store.newStore.CommitTransaction(ctx, &tx4) - require.NoError(t, err) - - _, _, err = store.newStore.UpdateTransactionMetadata(ctx, tx3BeforeRevert.ID, metadata.Metadata{ - "additional_metadata": "true", - }) - require.NoError(t, err) - - // refresh tx3 - // we can't take the result of the call on RevertTransaction nor UpdateTransactionMetadata as the result does not contains pc(e)v - tx3 := func() ledger.Transaction { - tx3, err := store.Store.GetTransactionWithVolumes(ctx, ledgerstore.NewGetTransactionQuery(tx3BeforeRevert.ID). - WithExpandVolumes(). - WithExpandEffectiveVolumes()) - require.NoError(t, err) - return *tx3 - }() - - tx5 := ledger.NewTransaction(). - WithPostings( - ledger.NewPosting("users:marley", "sellers:amazon", "USD", big.NewInt(100)), - ). - WithTimestamp(now) - err = store.newStore.CommitTransaction(ctx, &tx5) - require.NoError(t, err) - - type testCase struct { - name string - query ledgercontroller.PaginatedQueryOptions[ledgerstore.PITFilterWithVolumes] - expected []ledger.Transaction - expectError error - } - testCases := []testCase{ - { - name: "nominal", - query: ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}), - expected: []ledger.Transaction{tx5, tx4, tx3, tx2, tx1}, - }, - { - name: "address filter", - query: ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). - WithQueryBuilder(query.Match("account", "bob")), - expected: []ledger.Transaction{tx2}, - }, - { - name: "address filter using segments matching two addresses by individual segments", - query: ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). - WithQueryBuilder(query.Match("account", "users:amazon")), - expected: []ledger.Transaction{}, - }, - { - name: "address filter using segment", - query: ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). - WithQueryBuilder(query.Match("account", "users:")), - expected: []ledger.Transaction{tx5, tx4, tx3}, - }, - { - name: "filter using metadata", - query: ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). - WithQueryBuilder(query.Match("metadata[category]", "2")), - expected: []ledger.Transaction{tx2}, - }, - { - name: "using point in time", - query: ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{ - PITFilter: ledgerstore.PITFilter{ - PIT: pointer.For(now.Add(-time.Hour)), - }, - }), - expected: []ledger.Transaction{tx3BeforeRevert, tx2, tx1}, - }, - { - name: "reverted transactions", - query: ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). - WithQueryBuilder(query.Match("reverted", true)), - expected: []ledger.Transaction{tx3}, - }, - { - name: "filter using exists metadata", - query: ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). - WithQueryBuilder(query.Exists("metadata", "category")), - expected: []ledger.Transaction{tx3, tx2, tx1}, - }, - { - name: "filter using metadata and pit", - query: ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{ - PITFilter: ledgerstore.PITFilter{ - PIT: pointer.For(tx3.Timestamp), - }, - }). - WithQueryBuilder(query.Match("metadata[category]", "2")), - expected: []ledger.Transaction{tx2}, - }, - { - name: "filter using not exists metadata", - query: ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). - WithQueryBuilder(query.Not(query.Exists("metadata", "category"))), - expected: []ledger.Transaction{tx5, tx4}, - }, - { - name: "filter using timestamp", - query: ledgercontroller.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}). - WithQueryBuilder(query.Match("timestamp", tx5.Timestamp.Format(time.RFC3339Nano))), - expected: []ledger.Transaction{tx5, tx4}, - }, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - tc.query.Options.ExpandVolumes = true - tc.query.Options.ExpandEffectiveVolumes = true - - cursor, err := store.GetTransactions(ctx, ledgerstore.NewListTransactionsQuery(tc.query)) - if tc.expectError != nil { - require.True(t, errors.Is(err, tc.expectError)) - } else { - require.NoError(t, err) - require.Len(t, cursor.Data, len(tc.expected)) - RequireEqual(t, tc.expected, cursor.Data) - - count, err := store.CountTransactions(ctx, ledgerstore.NewListTransactionsQuery(tc.query)) - require.NoError(t, err) - - require.EqualValues(t, len(tc.expected), count) - } - }) - } -} diff --git a/internal/storage/ledger/legacy/utils.go b/internal/storage/ledger/legacy/utils.go deleted file mode 100644 index 97c6b51c6..000000000 --- a/internal/storage/ledger/legacy/utils.go +++ /dev/null @@ -1,185 +0,0 @@ -package legacy - -import ( - "context" - "encoding/json" - "fmt" - "github.com/formancehq/go-libs/v2/platform/postgres" - "reflect" - "strings" - - "github.com/formancehq/go-libs/v2/time" - - "github.com/formancehq/go-libs/v2/bun/bunpaginate" - - "github.com/uptrace/bun" -) - -func fetch[T any](s *Store, addModel bool, ctx context.Context, builders ...func(query *bun.SelectQuery) *bun.SelectQuery) (T, error) { - - var ret T - ret = reflect.New(reflect.TypeOf(ret).Elem()).Interface().(T) - - query := s.db.NewSelect() - - if addModel { - query = query.Model(ret) - } - - for _, builder := range builders { - query = query.Apply(builder) - } - - if err := query.Scan(ctx, ret); err != nil { - return ret, postgres.ResolveError(err) - } - - return ret, nil -} - -func paginateWithOffset[FILTERS any, RETURN any](s *Store, ctx context.Context, - q *bunpaginate.OffsetPaginatedQuery[FILTERS], builders ...func(query *bun.SelectQuery) *bun.SelectQuery) (*bunpaginate.Cursor[RETURN], error) { - - query := s.db.NewSelect() - for _, builder := range builders { - query = query.Apply(builder) - } - return bunpaginate.UsingOffset[FILTERS, RETURN](ctx, query, *q) -} - -func paginateWithOffsetWithoutModel[FILTERS any, RETURN any](s *Store, ctx context.Context, - q *bunpaginate.OffsetPaginatedQuery[FILTERS], builders ...func(query *bun.SelectQuery) *bun.SelectQuery) (*bunpaginate.Cursor[RETURN], error) { - - query := s.db.NewSelect() - for _, builder := range builders { - query = query.Apply(builder) - } - - return bunpaginate.UsingOffset[FILTERS, RETURN](ctx, query, *q) -} - -func paginateWithColumn[FILTERS any, RETURN any](s *Store, ctx context.Context, q *bunpaginate.ColumnPaginatedQuery[FILTERS], builders ...func(query *bun.SelectQuery) *bun.SelectQuery) (*bunpaginate.Cursor[RETURN], error) { - query := s.db.NewSelect() - for _, builder := range builders { - query = query.Apply(builder) - } - - ret, err := bunpaginate.UsingColumn[FILTERS, RETURN](ctx, query, *q) - if err != nil { - return nil, postgres.ResolveError(err) - } - - return ret, nil -} - -func count[T any](s *Store, addModel bool, ctx context.Context, builders ...func(query *bun.SelectQuery) *bun.SelectQuery) (int, error) { - query := s.db.NewSelect() - if addModel { - query = query.Model((*T)(nil)) - } - for _, builder := range builders { - query = query.Apply(builder) - } - return s.db.NewSelect(). - TableExpr("(" + query.String() + ") data"). - Count(ctx) -} - -func filterAccountAddress(address, key string) string { - parts := make([]string, 0) - src := strings.Split(address, ":") - - needSegmentCheck := false - for _, segment := range src { - needSegmentCheck = segment == "" - if needSegmentCheck { - break - } - } - - if needSegmentCheck { - parts = append(parts, fmt.Sprintf("jsonb_array_length(%s_array) = %d", key, len(src))) - - for i, segment := range src { - if len(segment) == 0 { - continue - } - parts = append(parts, fmt.Sprintf("%s_array @@ ('$[%d] == \"%s\"')::jsonpath", key, i, segment)) - } - } else { - parts = append(parts, fmt.Sprintf("%s = '%s'", key, address)) - } - - return strings.Join(parts, " and ") -} - -func filterAccountAddressOnTransactions(address string, source, destination bool) string { - src := strings.Split(address, ":") - - needSegmentCheck := false - for _, segment := range src { - needSegmentCheck = segment == "" - if needSegmentCheck { - break - } - } - - if needSegmentCheck { - m := map[string]any{ - fmt.Sprint(len(src)): nil, - } - parts := make([]string, 0) - - for i, segment := range src { - if len(segment) == 0 { - continue - } - m[fmt.Sprint(i)] = segment - } - - data, err := json.Marshal([]any{m}) - if err != nil { - panic(err) - } - - if source { - parts = append(parts, fmt.Sprintf("sources_arrays @> '%s'", string(data))) - } - if destination { - parts = append(parts, fmt.Sprintf("destinations_arrays @> '%s'", string(data))) - } - return strings.Join(parts, " or ") - } else { - data, err := json.Marshal([]string{address}) - if err != nil { - panic(err) - } - - parts := make([]string, 0) - if source { - parts = append(parts, fmt.Sprintf("sources @> '%s'", string(data))) - } - if destination { - parts = append(parts, fmt.Sprintf("destinations @> '%s'", string(data))) - } - return strings.Join(parts, " or ") - } -} - -func filterPIT(pit *time.Time, column string) func(query *bun.SelectQuery) *bun.SelectQuery { - return func(query *bun.SelectQuery) *bun.SelectQuery { - if pit == nil || pit.IsZero() { - return query - } - return query.Where(fmt.Sprintf("%s <= ?", column), pit) - } -} - -func filterOOT(oot *time.Time, column string) func(query *bun.SelectQuery) *bun.SelectQuery { - return func(query *bun.SelectQuery) *bun.SelectQuery { - if oot == nil || oot.IsZero() { - return query - } - return query.Where(fmt.Sprintf("%s >= ?", column), oot) - } -} diff --git a/internal/storage/ledger/legacy/volumes.go b/internal/storage/ledger/legacy/volumes.go deleted file mode 100644 index c427227c6..000000000 --- a/internal/storage/ledger/legacy/volumes.go +++ /dev/null @@ -1,188 +0,0 @@ -package legacy - -import ( - "context" - "fmt" - ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" - "regexp" - - "github.com/formancehq/go-libs/v2/bun/bunpaginate" - lquery "github.com/formancehq/go-libs/v2/query" - ledger "github.com/formancehq/ledger/internal" - "github.com/uptrace/bun" -) - -func (store *Store) volumesQueryContext(q GetVolumesWithBalancesQuery) (string, []any, bool, error) { - - metadataRegex := regexp.MustCompile(`metadata\[(.+)]`) - balanceRegex := regexp.MustCompile(`balance\[(.*)]`) - var ( - subQuery string - args []any - err error - ) - - var useMetadata = false - - if q.Options.QueryBuilder != nil { - subQuery, args, err = q.Options.QueryBuilder.Build(lquery.ContextFn(func(key, operator string, value any) (string, []any, error) { - - convertOperatorToSQL := func() string { - switch operator { - case "$match": - return "=" - case "$lt": - return "<" - case "$gt": - return ">" - case "$lte": - return "<=" - case "$gte": - return ">=" - } - panic("unreachable") - } - - switch { - case key == "account" || key == "address": - if operator != "$match" { - return "", nil, newErrInvalidQuery("'%s' column can only be used with $match", key) - } - - switch address := value.(type) { - case string: - return filterAccountAddress(address, "accounts_address"), nil, nil - default: - return "", nil, newErrInvalidQuery("unexpected type %T for column 'address'", address) - } - case metadataRegex.Match([]byte(key)): - if operator != "$match" { - return "", nil, newErrInvalidQuery("'metadata' column can only be used with $match") - } - useMetadata = true - match := metadataRegex.FindAllStringSubmatch(key, 3) - key := "metadata" - - return key + " @> ?", []any{map[string]any{ - match[0][1]: value, - }}, nil - case key == "metadata": - if operator != "$exists" { - return "", nil, newErrInvalidQuery("'metadata' key filter can only be used with $exists") - } - useMetadata = true - key := "metadata" - - return fmt.Sprintf("%s -> ? IS NOT NULL", key), []any{value}, nil - case balanceRegex.Match([]byte(key)): - match := balanceRegex.FindAllStringSubmatch(key, 2) - return fmt.Sprintf(`balance %s ? and asset = ?`, convertOperatorToSQL()), []any{value, match[0][1]}, nil - default: - return "", nil, newErrInvalidQuery("unknown key '%s' when building query", key) - } - })) - if err != nil { - return "", nil, false, err - } - } - - return subQuery, args, useMetadata, nil - -} - -func (store *Store) buildVolumesWithBalancesQuery(query *bun.SelectQuery, q GetVolumesWithBalancesQuery, where string, args []any, useMetadata bool) *bun.SelectQuery { - - filtersForVolumes := q.Options.Options - dateFilterColumn := "effective_date" - - if filtersForVolumes.UseInsertionDate { - dateFilterColumn = "insertion_date" - } - - selectAccounts := store.GetDB().NewSelect(). - Column("accounts_address_array"). - Column("accounts_address"). - Column("accounts_seq"). - Column("asset"). - Column("ledger"). - ColumnExpr("sum(case when not is_source then amount else 0 end) as input"). - ColumnExpr("sum(case when is_source then amount else 0 end) as output"). - ColumnExpr("sum(case when not is_source then amount else -amount end) as balance"). - ModelTableExpr(store.GetPrefixedRelationName("moves")). - Group("ledger", "accounts_seq", "accounts_address", "accounts_address_array", "asset"). - Apply(filterPIT(filtersForVolumes.PIT, dateFilterColumn)). - Apply(filterOOT(filtersForVolumes.OOT, dateFilterColumn)) - - query = query. - TableExpr("(?) accountsWithVolumes", selectAccounts). - Column( - "accounts_address", - "accounts_address_array", - "accounts_seq", - "ledger", - "asset", - "input", - "output", - "balance", - ) - - if useMetadata { - query = query. - ColumnExpr("accounts_metadata.metadata as metadata"). - Join(`join lateral ( - select metadata - from ` + store.GetPrefixedRelationName("accounts") + ` a - where a.seq = accountsWithVolumes.accounts_seq - ) accounts_metadata on true`, - ) - } - - query = query. - Where("ledger = ?", store.name) - - globalQuery := query.NewSelect() - globalQuery = globalQuery. - With("query", query). - ModelTableExpr("query") - - if where != "" { - globalQuery.Where(where, args...) - } - - if filtersForVolumes.GroupLvl > 0 { - globalQuery = globalQuery. - ColumnExpr(fmt.Sprintf(`(array_to_string((string_to_array(accounts_address, ':'))[1:LEAST(array_length(string_to_array(accounts_address, ':'),1),%d)],':')) as account`, filtersForVolumes.GroupLvl)). - ColumnExpr("asset"). - ColumnExpr("sum(input) as input"). - ColumnExpr("sum(output) as output"). - ColumnExpr("sum(balance) as balance"). - GroupExpr("account, asset") - } else { - globalQuery = globalQuery.ColumnExpr("accounts_address as account, asset, input, output, balance") - } - globalQuery = globalQuery.Order("account", "asset") - - return globalQuery -} - -func (store *Store) GetVolumesWithBalances(ctx context.Context, q GetVolumesWithBalancesQuery) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) { - var ( - where string - args []any - err error - useMetadata bool - ) - if q.Options.QueryBuilder != nil { - where, args, useMetadata, err = store.volumesQueryContext(q) - if err != nil { - return nil, err - } - } - - return paginateWithOffsetWithoutModel[ledgercontroller.PaginatedQueryOptions[FiltersForVolumes], ledger.VolumesWithBalanceByAssetByAccount]( - store, ctx, (*bunpaginate.OffsetPaginatedQuery[ledgercontroller.PaginatedQueryOptions[FiltersForVolumes]])(&q), - func(query *bun.SelectQuery) *bun.SelectQuery { - return store.buildVolumesWithBalancesQuery(query, q, where, args, useMetadata) - }, - ) -} diff --git a/internal/storage/ledger/legacy/volumes_test.go b/internal/storage/ledger/legacy/volumes_test.go deleted file mode 100644 index 4f5553167..000000000 --- a/internal/storage/ledger/legacy/volumes_test.go +++ /dev/null @@ -1,675 +0,0 @@ -//go:build it - -package legacy_test - -import ( - "github.com/formancehq/go-libs/v2/pointer" - ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" - ledgerstore "github.com/formancehq/ledger/internal/storage/ledger/legacy" - "math/big" - "testing" - - "github.com/formancehq/go-libs/v2/time" - - "github.com/formancehq/go-libs/v2/logging" - - "github.com/formancehq/go-libs/v2/metadata" - "github.com/formancehq/go-libs/v2/query" - ledger "github.com/formancehq/ledger/internal" - "github.com/stretchr/testify/require" -) - -func TestVolumesList(t *testing.T) { - t.Parallel() - store := newLedgerStore(t) - now := time.Now() - ctx := logging.TestingContext() - - previousPIT := now.Add(-2 * time.Minute) - futurPIT := now.Add(2 * time.Minute) - - previousOOT := now.Add(-2 * time.Minute) - futurOOT := now.Add(2 * time.Minute) - - require.NoError(t, store.newStore.UpdateAccountsMetadata(ctx, map[string]metadata.Metadata{ - "account:1": { - "category": "1", - }, - "account:2": { - "category": "2", - }, - "world": { - "foo": "bar", - }, - })) - - tx1 := ledger.NewTransaction(). - WithPostings(ledger.NewPosting("world", "account:1", "USD", big.NewInt(100))). - WithTimestamp(now.Add(-4 * time.Minute)). - WithInsertedAt(now.Add(4 * time.Minute)) - err := store.newStore.CommitTransaction(ctx, &tx1) - require.NoError(t, err) - - tx2 := ledger.NewTransaction(). - WithPostings(ledger.NewPosting("world", "account:1", "USD", big.NewInt(100))). - WithTimestamp(now.Add(-3 * time.Minute)). - WithInsertedAt(now.Add(3 * time.Minute)) - err = store.newStore.CommitTransaction(ctx, &tx2) - require.NoError(t, err) - - tx3 := ledger.NewTransaction(). - WithPostings(ledger.NewPosting("account:1", "bank", "USD", big.NewInt(50))). - WithTimestamp(now.Add(-2 * time.Minute)). - WithInsertedAt(now.Add(2 * time.Minute)) - err = store.newStore.CommitTransaction(ctx, &tx3) - require.NoError(t, err) - - tx4 := ledger.NewTransaction(). - WithPostings(ledger.NewPosting("world", "account:1", "USD", big.NewInt(0))). - WithTimestamp(now.Add(-time.Minute)). - WithInsertedAt(now.Add(time.Minute)) - err = store.newStore.CommitTransaction(ctx, &tx4) - require.NoError(t, err) - - tx5 := ledger.NewTransaction(). - WithPostings(ledger.NewPosting("world", "account:2", "USD", big.NewInt(50))). - WithTimestamp(now). - WithInsertedAt(now) - err = store.newStore.CommitTransaction(ctx, &tx5) - require.NoError(t, err) - - tx6 := ledger.NewTransaction(). - WithPostings(ledger.NewPosting("world", "account:2", "USD", big.NewInt(50))). - WithTimestamp(now.Add(1 * time.Minute)). - WithInsertedAt(now.Add(-time.Minute)) - err = store.newStore.CommitTransaction(ctx, &tx6) - require.NoError(t, err) - - tx7 := ledger.NewTransaction(). - WithPostings(ledger.NewPosting("account:2", "bank", "USD", big.NewInt(50))). - WithTimestamp(now.Add(2 * time.Minute)). - WithInsertedAt(now.Add(-2 * time.Minute)) - err = store.newStore.CommitTransaction(ctx, &tx7) - require.NoError(t, err) - - tx8 := ledger.NewTransaction(). - WithPostings(ledger.NewPosting("world", "account:2", "USD", big.NewInt(25))). - WithTimestamp(now.Add(3 * time.Minute)). - WithInsertedAt(now.Add(-3 * time.Minute)) - err = store.newStore.CommitTransaction(ctx, &tx8) - require.NoError(t, err) - - t.Run("Get all volumes with balance for insertion date", func(t *testing.T) { - t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.FiltersForVolumes{UseInsertionDate: true}))) - require.NoError(t, err) - - require.Len(t, volumes.Data, 4) - }) - - t.Run("Get all volumes with balance for effective date", func(t *testing.T) { - t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions(ledgerstore.FiltersForVolumes{UseInsertionDate: false}))) - require.NoError(t, err) - - require.Len(t, volumes.Data, 4) - }) - - t.Run("Get all volumes with balance for insertion date with previous pit", func(t *testing.T) { - t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgerstore.FiltersForVolumes{ - PITFilter: ledgerstore.PITFilter{PIT: &previousPIT, OOT: nil}, - UseInsertionDate: true, - }))) - - require.NoError(t, err) - require.Len(t, volumes.Data, 3) - require.Equal(t, ledger.VolumesWithBalanceByAssetByAccount{ - Account: "account:2", - Asset: "USD", - VolumesWithBalance: ledger.VolumesWithBalance{ - Input: big.NewInt(25), - Output: big.NewInt(50), - Balance: big.NewInt(-25), - }, - }, volumes.Data[0]) - }) - - t.Run("Get all volumes with balance for insertion date with futur pit", func(t *testing.T) { - t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgerstore.FiltersForVolumes{ - PITFilter: ledgerstore.PITFilter{PIT: &futurPIT, OOT: nil}, - UseInsertionDate: true, - }))) - require.NoError(t, err) - - require.Len(t, volumes.Data, 4) - }) - - t.Run("Get all volumes with balance for insertion date with previous oot", func(t *testing.T) { - t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgerstore.FiltersForVolumes{ - PITFilter: ledgerstore.PITFilter{PIT: nil, OOT: &previousOOT}, - UseInsertionDate: true, - }))) - require.NoError(t, err) - - require.Len(t, volumes.Data, 4) - }) - - t.Run("Get all volumes with balance for insertion date with future oot", func(t *testing.T) { - t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgerstore.FiltersForVolumes{ - PITFilter: ledgerstore.PITFilter{PIT: nil, OOT: &futurOOT}, - UseInsertionDate: true, - }))) - - require.NoError(t, err) - require.Len(t, volumes.Data, 3) - require.Equal(t, ledger.VolumesWithBalanceByAssetByAccount{ - Account: "account:1", - Asset: "USD", - VolumesWithBalance: ledger.VolumesWithBalance{ - Input: big.NewInt(200), - Output: big.NewInt(50), - Balance: big.NewInt(150), - }, - }, volumes.Data[0]) - }) - - t.Run("Get all volumes with balance for effective date with previous pit", func(t *testing.T) { - t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgerstore.FiltersForVolumes{ - PITFilter: ledgerstore.PITFilter{PIT: &previousPIT, OOT: nil}, - UseInsertionDate: false, - }))) - - require.NoError(t, err) - require.Len(t, volumes.Data, 3) - require.Equal(t, ledger.VolumesWithBalanceByAssetByAccount{ - Account: "account:1", - Asset: "USD", - VolumesWithBalance: ledger.VolumesWithBalance{ - Input: big.NewInt(200), - Output: big.NewInt(50), - Balance: big.NewInt(150), - }, - }, volumes.Data[0]) - }) - - t.Run("Get all volumes with balance for effective date with futur pit", func(t *testing.T) { - t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgerstore.FiltersForVolumes{ - PITFilter: ledgerstore.PITFilter{PIT: &futurPIT, OOT: nil}, - UseInsertionDate: false, - }))) - require.NoError(t, err) - - require.Len(t, volumes.Data, 4) - }) - - t.Run("Get all volumes with balance for effective date with previous oot", func(t *testing.T) { - t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgerstore.FiltersForVolumes{ - PITFilter: ledgerstore.PITFilter{PIT: nil, OOT: &previousOOT}, - UseInsertionDate: false, - }))) - require.NoError(t, err) - - require.Len(t, volumes.Data, 4) - }) - - t.Run("Get all volumes with balance for effective date with futur oot", func(t *testing.T) { - t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgerstore.FiltersForVolumes{ - PITFilter: ledgerstore.PITFilter{PIT: nil, OOT: &futurOOT}, - UseInsertionDate: false, - }))) - - require.NoError(t, err) - require.Len(t, volumes.Data, 3) - require.Equal(t, ledger.VolumesWithBalanceByAssetByAccount{ - Account: "account:2", - Asset: "USD", - VolumesWithBalance: ledger.VolumesWithBalance{ - Input: big.NewInt(25), - Output: big.NewInt(50), - Balance: big.NewInt(-25), - }, - }, volumes.Data[0]) - }) - - t.Run("Get all volumes with balance for insertion date with future PIT and now OOT", func(t *testing.T) { - t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgerstore.FiltersForVolumes{ - PITFilter: ledgerstore.PITFilter{PIT: &futurPIT, OOT: &now}, - UseInsertionDate: true, - }))) - - require.NoError(t, err) - require.Len(t, volumes.Data, 4) - require.Equal(t, ledger.VolumesWithBalanceByAssetByAccount{ - Account: "account:1", - Asset: "USD", - VolumesWithBalance: ledger.VolumesWithBalance{ - Input: big.NewInt(0), - Output: big.NewInt(50), - Balance: big.NewInt(-50), - }, - }, volumes.Data[0]) - - }) - - t.Run("Get all volumes with balance for insertion date with previous OOT and now PIT", func(t *testing.T) { - t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgerstore.FiltersForVolumes{ - PITFilter: ledgerstore.PITFilter{PIT: &now, OOT: &previousOOT}, - UseInsertionDate: true, - }))) - - require.NoError(t, err) - require.Len(t, volumes.Data, 3) - require.Equal(t, ledger.VolumesWithBalanceByAssetByAccount{ - Account: "account:2", - Asset: "USD", - VolumesWithBalance: ledger.VolumesWithBalance{ - Input: big.NewInt(100), - Output: big.NewInt(50), - Balance: big.NewInt(50), - }, - }, volumes.Data[0]) - - }) - - t.Run("Get all volumes with balance for effective date with future PIT and now OOT", func(t *testing.T) { - t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgerstore.FiltersForVolumes{ - PITFilter: ledgerstore.PITFilter{PIT: &futurPIT, OOT: &now}, - UseInsertionDate: false, - }))) - - require.NoError(t, err) - require.Len(t, volumes.Data, 3) - require.Equal(t, ledger.VolumesWithBalanceByAssetByAccount{ - Account: "account:2", - Asset: "USD", - VolumesWithBalance: ledger.VolumesWithBalance{ - Input: big.NewInt(100), - Output: big.NewInt(50), - Balance: big.NewInt(50), - }, - }, volumes.Data[0]) - }) - - t.Run("Get all volumes with balance for insertion date with previous OOT and now PIT", func(t *testing.T) { - t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery(ledgercontroller.NewPaginatedQueryOptions( - ledgerstore.FiltersForVolumes{ - PITFilter: ledgerstore.PITFilter{PIT: &now, OOT: &previousOOT}, - UseInsertionDate: false, - }))) - - require.NoError(t, err) - require.Len(t, volumes.Data, 4) - require.Equal(t, ledger.VolumesWithBalanceByAssetByAccount{ - Account: "account:1", - Asset: "USD", - VolumesWithBalance: ledger.VolumesWithBalance{ - Input: big.NewInt(0), - Output: big.NewInt(50), - Balance: big.NewInt(-50), - }, - }, volumes.Data[0]) - - }) - - t.Run("Get account1 volume and Balance for insertion date with previous OOT and now PIT", func(t *testing.T) { - t.Parallel() - - volumes, err := store.GetVolumesWithBalances(ctx, - ledgerstore.NewGetVolumesWithBalancesQuery( - ledgercontroller.NewPaginatedQueryOptions( - ledgerstore.FiltersForVolumes{ - PITFilter: ledgerstore.PITFilter{PIT: &now, OOT: &previousOOT}, - UseInsertionDate: false, - }).WithQueryBuilder(query.Match("account", "account:1"))), - ) - - require.NoError(t, err) - require.Len(t, volumes.Data, 1) - require.Equal(t, ledger.VolumesWithBalanceByAssetByAccount{ - Account: "account:1", - Asset: "USD", - VolumesWithBalance: ledger.VolumesWithBalance{ - Input: big.NewInt(0), - Output: big.NewInt(50), - Balance: big.NewInt(-50), - }, - }, volumes.Data[0]) - - }) - - t.Run("Using Metadata regex", func(t *testing.T) { - t.Parallel() - - volumes, err := store.GetVolumesWithBalances(ctx, - ledgerstore.NewGetVolumesWithBalancesQuery( - ledgercontroller.NewPaginatedQueryOptions( - ledgerstore.FiltersForVolumes{}).WithQueryBuilder(query.Match("metadata[foo]", "bar"))), - ) - - require.NoError(t, err) - require.Len(t, volumes.Data, 1) - - }) - - t.Run("Using exists metadata filter 1", func(t *testing.T) { - t.Parallel() - - volumes, err := store.GetVolumesWithBalances(ctx, - ledgerstore.NewGetVolumesWithBalancesQuery( - ledgercontroller.NewPaginatedQueryOptions( - ledgerstore.FiltersForVolumes{}).WithQueryBuilder(query.Exists("metadata", "category"))), - ) - - require.NoError(t, err) - require.Len(t, volumes.Data, 2) - }) - - t.Run("Using exists metadata filter 2", func(t *testing.T) { - t.Parallel() - - volumes, err := store.GetVolumesWithBalances(ctx, - ledgerstore.NewGetVolumesWithBalancesQuery( - ledgercontroller.NewPaginatedQueryOptions( - ledgerstore.FiltersForVolumes{}).WithQueryBuilder(query.Exists("metadata", "foo"))), - ) - - require.NoError(t, err) - require.Len(t, volumes.Data, 1) - }) -} - -func TestVolumesAggregate(t *testing.T) { - t.Parallel() - store := newLedgerStore(t) - now := time.Now() - ctx := logging.TestingContext() - - pit := now.Add(2 * time.Minute) - oot := now.Add(-2 * time.Minute) - - tx1 := ledger.NewTransaction(). - WithPostings(ledger.NewPosting("world", "account:1:2", "USD", big.NewInt(100))). - WithTimestamp(now.Add(-4 * time.Minute)). - WithInsertedAt(now) - err := store.newStore.CommitTransaction(ctx, &tx1) - require.NoError(t, err) - - tx2 := ledger.NewTransaction(). - WithPostings(ledger.NewPosting("world", "account:1:1", "EUR", big.NewInt(100))). - WithTimestamp(now.Add(-3 * time.Minute)) - err = store.newStore.CommitTransaction(ctx, &tx2) - require.NoError(t, err) - - tx3 := ledger.NewTransaction(). - WithPostings(ledger.NewPosting("world", "account:1:2", "EUR", big.NewInt(50))). - WithTimestamp(now.Add(-2 * time.Minute)) - err = store.newStore.CommitTransaction(ctx, &tx3) - require.NoError(t, err) - - tx4 := ledger.NewTransaction(). - WithPostings(ledger.NewPosting("world", "account:1:3", "USD", big.NewInt(0))). - WithTimestamp(now.Add(-time.Minute)) - err = store.newStore.CommitTransaction(ctx, &tx4) - require.NoError(t, err) - - tx5 := ledger.NewTransaction(). - WithPostings(ledger.NewPosting("world", "account:2:1", "USD", big.NewInt(50))). - WithTimestamp(now) - err = store.newStore.CommitTransaction(ctx, &tx5) - require.NoError(t, err) - - tx6 := ledger.NewTransaction(). - WithPostings(ledger.NewPosting("world", "account:2:2", "USD", big.NewInt(50))). - WithTimestamp(now.Add(1 * time.Minute)) - err = store.newStore.CommitTransaction(ctx, &tx6) - require.NoError(t, err) - - tx7 := ledger.NewTransaction(). - WithPostings(ledger.NewPosting("world", "account:2:3", "EUR", big.NewInt(25))). - WithTimestamp(now.Add(3 * time.Minute)) - err = store.newStore.CommitTransaction(ctx, &tx7) - require.NoError(t, err) - - require.NoError(t, store.newStore.UpdateAccountsMetadata(ctx, map[string]metadata.Metadata{ - "account:1:1": { - "foo": "bar", - }, - })) - - t.Run("Aggregation Volumes with balance for GroupLvl 0", func(t *testing.T) { - t.Parallel() - - volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery( - ledgercontroller.NewPaginatedQueryOptions( - ledgerstore.FiltersForVolumes{ - UseInsertionDate: true, - GroupLvl: 0, - }).WithQueryBuilder(query.Match("account", "account::")))) - - require.NoError(t, err) - require.Len(t, volumes.Data, 7) - }) - - t.Run("Aggregation Volumes with balance for GroupLvl 1", func(t *testing.T) { - t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery( - ledgercontroller.NewPaginatedQueryOptions( - ledgerstore.FiltersForVolumes{ - UseInsertionDate: true, - GroupLvl: 1, - }).WithQueryBuilder(query.Match("account", "account::")))) - - require.NoError(t, err) - require.Len(t, volumes.Data, 2) - }) - - t.Run("Aggregation Volumes with balance for GroupLvl 2", func(t *testing.T) { - t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery( - ledgercontroller.NewPaginatedQueryOptions( - ledgerstore.FiltersForVolumes{ - UseInsertionDate: true, - GroupLvl: 2, - }).WithQueryBuilder(query.Match("account", "account::")))) - - require.NoError(t, err) - require.Len(t, volumes.Data, 4) - }) - - t.Run("Aggregation Volumes with balance for GroupLvl 3", func(t *testing.T) { - t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery( - ledgercontroller.NewPaginatedQueryOptions( - ledgerstore.FiltersForVolumes{ - UseInsertionDate: true, - GroupLvl: 3, - }).WithQueryBuilder(query.Match("account", "account::")))) - - require.NoError(t, err) - require.Len(t, volumes.Data, 7) - }) - - t.Run("Aggregation Volumes with balance for GroupLvl 1 && PIT && OOT && effectiveDate", func(t *testing.T) { - t.Parallel() - - volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery( - ledgercontroller.NewPaginatedQueryOptions( - ledgerstore.FiltersForVolumes{ - PITFilter: ledgerstore.PITFilter{ - PIT: &pit, - OOT: &oot, - }, - GroupLvl: 1, - }).WithQueryBuilder(query.Match("account", "account::")))) - - require.NoError(t, err) - require.Len(t, volumes.Data, 2) - require.Equal(t, volumes.Data[0], ledger.VolumesWithBalanceByAssetByAccount{ - Account: "account", - Asset: "EUR", - VolumesWithBalance: ledger.VolumesWithBalance{ - Input: big.NewInt(50), - Output: big.NewInt(0), - Balance: big.NewInt(50), - }, - }) - require.Equal(t, volumes.Data[1], ledger.VolumesWithBalanceByAssetByAccount{ - Account: "account", - Asset: "USD", - VolumesWithBalance: ledger.VolumesWithBalance{ - Input: big.NewInt(100), - Output: big.NewInt(0), - Balance: big.NewInt(100), - }, - }) - }) - - t.Run("Aggregation Volumes with balance for GroupLvl 1 && PIT && OOT && effectiveDate && Balance Filter 1", func(t *testing.T) { - t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery( - ledgercontroller.NewPaginatedQueryOptions( - ledgerstore.FiltersForVolumes{ - PITFilter: ledgerstore.PITFilter{ - PIT: &pit, - OOT: &oot, - }, - UseInsertionDate: false, - GroupLvl: 1, - }).WithQueryBuilder( - query.And(query.Match("account", "account::"), query.Gte("balance[EUR]", 50))))) - - require.NoError(t, err) - require.Len(t, volumes.Data, 1) - require.Equal(t, volumes.Data[0], ledger.VolumesWithBalanceByAssetByAccount{ - Account: "account", - Asset: "EUR", - VolumesWithBalance: ledger.VolumesWithBalance{ - Input: big.NewInt(50), - Output: big.NewInt(0), - Balance: big.NewInt(50), - }, - }) - }) - - t.Run("Aggregation Volumes with balance for GroupLvl 1 && Balance Filter 2", func(t *testing.T) { - t.Parallel() - volumes, err := store.GetVolumesWithBalances(ctx, ledgerstore.NewGetVolumesWithBalancesQuery( - ledgercontroller.NewPaginatedQueryOptions( - ledgerstore.FiltersForVolumes{ - PITFilter: ledgerstore.PITFilter{}, - UseInsertionDate: true, - GroupLvl: 2, - }).WithQueryBuilder( - query.Or( - query.Match("account", "account:1:"), - query.Lte("balance[USD]", 0))))) - - require.NoError(t, err) - require.Len(t, volumes.Data, 3) - require.Equal(t, volumes.Data[0], ledger.VolumesWithBalanceByAssetByAccount{ - Account: "account:1", - Asset: "EUR", - VolumesWithBalance: ledger.VolumesWithBalance{ - Input: big.NewInt(150), - Output: big.NewInt(0), - Balance: big.NewInt(150), - }, - }) - require.Equal(t, volumes.Data[1], ledger.VolumesWithBalanceByAssetByAccount{ - Account: "account:1", - Asset: "USD", - VolumesWithBalance: ledger.VolumesWithBalance{ - Input: big.NewInt(100), - Output: big.NewInt(0), - Balance: big.NewInt(100), - }, - }) - require.Equal(t, volumes.Data[2], ledger.VolumesWithBalanceByAssetByAccount{ - Account: "world", - Asset: "USD", - VolumesWithBalance: ledger.VolumesWithBalance{ - Input: big.NewInt(0), - Output: big.NewInt(200), - Balance: big.NewInt(-200), - }, - }) - }) - t.Run("filter using account matching, metadata, and group", func(t *testing.T) { - t.Parallel() - - volumes, err := store.GetVolumesWithBalances(ctx, - ledgerstore.NewGetVolumesWithBalancesQuery( - ledgercontroller.NewPaginatedQueryOptions( - ledgerstore.FiltersForVolumes{ - GroupLvl: 1, - }).WithQueryBuilder(query.And( - query.Match("account", "account::"), - query.Match("metadata[foo]", "bar"), - ))), - ) - - require.NoError(t, err) - require.Len(t, volumes.Data, 1) - }) - - t.Run("filter using account matching, metadata, and group and PIT", func(t *testing.T) { - t.Parallel() - - volumes, err := store.GetVolumesWithBalances(ctx, - ledgerstore.NewGetVolumesWithBalancesQuery( - ledgercontroller.NewPaginatedQueryOptions( - ledgerstore.FiltersForVolumes{ - GroupLvl: 1, - PITFilter: ledgerstore.PITFilter{ - PIT: pointer.For(now.Add(time.Minute)), - }, - }).WithQueryBuilder(query.And( - query.Match("account", "account::"), - query.Match("metadata[foo]", "bar"), - ))), - ) - - require.NoError(t, err) - require.Len(t, volumes.Data, 1) - }) - - t.Run("filter using metadata matching only", func(t *testing.T) { - t.Parallel() - - volumes, err := store.GetVolumesWithBalances(ctx, - ledgerstore.NewGetVolumesWithBalancesQuery( - ledgercontroller.NewPaginatedQueryOptions( - ledgerstore.FiltersForVolumes{ - GroupLvl: 1, - }).WithQueryBuilder(query.And( - query.Match("metadata[foo]", "bar"), - ))), - ) - - require.NoError(t, err) - require.Len(t, volumes.Data, 1) - }) -} diff --git a/internal/storage/ledger/resource_aggregated_balances.go b/internal/storage/ledger/resource_aggregated_balances.go index b5a32370c..f1cbe204f 100644 --- a/internal/storage/ledger/resource_aggregated_balances.go +++ b/internal/storage/ledger/resource_aggregated_balances.go @@ -166,7 +166,7 @@ func (h aggregatedBalancesResourceRepositoryHandler) project( return store.db.NewSelect(). TableExpr("(?) values", sumVolumesForAsset). - ColumnExpr("aggregate_objects(json_build_object(asset, volumes)::jsonb) as aggregated"), nil + ColumnExpr("public.aggregate_objects(json_build_object(asset, volumes)::jsonb) as aggregated"), nil } var _ repositoryHandler[ledgercontroller.GetAggregatedVolumesOptions] = aggregatedBalancesResourceRepositoryHandler{} diff --git a/internal/storage/system/migrations.go b/internal/storage/system/migrations.go index 0de325056..f096d202c 100644 --- a/internal/storage/system/migrations.go +++ b/internal/storage/system/migrations.go @@ -215,6 +215,18 @@ func GetMigrator(db *bun.DB, options ...migrations.Option) *migrations.Migrator }) }, }, + migrations.Migration{ + Name: "set default metadata on ledgers", + Up: func(ctx context.Context, db bun.IDB) error { + return db.RunInTx(ctx, &sql.TxOptions{}, func(ctx context.Context, tx bun.Tx) error { + _, err := tx.ExecContext(ctx, ` + alter table _system.ledgers + alter column metadata set default '{}'::jsonb; + `) + return err + }) + }, + }, ) return migrator From 9db25769d40b860f7fc558e69b55dca5d03e4883 Mon Sep 17 00:00:00 2001 From: Ragot Geoffrey Date: Fri, 3 Jan 2025 16:18:33 +0100 Subject: [PATCH 70/71] chore: upgrade dependencies (#636) * chore: upgrade dependencies * fix: pre commit --- go.mod | 34 +++---- go.sum | 68 +++++++------- internal/README.md | 198 ++++++++++++++++++++--------------------- internal/doc.go | 2 +- tools/generator/go.mod | 10 +-- tools/generator/go.sum | 56 ++++++------ 6 files changed, 184 insertions(+), 184 deletions(-) diff --git a/go.mod b/go.mod index cc9c8f740..2b05979a5 100644 --- a/go.mod +++ b/go.mod @@ -14,19 +14,19 @@ require ( github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 github.com/bluele/gcache v0.0.2 github.com/dop251/goja v0.0.0-20241009100908-5f46f2705ca3 - github.com/formancehq/go-libs/v2 v2.0.1-0.20241121194732-b79c48b683f2 + github.com/formancehq/go-libs/v2 v2.0.1-0.20250101192540-cfa76c1dedeb github.com/formancehq/ledger/pkg/client v0.0.0-00010101000000-000000000000 github.com/go-chi/chi/v5 v5.2.0 github.com/go-chi/cors v1.2.1 github.com/google/go-cmp v0.6.0 github.com/google/uuid v1.6.0 - github.com/invopop/jsonschema v0.12.0 - github.com/jackc/pgx/v5 v5.7.1 + github.com/invopop/jsonschema v0.13.0 + github.com/jackc/pgx/v5 v5.7.2 github.com/jamiealquiza/tachymeter v2.0.0+incompatible github.com/logrusorgru/aurora v2.0.3+incompatible github.com/nats-io/nats.go v1.38.0 - github.com/onsi/ginkgo/v2 v2.22.0 - github.com/onsi/gomega v1.35.1 + github.com/onsi/ginkgo/v2 v2.22.1 + github.com/onsi/gomega v1.36.2 github.com/ory/dockertest/v3 v3.11.0 github.com/pborman/uuid v1.2.1 github.com/shomali11/xsql v0.0.0-20190608141458-bf76292144df @@ -34,8 +34,8 @@ require ( github.com/spf13/pflag v1.0.5 github.com/stoewer/go-strcase v1.3.0 github.com/stretchr/testify v1.10.0 - github.com/uptrace/bun v1.2.6 - github.com/uptrace/bun/dialect/pgdialect v1.2.6 + github.com/uptrace/bun v1.2.7 + github.com/uptrace/bun/dialect/pgdialect v1.2.7 github.com/uptrace/bun/extra/bundebug v1.2.5 github.com/xeipuuv/gojsonschema v1.2.0 github.com/xo/dburl v0.23.2 @@ -62,7 +62,7 @@ require ( dario.cat/mergo v1.0.1 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect - github.com/IBM/sarama v1.43.3 // indirect + github.com/IBM/sarama v1.44.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/ThreeDotsLabs/watermill-http/v2 v2.3.1 // indirect @@ -115,7 +115,7 @@ require ( github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect + github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/schema v1.4.1 // indirect @@ -147,8 +147,8 @@ require ( github.com/moby/term v0.5.0 // indirect github.com/muhlemmer/gu v0.3.1 // indirect github.com/muhlemmer/httpforwarded v0.1.0 // indirect - github.com/nats-io/jwt/v2 v2.7.0 // indirect - github.com/nats-io/nats-server/v2 v2.10.22 // indirect + github.com/nats-io/jwt/v2 v2.7.3 // indirect + github.com/nats-io/nats-server/v2 v2.10.24 // indirect github.com/nats-io/nkeys v0.4.9 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/oklog/ulid v1.3.1 // indirect @@ -162,13 +162,13 @@ require ( github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect github.com/riandyrn/otelchi v0.11.0 // indirect github.com/rs/cors v1.11.1 // indirect - github.com/shirou/gopsutil/v4 v4.24.11 // indirect + github.com/shirou/gopsutil/v4 v4.24.12 // indirect github.com/shomali11/util v0.0.0-20220717175126-f0771b70947f // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/tklauser/go-sysconf v0.3.14 // indirect github.com/tklauser/numcpus v0.9.0 // indirect github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect - github.com/uptrace/bun/extra/bunotel v1.2.6 // indirect + github.com/uptrace/bun/extra/bunotel v1.2.7 // indirect github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 // indirect github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 // indirect @@ -204,12 +204,12 @@ require ( golang.org/x/net v0.33.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect - golang.org/x/time v0.7.0 // indirect + golang.org/x/time v0.8.0 // indirect golang.org/x/tools v0.28.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20241219192143-6b3ec007d9bb // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241219192143-6b3ec007d9bb // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d // indirect google.golang.org/grpc v1.69.2 // indirect - google.golang.org/protobuf v1.36.0 // indirect + google.golang.org/protobuf v1.36.1 // indirect gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 5db9eaddd..4a509a421 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/IBM/sarama v1.43.3 h1:Yj6L2IaNvb2mRBop39N7mmJAHBVY3dTPncr3qGVkxPA= -github.com/IBM/sarama v1.43.3/go.mod h1:FVIRaLrhK3Cla/9FfRF5X9Zua2KpS3SYIXxhac1H+FQ= +github.com/IBM/sarama v1.44.0 h1:puNKqcScjSAgVLramjsuovZrS0nJZFVsrvuUymkWqhE= +github.com/IBM/sarama v1.44.0/go.mod h1:MxQ9SvGfvKIorbk077Ff6DUnBlGpidiQOtU2vuBaxVw= github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= @@ -104,8 +104,8 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241121194732-b79c48b683f2 h1:JsacSDe6MYGCm04ZY/VJyYTUAWg8jhOBZm6AGyErF/I= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241121194732-b79c48b683f2/go.mod h1:ZGHVcAC54ZbPgeoGKy/d31CHy94RMhHQlX+Vui15ST8= +github.com/formancehq/go-libs/v2 v2.0.1-0.20250101192540-cfa76c1dedeb h1:v471RK/yxiFFUkJUgZ2CJXGl46KMjOg+Tlr0uMTlQJg= +github.com/formancehq/go-libs/v2 v2.0.1-0.20250101192540-cfa76c1dedeb/go.mod h1:s2c9ULpCJnof0wJsLout05Aj7EwYGUByGBWVviptqTE= github.com/formancehq/numscript v0.0.10 h1:ElvYpoayUX5tHtCCR18ihJTjNlHzdkE4M0IqSm9aufg= github.com/formancehq/numscript v0.0.10/go.mod h1:btuSv05cYwi9BvLRxVs5zrunU+O1vTgigG1T6UsawcY= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= @@ -152,8 +152,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -186,14 +186,14 @@ github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/C github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI= -github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= +github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= -github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= +github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= +github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= github.com/jackc/pgxlisten v0.0.0-20241106001234-1d6f6656415c h1:bTgmg761ac9Ki27HoLx8IBvc+T+Qj6eptBpKahKIRT4= github.com/jackc/pgxlisten v0.0.0-20241106001234-1d6f6656415c/go.mod h1:N4E1APLOYrbM11HH5kdqAjDa8RJWVwD3JqWpvH22h64= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= @@ -255,10 +255,10 @@ github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY= github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0= -github.com/nats-io/jwt/v2 v2.7.0 h1:J+ZnaaMGQi3xSB8iOhVM5ipiWCDrQvgEoitTwWFyOYw= -github.com/nats-io/jwt/v2 v2.7.0/go.mod h1:ZdWS1nZa6WMZfFwwgpEaqBV8EPGVgOTDHN/wTbz0Y5A= -github.com/nats-io/nats-server/v2 v2.10.22 h1:Yt63BGu2c3DdMoBZNcR6pjGQwk/asrKU7VX846ibxDA= -github.com/nats-io/nats-server/v2 v2.10.22/go.mod h1:X/m1ye9NYansUXYFrbcDwUi/blHkrgHh2rgCJaakonk= +github.com/nats-io/jwt/v2 v2.7.3 h1:6bNPK+FXgBeAqdj4cYQ0F8ViHRbi7woQLq4W29nUAzE= +github.com/nats-io/jwt/v2 v2.7.3/go.mod h1:GvkcbHhKquj3pkioy5put1wvPxs78UlZ7D/pY+BgZk4= +github.com/nats-io/nats-server/v2 v2.10.24 h1:KcqqQAD0ZZcG4yLxtvSFJY7CYKVYlnlWoAiVZ6i/IY4= +github.com/nats-io/nats-server/v2 v2.10.24/go.mod h1:olvKt8E5ZlnjyqBGbAXtxvSQKsPodISK5Eo/euIta4s= github.com/nats-io/nats.go v1.38.0 h1:A7P+g7Wjp4/NWqDOOP/K6hfhr54DvdDQUznt5JFg9XA= github.com/nats-io/nats.go v1.38.0/go.mod h1:IGUM++TwokGnXPs82/wCuiHS02/aKrdYUQkU8If6yjw= github.com/nats-io/nkeys v0.4.9 h1:qe9Faq2Gxwi6RZnZMXfmGMZkg3afLLOtrU+gDZJ35b0= @@ -267,10 +267,10 @@ github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= -github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= -github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= -github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/onsi/ginkgo/v2 v2.22.1 h1:QW7tbJAUDyVDVOM5dFa7qaybo+CRfR7bemlQUN6Z8aM= +github.com/onsi/ginkgo/v2 v2.22.1/go.mod h1:S6aTpoRsSq2cZOd+pssHAlKW/Q/jZt6cPrPlnj4a1xM= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= @@ -301,8 +301,8 @@ github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWN github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shirou/gopsutil/v4 v4.24.11 h1:WaU9xqGFKvFfsUv94SXcUPD7rCkU0vr/asVdQOBZNj8= -github.com/shirou/gopsutil/v4 v4.24.11/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8= +github.com/shirou/gopsutil/v4 v4.24.12 h1:qvePBOk20e0IKA1QXrIIU+jmk+zEiYVVx06WjBRlZo4= +github.com/shirou/gopsutil/v4 v4.24.12/go.mod h1:DCtMPAad2XceTeIAbGyVfycbYQNBGk2P8cvDi7/VN9o= github.com/shomali11/parallelizer v0.0.0-20220717173222-a6776fbf40a9/go.mod h1:QsLM53l8gzX0sQbOjVir85bzOUucuJEF8JgE39wD7w0= github.com/shomali11/util v0.0.0-20180607005212-e0f70fd665ff/go.mod h1:WWE2GJM9B5UpdOiwH2val10w/pvJ2cUUQOOA/4LgOng= github.com/shomali11/util v0.0.0-20220717175126-f0771b70947f h1:OM0LVaVycWC+/j5Qra7USyCg2sc+shg3KwygAA+pYvA= @@ -344,14 +344,14 @@ github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPD github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI= github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= -github.com/uptrace/bun v1.2.6 h1:lyGBQAhNiClchb97HA2cBnDeRxwTRLhSIgiFPXVisV8= -github.com/uptrace/bun v1.2.6/go.mod h1:xMgnVFf+/5xsrFBU34HjDJmzZnXbVuNEt/Ih56I8qBU= -github.com/uptrace/bun/dialect/pgdialect v1.2.6 h1:iNd1YLx619K+sZK+dRcWPzluurXYK1QwIkp9FEfNB/8= -github.com/uptrace/bun/dialect/pgdialect v1.2.6/go.mod h1:OL7d3qZLxKYP8kxNhMg3IheN1pDR3UScGjoUP+ivxJQ= +github.com/uptrace/bun v1.2.7 h1:rFjJDW9RM+P08FJkwO5xB+cnYSaQAqsAu9LIQH1iEQY= +github.com/uptrace/bun v1.2.7/go.mod h1:tYihS32vC8v3sNzGtakjd2Q5Vye0D9hBR+0MjvmbaQE= +github.com/uptrace/bun/dialect/pgdialect v1.2.7 h1:HvHPbXQ9f9uE7GaNAikb700i67QpJ50kvyyGRmoMDNA= +github.com/uptrace/bun/dialect/pgdialect v1.2.7/go.mod h1:nUgYSlUrZ5F24XO1df1eSlNzsWk6abB8weKSfmGO7is= github.com/uptrace/bun/extra/bundebug v1.2.5 h1:DsI/gl4jvq5tQ84yPqnlRYIQ4U6AouTqxJ8Y5Oijfz4= github.com/uptrace/bun/extra/bundebug v1.2.5/go.mod h1:JFeYvklf5p92ZILXx4siMe2tEVn5JLelww3IGcJb4yA= -github.com/uptrace/bun/extra/bunotel v1.2.6 h1:6m90acv9hsDuTYRo3oiKCWMatGPmi+feKAx8Y/GPj9A= -github.com/uptrace/bun/extra/bunotel v1.2.6/go.mod h1:QGqnFNJ2H88juh7DmgdPJZVN9bSTpj7UaGllSO9JDKk= +github.com/uptrace/bun/extra/bunotel v1.2.7 h1:8UsbMxWJUVZNKGe+fgCSzBJx/a8fXnlOSXq0R9kSGMY= +github.com/uptrace/bun/extra/bunotel v1.2.7/go.mod h1:BXQlhJmxz5ANiOJPqkRowDmALX4SBAeufmD8sL4vmf0= github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 h1:H8wwQwTe5sL6x30z71lUgNiwBdeCHQjrphCfLwqIHGo= github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2/go.mod h1:/kR4beFhlz2g+V5ik8jW+3PMiMQAPt29y6K64NNY53c= github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 h1:ZjUj9BLYf9PEqBn8W/OapxhPjVRdC6CsXTdULHsyk5c= @@ -500,8 +500,8 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= -golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -513,14 +513,14 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/api v0.0.0-20241219192143-6b3ec007d9bb h1:B7GIB7sr443wZ/EAEl7VZjmh1V6qzkt5V+RYcUYtS1U= -google.golang.org/genproto/googleapis/api v0.0.0-20241219192143-6b3ec007d9bb/go.mod h1:E5//3O5ZIG2l71Xnt+P/CYUY8Bxs8E7WMoZ9tlcMbAY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241219192143-6b3ec007d9bb h1:3oy2tynMOP1QbTC0MsNNAV+Se8M2Bd0A5+x1QHyw+pI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241219192143-6b3ec007d9bb/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= +google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d h1:H8tOf8XM88HvKqLTxe755haY6r1fqqzLbEnfrmLXlSA= +google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d/go.mod h1:2v7Z7gP2ZUOGsaFyxATQSRoBnKygqVq2Cwnvom7QiqY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU= google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= -google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ= -google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= +google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/internal/README.md b/internal/README.md index 616e988bf..bdd4b1dcd 100644 --- a/internal/README.md +++ b/internal/README.md @@ -147,7 +147,7 @@ var Zero = big.NewInt(0) ``` -## func ComputeIdempotencyHash +## func [ComputeIdempotencyHash]() ```go func ComputeIdempotencyHash(inputs any) string @@ -156,7 +156,7 @@ func ComputeIdempotencyHash(inputs any) string -## type Account +## type [Account]() @@ -175,7 +175,7 @@ type Account struct { ``` -### func \(Account\) GetAddress +### func \(Account\) [GetAddress]() ```go func (a Account) GetAddress() string @@ -184,7 +184,7 @@ func (a Account) GetAddress() string -## type AccountMetadata +## type [AccountMetadata]() @@ -193,7 +193,7 @@ type AccountMetadata map[string]metadata.Metadata ``` -## type AccountsVolumes +## type [AccountsVolumes]() @@ -209,7 +209,7 @@ type AccountsVolumes struct { ``` -## type AggregatedVolumes +## type [AggregatedVolumes]() @@ -220,7 +220,7 @@ type AggregatedVolumes struct { ``` -## type BalancesByAssets +## type [BalancesByAssets]() @@ -229,7 +229,7 @@ type BalancesByAssets map[string]*big.Int ``` -## type BalancesByAssetsByAccounts +## type [BalancesByAssetsByAccounts]() @@ -238,7 +238,7 @@ type BalancesByAssetsByAccounts map[string]BalancesByAssets ``` -## type Configuration +## type [Configuration]() @@ -251,7 +251,7 @@ type Configuration struct { ``` -### func NewDefaultConfiguration +### func [NewDefaultConfiguration]() ```go func NewDefaultConfiguration() Configuration @@ -260,7 +260,7 @@ func NewDefaultConfiguration() Configuration -### func \(\*Configuration\) SetDefaults +### func \(\*Configuration\) [SetDefaults]() ```go func (c *Configuration) SetDefaults() @@ -269,7 +269,7 @@ func (c *Configuration) SetDefaults() -### func \(\*Configuration\) Validate +### func \(\*Configuration\) [Validate]() ```go func (c *Configuration) Validate() error @@ -278,7 +278,7 @@ func (c *Configuration) Validate() error -## type CreatedTransaction +## type [CreatedTransaction]() @@ -290,7 +290,7 @@ type CreatedTransaction struct { ``` -### func \(CreatedTransaction\) GetMemento +### func \(CreatedTransaction\) [GetMemento]() ```go func (p CreatedTransaction) GetMemento() any @@ -299,7 +299,7 @@ func (p CreatedTransaction) GetMemento() any -### func \(CreatedTransaction\) Type +### func \(CreatedTransaction\) [Type]() ```go func (p CreatedTransaction) Type() LogType @@ -308,7 +308,7 @@ func (p CreatedTransaction) Type() LogType -## type DeletedMetadata +## type [DeletedMetadata]() @@ -321,7 +321,7 @@ type DeletedMetadata struct { ``` -### func \(DeletedMetadata\) Type +### func \(DeletedMetadata\) [Type]() ```go func (s DeletedMetadata) Type() LogType @@ -330,7 +330,7 @@ func (s DeletedMetadata) Type() LogType -### func \(\*DeletedMetadata\) UnmarshalJSON +### func \(\*DeletedMetadata\) [UnmarshalJSON]() ```go func (s *DeletedMetadata) UnmarshalJSON(data []byte) error @@ -339,7 +339,7 @@ func (s *DeletedMetadata) UnmarshalJSON(data []byte) error -## type ErrInvalidBucketName +## type [ErrInvalidBucketName]() @@ -350,7 +350,7 @@ type ErrInvalidBucketName struct { ``` -### func \(ErrInvalidBucketName\) Error +### func \(ErrInvalidBucketName\) [Error]() ```go func (e ErrInvalidBucketName) Error() string @@ -359,7 +359,7 @@ func (e ErrInvalidBucketName) Error() string -### func \(ErrInvalidBucketName\) Is +### func \(ErrInvalidBucketName\) [Is]() ```go func (e ErrInvalidBucketName) Is(err error) bool @@ -368,7 +368,7 @@ func (e ErrInvalidBucketName) Is(err error) bool -## type ErrInvalidLedgerName +## type [ErrInvalidLedgerName]() @@ -379,7 +379,7 @@ type ErrInvalidLedgerName struct { ``` -### func \(ErrInvalidLedgerName\) Error +### func \(ErrInvalidLedgerName\) [Error]() ```go func (e ErrInvalidLedgerName) Error() string @@ -388,7 +388,7 @@ func (e ErrInvalidLedgerName) Error() string -### func \(ErrInvalidLedgerName\) Is +### func \(ErrInvalidLedgerName\) [Is]() ```go func (e ErrInvalidLedgerName) Is(err error) bool @@ -397,7 +397,7 @@ func (e ErrInvalidLedgerName) Is(err error) bool -## type Ledger +## type [Ledger]() @@ -413,7 +413,7 @@ type Ledger struct { ``` -### func MustNewWithDefault +### func [MustNewWithDefault]() ```go func MustNewWithDefault(name string) Ledger @@ -422,7 +422,7 @@ func MustNewWithDefault(name string) Ledger -### func New +### func [New]() ```go func New(name string, configuration Configuration) (*Ledger, error) @@ -431,7 +431,7 @@ func New(name string, configuration Configuration) (*Ledger, error) -### func NewWithDefaults +### func [NewWithDefaults]() ```go func NewWithDefaults(name string) (*Ledger, error) @@ -440,7 +440,7 @@ func NewWithDefaults(name string) (*Ledger, error) -### func \(Ledger\) HasFeature +### func \(Ledger\) [HasFeature]() ```go func (l Ledger) HasFeature(feature, value string) bool @@ -449,7 +449,7 @@ func (l Ledger) HasFeature(feature, value string) bool -### func \(Ledger\) WithMetadata +### func \(Ledger\) [WithMetadata]() ```go func (l Ledger) WithMetadata(m metadata.Metadata) Ledger @@ -458,7 +458,7 @@ func (l Ledger) WithMetadata(m metadata.Metadata) Ledger -## type Log +## type [Log]() Log represents atomic actions made on the ledger. @@ -479,7 +479,7 @@ type Log struct { ``` -### func NewLog +### func [NewLog]() ```go func NewLog(payload LogPayload) Log @@ -488,7 +488,7 @@ func NewLog(payload LogPayload) Log -### func \(Log\) ChainLog +### func \(Log\) [ChainLog]() ```go func (l Log) ChainLog(previous *Log) Log @@ -497,7 +497,7 @@ func (l Log) ChainLog(previous *Log) Log -### func \(\*Log\) ComputeHash +### func \(\*Log\) [ComputeHash]() ```go func (l *Log) ComputeHash(previous *Log) @@ -506,7 +506,7 @@ func (l *Log) ComputeHash(previous *Log) -### func \(\*Log\) UnmarshalJSON +### func \(\*Log\) [UnmarshalJSON]() ```go func (l *Log) UnmarshalJSON(data []byte) error @@ -515,7 +515,7 @@ func (l *Log) UnmarshalJSON(data []byte) error -### func \(Log\) WithIdempotencyKey +### func \(Log\) [WithIdempotencyKey]() ```go func (l Log) WithIdempotencyKey(key string) Log @@ -524,7 +524,7 @@ func (l Log) WithIdempotencyKey(key string) Log -## type LogPayload +## type [LogPayload]() @@ -535,7 +535,7 @@ type LogPayload interface { ``` -### func HydrateLog +### func [HydrateLog]() ```go func HydrateLog(_type LogType, data []byte) (LogPayload, error) @@ -544,7 +544,7 @@ func HydrateLog(_type LogType, data []byte) (LogPayload, error) -## type LogType +## type [LogType]() @@ -564,7 +564,7 @@ const ( ``` -### func LogTypeFromString +### func [LogTypeFromString]() ```go func LogTypeFromString(logType string) LogType @@ -573,7 +573,7 @@ func LogTypeFromString(logType string) LogType -### func \(LogType\) MarshalJSON +### func \(LogType\) [MarshalJSON]() ```go func (lt LogType) MarshalJSON() ([]byte, error) @@ -582,7 +582,7 @@ func (lt LogType) MarshalJSON() ([]byte, error) -### func \(\*LogType\) Scan +### func \(\*LogType\) [Scan]() ```go func (lt *LogType) Scan(src interface{}) error @@ -591,7 +591,7 @@ func (lt *LogType) Scan(src interface{}) error -### func \(LogType\) String +### func \(LogType\) [String]() ```go func (lt LogType) String() string @@ -600,7 +600,7 @@ func (lt LogType) String() string -### func \(\*LogType\) UnmarshalJSON +### func \(\*LogType\) [UnmarshalJSON]() ```go func (lt *LogType) UnmarshalJSON(data []byte) error @@ -609,7 +609,7 @@ func (lt *LogType) UnmarshalJSON(data []byte) error -### func \(LogType\) Value +### func \(LogType\) [Value]() ```go func (lt LogType) Value() (driver.Value, error) @@ -618,7 +618,7 @@ func (lt LogType) Value() (driver.Value, error) -## type Memento +## type [Memento]() @@ -629,7 +629,7 @@ type Memento interface { ``` -## type Move +## type [Move]() @@ -650,7 +650,7 @@ type Move struct { ``` -## type Moves +## type [Moves]() @@ -659,7 +659,7 @@ type Moves []*Move ``` -### func \(Moves\) ComputePostCommitEffectiveVolumes +### func \(Moves\) [ComputePostCommitEffectiveVolumes]() ```go func (m Moves) ComputePostCommitEffectiveVolumes() PostCommitVolumes @@ -668,7 +668,7 @@ func (m Moves) ComputePostCommitEffectiveVolumes() PostCommitVolumes -## type PostCommitVolumes +## type [PostCommitVolumes]() @@ -677,7 +677,7 @@ type PostCommitVolumes map[string]VolumesByAssets ``` -### func \(PostCommitVolumes\) AddInput +### func \(PostCommitVolumes\) [AddInput]() ```go func (a PostCommitVolumes) AddInput(account, asset string, input *big.Int) @@ -686,7 +686,7 @@ func (a PostCommitVolumes) AddInput(account, asset string, input *big.Int) -### func \(PostCommitVolumes\) AddOutput +### func \(PostCommitVolumes\) [AddOutput]() ```go func (a PostCommitVolumes) AddOutput(account, asset string, output *big.Int) @@ -695,7 +695,7 @@ func (a PostCommitVolumes) AddOutput(account, asset string, output *big.Int) -### func \(PostCommitVolumes\) Copy +### func \(PostCommitVolumes\) [Copy]() ```go func (a PostCommitVolumes) Copy() PostCommitVolumes @@ -704,7 +704,7 @@ func (a PostCommitVolumes) Copy() PostCommitVolumes -### func \(PostCommitVolumes\) Merge +### func \(PostCommitVolumes\) [Merge]() ```go func (a PostCommitVolumes) Merge(volumes PostCommitVolumes) PostCommitVolumes @@ -713,7 +713,7 @@ func (a PostCommitVolumes) Merge(volumes PostCommitVolumes) PostCommitVolumes -## type Posting +## type [Posting]() @@ -727,7 +727,7 @@ type Posting struct { ``` -### func NewPosting +### func [NewPosting]() ```go func NewPosting(source string, destination string, asset string, amount *big.Int) Posting @@ -736,7 +736,7 @@ func NewPosting(source string, destination string, asset string, amount *big.Int -## type Postings +## type [Postings]() @@ -745,7 +745,7 @@ type Postings []Posting ``` -### func \(Postings\) Reverse +### func \(Postings\) [Reverse]() ```go func (p Postings) Reverse() Postings @@ -754,7 +754,7 @@ func (p Postings) Reverse() Postings -### func \(Postings\) Validate +### func \(Postings\) [Validate]() ```go func (p Postings) Validate() (int, error) @@ -763,7 +763,7 @@ func (p Postings) Validate() (int, error) -## type RevertedTransaction +## type [RevertedTransaction]() @@ -775,7 +775,7 @@ type RevertedTransaction struct { ``` -### func \(RevertedTransaction\) GetMemento +### func \(RevertedTransaction\) [GetMemento]() ```go func (r RevertedTransaction) GetMemento() any @@ -784,7 +784,7 @@ func (r RevertedTransaction) GetMemento() any -### func \(RevertedTransaction\) Type +### func \(RevertedTransaction\) [Type]() ```go func (r RevertedTransaction) Type() LogType @@ -793,7 +793,7 @@ func (r RevertedTransaction) Type() LogType -## type SavedMetadata +## type [SavedMetadata]() @@ -806,7 +806,7 @@ type SavedMetadata struct { ``` -### func \(SavedMetadata\) Type +### func \(SavedMetadata\) [Type]() ```go func (s SavedMetadata) Type() LogType @@ -815,7 +815,7 @@ func (s SavedMetadata) Type() LogType -### func \(\*SavedMetadata\) UnmarshalJSON +### func \(\*SavedMetadata\) [UnmarshalJSON]() ```go func (s *SavedMetadata) UnmarshalJSON(data []byte) error @@ -824,7 +824,7 @@ func (s *SavedMetadata) UnmarshalJSON(data []byte) error -## type Transaction +## type [Transaction]() @@ -845,7 +845,7 @@ type Transaction struct { ``` -### func NewTransaction +### func [NewTransaction]() ```go func NewTransaction() Transaction @@ -854,7 +854,7 @@ func NewTransaction() Transaction -### func \(Transaction\) InvolvedAccounts +### func \(Transaction\) [InvolvedAccounts]() ```go func (tx Transaction) InvolvedAccounts() []string @@ -863,7 +863,7 @@ func (tx Transaction) InvolvedAccounts() []string -### func \(Transaction\) InvolvedDestinations +### func \(Transaction\) [InvolvedDestinations]() ```go func (tx Transaction) InvolvedDestinations() map[string][]string @@ -872,7 +872,7 @@ func (tx Transaction) InvolvedDestinations() map[string][]string -### func \(Transaction\) IsReverted +### func \(Transaction\) [IsReverted]() ```go func (tx Transaction) IsReverted() bool @@ -881,7 +881,7 @@ func (tx Transaction) IsReverted() bool -### func \(Transaction\) JSONSchemaExtend +### func \(Transaction\) [JSONSchemaExtend]() ```go func (Transaction) JSONSchemaExtend(schema *jsonschema.Schema) @@ -890,7 +890,7 @@ func (Transaction) JSONSchemaExtend(schema *jsonschema.Schema) -### func \(Transaction\) MarshalJSON +### func \(Transaction\) [MarshalJSON]() ```go func (tx Transaction) MarshalJSON() ([]byte, error) @@ -899,7 +899,7 @@ func (tx Transaction) MarshalJSON() ([]byte, error) -### func \(Transaction\) Reverse +### func \(Transaction\) [Reverse]() ```go func (tx Transaction) Reverse() Transaction @@ -908,7 +908,7 @@ func (tx Transaction) Reverse() Transaction -### func \(Transaction\) VolumeUpdates +### func \(Transaction\) [VolumeUpdates]() ```go func (tx Transaction) VolumeUpdates() []AccountsVolumes @@ -917,7 +917,7 @@ func (tx Transaction) VolumeUpdates() []AccountsVolumes -### func \(Transaction\) WithInsertedAt +### func \(Transaction\) [WithInsertedAt]() ```go func (tx Transaction) WithInsertedAt(date time.Time) Transaction @@ -926,7 +926,7 @@ func (tx Transaction) WithInsertedAt(date time.Time) Transaction -### func \(Transaction\) WithMetadata +### func \(Transaction\) [WithMetadata]() ```go func (tx Transaction) WithMetadata(m metadata.Metadata) Transaction @@ -935,7 +935,7 @@ func (tx Transaction) WithMetadata(m metadata.Metadata) Transaction -### func \(Transaction\) WithPostCommitEffectiveVolumes +### func \(Transaction\) [WithPostCommitEffectiveVolumes]() ```go func (tx Transaction) WithPostCommitEffectiveVolumes(volumes PostCommitVolumes) Transaction @@ -944,7 +944,7 @@ func (tx Transaction) WithPostCommitEffectiveVolumes(volumes PostCommitVolumes) -### func \(Transaction\) WithPostings +### func \(Transaction\) [WithPostings]() ```go func (tx Transaction) WithPostings(postings ...Posting) Transaction @@ -953,7 +953,7 @@ func (tx Transaction) WithPostings(postings ...Posting) Transaction -### func \(Transaction\) WithReference +### func \(Transaction\) [WithReference]() ```go func (tx Transaction) WithReference(ref string) Transaction @@ -962,7 +962,7 @@ func (tx Transaction) WithReference(ref string) Transaction -### func \(Transaction\) WithRevertedAt +### func \(Transaction\) [WithRevertedAt]() ```go func (tx Transaction) WithRevertedAt(timestamp time.Time) Transaction @@ -971,7 +971,7 @@ func (tx Transaction) WithRevertedAt(timestamp time.Time) Transaction -### func \(Transaction\) WithTimestamp +### func \(Transaction\) [WithTimestamp]() ```go func (tx Transaction) WithTimestamp(ts time.Time) Transaction @@ -980,7 +980,7 @@ func (tx Transaction) WithTimestamp(ts time.Time) Transaction -## type TransactionData +## type [TransactionData]() @@ -995,7 +995,7 @@ type TransactionData struct { ``` -### func NewTransactionData +### func [NewTransactionData]() ```go func NewTransactionData() TransactionData @@ -1004,7 +1004,7 @@ func NewTransactionData() TransactionData -### func \(TransactionData\) WithPostings +### func \(TransactionData\) [WithPostings]() ```go func (data TransactionData) WithPostings(postings ...Posting) TransactionData @@ -1013,7 +1013,7 @@ func (data TransactionData) WithPostings(postings ...Posting) TransactionData -## type Transactions +## type [Transactions]() @@ -1024,7 +1024,7 @@ type Transactions struct { ``` -## type Volumes +## type [Volumes]() @@ -1036,7 +1036,7 @@ type Volumes struct { ``` -### func NewEmptyVolumes +### func [NewEmptyVolumes]() ```go func NewEmptyVolumes() Volumes @@ -1045,7 +1045,7 @@ func NewEmptyVolumes() Volumes -### func NewVolumesInt64 +### func [NewVolumesInt64]() ```go func NewVolumesInt64(input, output int64) Volumes @@ -1054,7 +1054,7 @@ func NewVolumesInt64(input, output int64) Volumes -### func \(Volumes\) Balance +### func \(Volumes\) [Balance]() ```go func (v Volumes) Balance() *big.Int @@ -1063,7 +1063,7 @@ func (v Volumes) Balance() *big.Int -### func \(Volumes\) Copy +### func \(Volumes\) [Copy]() ```go func (v Volumes) Copy() Volumes @@ -1072,7 +1072,7 @@ func (v Volumes) Copy() Volumes -### func \(Volumes\) JSONSchemaExtend +### func \(Volumes\) [JSONSchemaExtend]() ```go func (Volumes) JSONSchemaExtend(schema *jsonschema.Schema) @@ -1081,7 +1081,7 @@ func (Volumes) JSONSchemaExtend(schema *jsonschema.Schema) -### func \(Volumes\) MarshalJSON +### func \(Volumes\) [MarshalJSON]() ```go func (v Volumes) MarshalJSON() ([]byte, error) @@ -1090,7 +1090,7 @@ func (v Volumes) MarshalJSON() ([]byte, error) -### func \(\*Volumes\) Scan +### func \(\*Volumes\) [Scan]() ```go func (v *Volumes) Scan(src interface{}) error @@ -1099,7 +1099,7 @@ func (v *Volumes) Scan(src interface{}) error -### func \(Volumes\) Value +### func \(Volumes\) [Value]() ```go func (v Volumes) Value() (driver.Value, error) @@ -1108,7 +1108,7 @@ func (v Volumes) Value() (driver.Value, error) -## type VolumesByAssets +## type [VolumesByAssets]() @@ -1117,7 +1117,7 @@ type VolumesByAssets map[string]Volumes ``` -### func \(VolumesByAssets\) Balances +### func \(VolumesByAssets\) [Balances]() ```go func (v VolumesByAssets) Balances() BalancesByAssets @@ -1126,7 +1126,7 @@ func (v VolumesByAssets) Balances() BalancesByAssets -## type VolumesWithBalance +## type [VolumesWithBalance]() @@ -1139,7 +1139,7 @@ type VolumesWithBalance struct { ``` -## type VolumesWithBalanceByAssetByAccount +## type [VolumesWithBalanceByAssetByAccount]() @@ -1152,7 +1152,7 @@ type VolumesWithBalanceByAssetByAccount struct { ``` -## type VolumesWithBalanceByAssets +## type [VolumesWithBalanceByAssets]() diff --git a/internal/doc.go b/internal/doc.go index 0d2884577..631b605c7 100644 --- a/internal/doc.go +++ b/internal/doc.go @@ -1,2 +1,2 @@ -//go:generate gomarkdoc -o README.md +//go:generate gomarkdoc -o README.md --repository.default-branch main package ledger diff --git a/tools/generator/go.mod b/tools/generator/go.mod index 12fa366ed..c407a3f2d 100644 --- a/tools/generator/go.mod +++ b/tools/generator/go.mod @@ -9,7 +9,7 @@ replace github.com/formancehq/ledger => ../.. replace github.com/formancehq/ledger/pkg/client => ../../pkg/client require ( - github.com/formancehq/go-libs/v2 v2.0.1-0.20241121194732-b79c48b683f2 + github.com/formancehq/go-libs/v2 v2.0.1-0.20250101192540-cfa76c1dedeb github.com/formancehq/ledger v0.0.0-00010101000000-000000000000 github.com/formancehq/ledger/pkg/client v0.0.0-00010101000000-000000000000 github.com/spf13/cobra v1.8.1 @@ -37,14 +37,14 @@ require ( github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect - github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect + github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/invopop/jsonschema v0.12.0 // indirect + github.com/invopop/jsonschema v0.13.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.7.1 // indirect + github.com/jackc/pgx/v5 v5.7.2 // indirect github.com/jackc/pgxlisten v0.0.0-20241106001234-1d6f6656415c // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect @@ -60,7 +60,7 @@ require ( github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect - github.com/uptrace/bun v1.2.6 // indirect + github.com/uptrace/bun v1.2.7 // indirect github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 // indirect github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect diff --git a/tools/generator/go.sum b/tools/generator/go.sum index 29c1e3556..baec969ed 100644 --- a/tools/generator/go.sum +++ b/tools/generator/go.sum @@ -4,8 +4,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/IBM/sarama v1.43.3 h1:Yj6L2IaNvb2mRBop39N7mmJAHBVY3dTPncr3qGVkxPA= -github.com/IBM/sarama v1.43.3/go.mod h1:FVIRaLrhK3Cla/9FfRF5X9Zua2KpS3SYIXxhac1H+FQ= +github.com/IBM/sarama v1.44.0 h1:puNKqcScjSAgVLramjsuovZrS0nJZFVsrvuUymkWqhE= +github.com/IBM/sarama v1.44.0/go.mod h1:MxQ9SvGfvKIorbk077Ff6DUnBlGpidiQOtU2vuBaxVw= github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= @@ -102,8 +102,8 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241121194732-b79c48b683f2 h1:JsacSDe6MYGCm04ZY/VJyYTUAWg8jhOBZm6AGyErF/I= -github.com/formancehq/go-libs/v2 v2.0.1-0.20241121194732-b79c48b683f2/go.mod h1:ZGHVcAC54ZbPgeoGKy/d31CHy94RMhHQlX+Vui15ST8= +github.com/formancehq/go-libs/v2 v2.0.1-0.20250101192540-cfa76c1dedeb h1:v471RK/yxiFFUkJUgZ2CJXGl46KMjOg+Tlr0uMTlQJg= +github.com/formancehq/go-libs/v2 v2.0.1-0.20250101192540-cfa76c1dedeb/go.mod h1:s2c9ULpCJnof0wJsLout05Aj7EwYGUByGBWVviptqTE= github.com/formancehq/numscript v0.0.10 h1:ElvYpoayUX5tHtCCR18ihJTjNlHzdkE4M0IqSm9aufg= github.com/formancehq/numscript v0.0.10/go.mod h1:btuSv05cYwi9BvLRxVs5zrunU+O1vTgigG1T6UsawcY= github.com/gkampitakis/ciinfo v0.3.0 h1:gWZlOC2+RYYttL0hBqcoQhM7h1qNkVqvRCV1fOvpAv8= @@ -141,8 +141,8 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -170,14 +170,14 @@ github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/C github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI= -github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= +github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= -github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= +github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= +github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= github.com/jackc/pgxlisten v0.0.0-20241106001234-1d6f6656415c h1:bTgmg761ac9Ki27HoLx8IBvc+T+Qj6eptBpKahKIRT4= github.com/jackc/pgxlisten v0.0.0-20241106001234-1d6f6656415c/go.mod h1:N4E1APLOYrbM11HH5kdqAjDa8RJWVwD3JqWpvH22h64= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= @@ -235,10 +235,10 @@ github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= -github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= -github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= -github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/onsi/ginkgo/v2 v2.22.1 h1:QW7tbJAUDyVDVOM5dFa7qaybo+CRfR7bemlQUN6Z8aM= +github.com/onsi/ginkgo/v2 v2.22.1/go.mod h1:S6aTpoRsSq2cZOd+pssHAlKW/Q/jZt6cPrPlnj4a1xM= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= @@ -267,8 +267,8 @@ github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWN github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shirou/gopsutil/v4 v4.24.11 h1:WaU9xqGFKvFfsUv94SXcUPD7rCkU0vr/asVdQOBZNj8= -github.com/shirou/gopsutil/v4 v4.24.11/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8= +github.com/shirou/gopsutil/v4 v4.24.12 h1:qvePBOk20e0IKA1QXrIIU+jmk+zEiYVVx06WjBRlZo4= +github.com/shirou/gopsutil/v4 v4.24.12/go.mod h1:DCtMPAad2XceTeIAbGyVfycbYQNBGk2P8cvDi7/VN9o= github.com/shomali11/util v0.0.0-20220717175126-f0771b70947f h1:OM0LVaVycWC+/j5Qra7USyCg2sc+shg3KwygAA+pYvA= github.com/shomali11/util v0.0.0-20220717175126-f0771b70947f/go.mod h1:9POpw/crb6YrseaYBOwraL0lAYy0aOW79eU3bvMxgbM= github.com/shomali11/xsql v0.0.0-20190608141458-bf76292144df h1:SVCDTuzM3KEk8WBwSSw7RTPLw9ajzBaXDg39Bo6xIeU= @@ -301,14 +301,14 @@ github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPD github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI= github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= -github.com/uptrace/bun v1.2.6 h1:lyGBQAhNiClchb97HA2cBnDeRxwTRLhSIgiFPXVisV8= -github.com/uptrace/bun v1.2.6/go.mod h1:xMgnVFf+/5xsrFBU34HjDJmzZnXbVuNEt/Ih56I8qBU= -github.com/uptrace/bun/dialect/pgdialect v1.2.6 h1:iNd1YLx619K+sZK+dRcWPzluurXYK1QwIkp9FEfNB/8= -github.com/uptrace/bun/dialect/pgdialect v1.2.6/go.mod h1:OL7d3qZLxKYP8kxNhMg3IheN1pDR3UScGjoUP+ivxJQ= +github.com/uptrace/bun v1.2.7 h1:rFjJDW9RM+P08FJkwO5xB+cnYSaQAqsAu9LIQH1iEQY= +github.com/uptrace/bun v1.2.7/go.mod h1:tYihS32vC8v3sNzGtakjd2Q5Vye0D9hBR+0MjvmbaQE= +github.com/uptrace/bun/dialect/pgdialect v1.2.7 h1:HvHPbXQ9f9uE7GaNAikb700i67QpJ50kvyyGRmoMDNA= +github.com/uptrace/bun/dialect/pgdialect v1.2.7/go.mod h1:nUgYSlUrZ5F24XO1df1eSlNzsWk6abB8weKSfmGO7is= github.com/uptrace/bun/extra/bundebug v1.2.5 h1:DsI/gl4jvq5tQ84yPqnlRYIQ4U6AouTqxJ8Y5Oijfz4= github.com/uptrace/bun/extra/bundebug v1.2.5/go.mod h1:JFeYvklf5p92ZILXx4siMe2tEVn5JLelww3IGcJb4yA= -github.com/uptrace/bun/extra/bunotel v1.2.6 h1:6m90acv9hsDuTYRo3oiKCWMatGPmi+feKAx8Y/GPj9A= -github.com/uptrace/bun/extra/bunotel v1.2.6/go.mod h1:QGqnFNJ2H88juh7DmgdPJZVN9bSTpj7UaGllSO9JDKk= +github.com/uptrace/bun/extra/bunotel v1.2.7 h1:8UsbMxWJUVZNKGe+fgCSzBJx/a8fXnlOSXq0R9kSGMY= +github.com/uptrace/bun/extra/bunotel v1.2.7/go.mod h1:BXQlhJmxz5ANiOJPqkRowDmALX4SBAeufmD8sL4vmf0= github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 h1:H8wwQwTe5sL6x30z71lUgNiwBdeCHQjrphCfLwqIHGo= github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2/go.mod h1:/kR4beFhlz2g+V5ik8jW+3PMiMQAPt29y6K64NNY53c= github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 h1:ZjUj9BLYf9PEqBn8W/OapxhPjVRdC6CsXTdULHsyk5c= @@ -413,14 +413,14 @@ golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= -google.golang.org/genproto/googleapis/api v0.0.0-20241219192143-6b3ec007d9bb h1:B7GIB7sr443wZ/EAEl7VZjmh1V6qzkt5V+RYcUYtS1U= -google.golang.org/genproto/googleapis/api v0.0.0-20241219192143-6b3ec007d9bb/go.mod h1:E5//3O5ZIG2l71Xnt+P/CYUY8Bxs8E7WMoZ9tlcMbAY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241219192143-6b3ec007d9bb h1:3oy2tynMOP1QbTC0MsNNAV+Se8M2Bd0A5+x1QHyw+pI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241219192143-6b3ec007d9bb/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= +google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d h1:H8tOf8XM88HvKqLTxe755haY6r1fqqzLbEnfrmLXlSA= +google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d/go.mod h1:2v7Z7gP2ZUOGsaFyxATQSRoBnKygqVq2Cwnvom7QiqY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU= google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= -google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ= -google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= +google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From 58854fe0b00b6a714f12902409fb35edc7629227 Mon Sep 17 00:00:00 2001 From: Ragot Geoffrey Date: Fri, 3 Jan 2025 18:24:13 +0100 Subject: [PATCH 71/71] fix: invalid minimal schema version (#637) --- internal/storage/bucket/default_bucket.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/storage/bucket/default_bucket.go b/internal/storage/bucket/default_bucket.go index 2267a2ca8..22f256ad1 100644 --- a/internal/storage/bucket/default_bucket.go +++ b/internal/storage/bucket/default_bucket.go @@ -15,7 +15,7 @@ import ( ) // stateless version (+1 regarding directory name, as migrations start from 1 in the lib) -const MinimalSchemaVersion = 27 +const MinimalSchemaVersion = 26 type DefaultBucket struct { name string