diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 6d56801849..43ace650e5 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -77,8 +77,6 @@ jobs:
GoReleaser:
runs-on: "shipfox-4vcpu-ubuntu-2404"
if: contains(github.event.pull_request.labels.*.name, 'build-images') || github.ref == 'refs/heads/main' || github.event_name == 'merge_group'
- needs:
- - Dirty
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
@@ -91,6 +89,7 @@ jobs:
- uses: 'actions/checkout@v4'
with:
fetch-depth: 0
+ ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Setup Env
uses: ./.github/actions/default
with:
diff --git a/Earthfile b/Earthfile
index 9f41764d2a..2fe395c88a 100644
--- a/Earthfile
+++ b/Earthfile
@@ -49,34 +49,4 @@ deploy:
RUN kubectl patch Versions.formance.com default -p "{\"spec\":{\"ledger\": \"${tag}\"}}" --type=merge
deploy-staging:
- BUILD --pass-args core+deploy-staging
-
-export-database-schema:
- FROM +sources
- RUN go install github.com/roerohan/wait-for-it@latest
- WITH DOCKER --load=postgres:15-alpine=+postgres --pull schemaspy/schemaspy:6.2.4
- 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
-
-openapi:
- FROM core+base-image
- WORKDIR /src
- COPY openapi.yaml openapi.yaml
- SAVE ARTIFACT ./openapi.yaml
\ No newline at end of file
+ BUILD --pass-args core+deploy-staging
\ No newline at end of file
diff --git a/Justfile b/Justfile
index 7683093e10..16a275cc6f 100644
--- a/Justfile
+++ b/Justfile
@@ -56,3 +56,8 @@ release-ci:
release:
@goreleaser release --clean
+
+generate-grpc-replication:
+ protoc --go_out=. --go_opt=paths=source_relative \
+ --go-grpc_out=. --go-grpc_opt=paths=source_relative \
+ ./internal/replication/grpc/replication_service.proto
\ No newline at end of file
diff --git a/cmd/config.go b/cmd/config.go
index 06b49f9f14..e5aae11523 100644
--- a/cmd/config.go
+++ b/cmd/config.go
@@ -2,18 +2,50 @@ package cmd
import (
"fmt"
+ "github.com/mitchellh/mapstructure"
+ "github.com/robfig/cron/v3"
"github.com/spf13/cobra"
"github.com/spf13/viper"
+ "reflect"
)
-func LoadConfig[V any](cmd *cobra.Command) (*V, error){
+type commonConfig struct {
+ NumscriptInterpreter bool `mapstructure:"experimental-numscript-interpreter"`
+ NumscriptInterpreterFlags []string `mapstructure:"experimental-numscript-interpreter-flags"`
+ ExperimentalFeaturesEnabled bool `mapstructure:"experimental-features"`
+ ExperimentalExporters bool `mapstructure:"experimental-exporters"`
+}
+
+func decodeCronSchedule(sourceType, destType reflect.Type, value any) (any, error) {
+ if sourceType.Kind() != reflect.String {
+ return value, nil
+ }
+ if destType != reflect.TypeOf((*cron.Schedule)(nil)).Elem() {
+ return value, nil
+ }
+
+ parser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
+ schedule, err := parser.Parse(value.(string))
+ if err != nil {
+ return nil, fmt.Errorf("parsing cron schedule: %w", err)
+ }
+
+ return schedule, nil
+}
+
+func LoadConfig[V any](cmd *cobra.Command) (*V, error) {
v := viper.New()
if err := v.BindPFlags(cmd.Flags()); err != nil {
return nil, fmt.Errorf("binding flags: %w", err)
}
var cfg V
- if err := v.Unmarshal(&cfg); err != nil {
+ if err := v.Unmarshal(&cfg,
+ viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(
+ decodeCronSchedule,
+ mapstructure.StringToTimeDurationHookFunc(),
+ )),
+ ); err != nil {
return nil, fmt.Errorf("unmarshalling config: %w", err)
}
diff --git a/cmd/docs_events.go b/cmd/docs_events.go
index 7996ab1958..ddde447432 100644
--- a/cmd/docs_events.go
+++ b/cmd/docs_events.go
@@ -3,7 +3,7 @@ package cmd
import (
"encoding/json"
"fmt"
- "github.com/formancehq/ledger/internal/bus"
+ "github.com/formancehq/ledger/pkg/events"
"github.com/invopop/jsonschema"
"github.com/spf13/cobra"
"os"
@@ -30,10 +30,10 @@ func NewDocEventsCommand() *cobra.Command {
}
for _, o := range []any{
- bus.CommittedTransactions{},
- bus.DeletedMetadata{},
- bus.SavedMetadata{},
- bus.RevertedTransaction{},
+ events.CommittedTransactions{},
+ events.DeletedMetadata{},
+ events.SavedMetadata{},
+ events.RevertedTransaction{},
} {
schema := jsonschema.Reflect(o)
data, err := json.MarshalIndent(schema, "", " ")
diff --git a/cmd/root.go b/cmd/root.go
index 2a7a406b9e..fa166e14ac 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -12,6 +12,11 @@ import (
const (
ServiceName = "ledger"
+
+ NumscriptInterpreterFlag = "experimental-numscript-interpreter"
+ NumscriptInterpreterFlagsToPass = "experimental-numscript-interpreter-flags"
+ ExperimentalFeaturesFlag = "experimental-features"
+ ExperimentalExporters = "experimental-exporters"
)
var (
@@ -28,6 +33,11 @@ func NewRootCommand() *cobra.Command {
Version: Version,
}
+ root.PersistentFlags().Bool(ExperimentalFeaturesFlag, false, "Enable features configurability")
+ root.PersistentFlags().Bool(NumscriptInterpreterFlag, false, "Enable experimental numscript rewrite")
+ root.PersistentFlags().String(NumscriptInterpreterFlagsToPass, "", "Feature flags to pass to the experimental numscript interpreter")
+ root.PersistentFlags().Bool(ExperimentalExporters, false, "Enable exporters support")
+
root.AddCommand(NewServeCommand())
root.AddCommand(NewBucketsCommand())
root.AddCommand(NewVersionCommand())
diff --git a/cmd/serve.go b/cmd/serve.go
index f89ab24d38..5d064afa48 100644
--- a/cmd/serve.go
+++ b/cmd/serve.go
@@ -4,8 +4,13 @@ import (
"fmt"
"github.com/formancehq/go-libs/v3/logging"
"github.com/formancehq/ledger/internal/api/common"
+ "github.com/formancehq/ledger/internal/replication"
+ "github.com/formancehq/ledger/internal/replication/drivers"
+ "github.com/formancehq/ledger/internal/replication/drivers/all"
systemstore "github.com/formancehq/ledger/internal/storage/system"
"github.com/formancehq/ledger/internal/worker"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials/insecure"
"net/http"
"net/http/pprof"
"time"
@@ -37,21 +42,20 @@ import (
"go.uber.org/fx"
)
-type ServeConfig struct {
+type ServeCommandConfig struct {
+ commonConfig `mapstructure:",squash"`
WorkerConfiguration `mapstructure:",squash"`
- Bind string `mapstructure:"bind"`
- BallastSizeInBytes uint `mapstructure:"ballast-size"`
- NumscriptCacheMaxCount uint `mapstructure:"numscript-cache-max-count"`
- AutoUpgrade bool `mapstructure:"auto-upgrade"`
- BulkMaxSize int `mapstructure:"bulk-max-size"`
- BulkParallel int `mapstructure:"bulk-parallel"`
- DefaultPageSize uint64 `mapstructure:"default-page-size"`
- MaxPageSize uint64 `mapstructure:"max-page-size"`
- WorkerEnabled bool `mapstructure:"worker"`
- NumscriptInterpreter bool `mapstructure:"experimental-numscript-interpreter"`
- NumscriptInterpreterFlags []string `mapstructure:"experimental-numscript-interpreter-flags"`
- ExperimentalFeaturesEnabled bool `mapstructure:"experimental-features"`
+ Bind string `mapstructure:"bind"`
+ BallastSizeInBytes uint `mapstructure:"ballast-size"`
+ NumscriptCacheMaxCount uint `mapstructure:"numscript-cache-max-count"`
+ AutoUpgrade bool `mapstructure:"auto-upgrade"`
+ BulkMaxSize int `mapstructure:"bulk-max-size"`
+ BulkParallel int `mapstructure:"bulk-parallel"`
+ DefaultPageSize uint64 `mapstructure:"default-page-size"`
+ MaxPageSize uint64 `mapstructure:"max-page-size"`
+ WorkerEnabled bool `mapstructure:"worker"`
+ WorkerAddress string `mapstructure:"worker-grpc-address"`
}
const (
@@ -62,12 +66,9 @@ const (
BulkMaxSizeFlag = "bulk-max-size"
BulkParallelFlag = "bulk-parallel"
- DefaultPageSizeFlag = "default-page-size"
- MaxPageSizeFlag = "max-page-size"
- WorkerEnabledFlag = "worker"
- NumscriptInterpreterFlag = "experimental-numscript-interpreter"
- NumscriptInterpreterFlagsToPass = "experimental-numscript-interpreter-flags"
- ExperimentalFeaturesFlag = "experimental-features"
+ DefaultPageSizeFlag = "default-page-size"
+ MaxPageSizeFlag = "max-page-size"
+ WorkerEnabledFlag = "worker"
)
func NewServeCommand() *cobra.Command {
@@ -76,7 +77,7 @@ func NewServeCommand() *cobra.Command {
SilenceUsage: true,
RunE: func(cmd *cobra.Command, _ []string) error {
- cfg, err := LoadConfig[ServeConfig](cmd)
+ cfg, err := LoadConfig[ServeCommandConfig](cmd)
if err != nil {
return fmt.Errorf("loading config: %w", err)
}
@@ -97,6 +98,8 @@ func NewServeCommand() *cobra.Command {
storage.NewFXModule(storage.ModuleConfig{
AutoUpgrade: cfg.AutoUpgrade,
}),
+ drivers.NewFXModule(),
+ fx.Invoke(all.Register),
systemcontroller.NewFXModule(systemcontroller.ModuleConfiguration{
NumscriptInterpreter: cfg.NumscriptInterpreter,
NumscriptInterpreterFlags: cfg.NumscriptInterpreterFlags,
@@ -122,6 +125,7 @@ func NewServeCommand() *cobra.Command {
MaxPageSize: cfg.MaxPageSize,
DefaultPageSize: cfg.DefaultPageSize,
},
+ Exporters: cfg.ExperimentalExporters,
}),
fx.Decorate(func(
params struct {
@@ -150,10 +154,18 @@ func NewServeCommand() *cobra.Command {
}
if cfg.WorkerEnabled {
- options = append(options, worker.NewFXModule(worker.ModuleConfig{
- Schedule: cfg.HashLogsBlockCRONSpec,
- MaxBlockSize: cfg.HashLogsBlockMaxSize,
- }))
+ options = append(options,
+ newWorkerModule(cfg.WorkerConfiguration),
+ replication.NewFXEmbeddedClientModule(),
+ )
+ } else {
+ options = append(options,
+ worker.NewGRPCClientFxModule(
+ cfg.WorkerAddress,
+ grpc.WithTransportCredentials(insecure.NewCredentials()),
+ ),
+ replication.NewFXGRPCClientModule(),
+ )
}
return service.New(cmd.OutOrStdout(), options...).Run(cmd)
@@ -171,6 +183,7 @@ func NewServeCommand() *cobra.Command {
cmd.Flags().Bool(ExperimentalFeaturesFlag, false, "Enable features configurability")
cmd.Flags().Bool(NumscriptInterpreterFlag, false, "Enable experimental numscript rewrite")
cmd.Flags().String(NumscriptInterpreterFlagsToPass, "", "Feature flags to pass to the experimental numscript interpreter")
+ cmd.Flags().String(WorkerGRPCAddressFlag, "localhost:8081", "GRPC address")
addWorkerFlags(cmd)
bunconnect.AddFlags(cmd.Flags())
diff --git a/cmd/worker.go b/cmd/worker.go
index cb709f36b9..d4cf80c686 100644
--- a/cmd/worker.go
+++ b/cmd/worker.go
@@ -7,25 +7,58 @@ import (
"github.com/formancehq/go-libs/v3/otlp/otlpmetrics"
"github.com/formancehq/go-libs/v3/otlp/otlptraces"
"github.com/formancehq/go-libs/v3/service"
+ "github.com/formancehq/ledger/internal/replication"
+ "github.com/formancehq/ledger/internal/replication/drivers"
+ "github.com/formancehq/ledger/internal/replication/drivers/all"
"github.com/formancehq/ledger/internal/storage"
"github.com/formancehq/ledger/internal/worker"
+ "github.com/robfig/cron/v3"
"github.com/spf13/cobra"
"go.uber.org/fx"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials/insecure"
+ "time"
)
const (
+ WorkerPipelinesPullIntervalFlag = "worker-pipelines-pull-interval"
+ WorkerPipelinesPushRetryPeriodFlag = "worker-pipelines-push-retry-period"
+ WorkerPipelinesSyncPeriod = "worker-pipelines-sync-period"
+ WorkerPipelinesLogsPageSize = "worker-pipelines-logs-page-size"
+
WorkerAsyncBlockHasherMaxBlockSizeFlag = "worker-async-block-hasher-max-block-size"
WorkerAsyncBlockHasherScheduleFlag = "worker-async-block-hasher-schedule"
+
+ WorkerGRPCAddressFlag = "worker-grpc-address"
)
+type WorkerGRPCConfig struct {
+ Address string `mapstructure:"worker-grpc-address"`
+}
+
type WorkerConfiguration struct {
- HashLogsBlockMaxSize int `mapstructure:"worker-async-block-hasher-max-block-size"`
- HashLogsBlockCRONSpec string `mapstructure:"worker-async-block-hasher-schedule"`
+ HashLogsBlockMaxSize int `mapstructure:"worker-async-block-hasher-max-block-size"`
+ HashLogsBlockCRONSpec cron.Schedule `mapstructure:"worker-async-block-hasher-schedule"`
+
+ PushRetryPeriod time.Duration `mapstructure:"worker-pipelines-push-retry-period"`
+ PullInterval time.Duration `mapstructure:"worker-pipelines-pull-interval"`
+ SyncPeriod time.Duration `mapstructure:"worker-pipelines-sync-period"`
+ LogsPageSize uint64 `mapstructure:"worker-pipelines-logs-page-size"`
+}
+
+type WorkerCommandConfiguration struct {
+ WorkerConfiguration `mapstructure:",squash"`
+ commonConfig `mapstructure:",squash"`
+ WorkerGRPCConfig `mapstructure:",squash"`
}
func addWorkerFlags(cmd *cobra.Command) {
cmd.Flags().Int(WorkerAsyncBlockHasherMaxBlockSizeFlag, 1000, "Max block size")
cmd.Flags().String(WorkerAsyncBlockHasherScheduleFlag, "0 * * * * *", "Schedule")
+ cmd.Flags().Duration(WorkerPipelinesPullIntervalFlag, 5*time.Second, "Pipelines pull interval")
+ cmd.Flags().Duration(WorkerPipelinesPushRetryPeriodFlag, 10*time.Second, "Pipelines push retry period")
+ cmd.Flags().Duration(WorkerPipelinesSyncPeriod, time.Minute, "Pipelines sync period")
+ cmd.Flags().Uint64(WorkerPipelinesLogsPageSize, 100, "Pipelines logs page size")
}
func NewWorkerCommand() *cobra.Command {
@@ -38,7 +71,7 @@ func NewWorkerCommand() *cobra.Command {
return err
}
- cfg, err := LoadConfig[WorkerConfiguration](cmd)
+ cfg, err := LoadConfig[WorkerCommandConfiguration](cmd)
if err != nil {
return fmt.Errorf("loading config: %w", err)
}
@@ -50,14 +83,21 @@ func NewWorkerCommand() *cobra.Command {
otlpmetrics.FXModuleFromFlags(cmd),
bunconnect.Module(*connectionOptions, service.IsDebug(cmd)),
storage.NewFXModule(storage.ModuleConfig{}),
- worker.NewFXModule(worker.ModuleConfig{
- MaxBlockSize: cfg.HashLogsBlockMaxSize,
- Schedule: cfg.HashLogsBlockCRONSpec,
+ drivers.NewFXModule(),
+ fx.Invoke(all.Register),
+ newWorkerModule(cfg.WorkerConfiguration),
+ worker.NewGRPCServerFXModule(worker.GRPCServerModuleConfig{
+ Address: cfg.Address,
+ ServerOptions: []grpc.ServerOption{
+ grpc.Creds(insecure.NewCredentials()),
+ },
}),
).Run(cmd)
},
}
+ cmd.Flags().String(WorkerGRPCAddressFlag, ":8081", "GRPC address")
+
addWorkerFlags(cmd)
service.AddFlags(cmd.Flags())
bunconnect.AddFlags(cmd.Flags())
@@ -66,3 +106,18 @@ func NewWorkerCommand() *cobra.Command {
return cmd
}
+
+func newWorkerModule(configuration WorkerConfiguration) fx.Option {
+ return worker.NewFXModule(worker.ModuleConfig{
+ AsyncBlockRunnerConfig: storage.AsyncBlockRunnerConfig{
+ MaxBlockSize: configuration.HashLogsBlockMaxSize,
+ Schedule: configuration.HashLogsBlockCRONSpec,
+ },
+ ReplicationConfig: replication.WorkerModuleConfig{
+ PushRetryPeriod: configuration.PushRetryPeriod,
+ PullInterval: configuration.PullInterval,
+ SyncPeriod: configuration.SyncPeriod,
+ LogsPageSize: configuration.LogsPageSize,
+ },
+ })
+}
diff --git a/deployments/pulumi/.gitignore b/deployments/pulumi/.gitignore
index 5117ff806e..87e121afc2 100644
--- a/deployments/pulumi/.gitignore
+++ b/deployments/pulumi/.gitignore
@@ -1 +1,2 @@
Pulumi.*.yaml
+examples/
diff --git a/deployments/pulumi/docs/schema.json b/deployments/pulumi/docs/schema.json
index d0c5b3b5fd..ed70d5afea 100644
--- a/deployments/pulumi/docs/schema.json
+++ b/deployments/pulumi/docs/schema.json
@@ -40,6 +40,10 @@
"experimental-numscript-interpreter": {
"type": "boolean",
"description": "ExperimentalNumscriptInterpreter is whether to enable the experimental numscript interpreter"
+ },
+ "experimental-exporters": {
+ "type": "boolean",
+ "description": "ExperimentalExporters is whether to enable experimental exporter"
}
},
"additionalProperties": false,
@@ -79,6 +83,10 @@
"$ref": "#/$defs/Worker",
"description": "Worker is the worker configuration for the ledger"
},
+ "exporters": {
+ "$ref": "#/$defs/Exporters",
+ "description": "Exporters is the exporters configuration for the ledger"
+ },
"ingress": {
"$ref": "#/$defs/Ingress",
"description": "Ingress is the ingress configuration for the ledger"
@@ -132,6 +140,25 @@
"additionalProperties": false,
"type": "object"
},
+ "Exporter": {
+ "properties": {
+ "driver": {
+ "type": "string",
+ "description": "Driver is the driver for the exporter"
+ },
+ "config": {
+ "description": "Config is the configuration for the exporter"
+ }
+ },
+ "additionalProperties": false,
+ "type": "object"
+ },
+ "Exporters": {
+ "additionalProperties": {
+ "$ref": "#/$defs/Exporter"
+ },
+ "type": "object"
+ },
"Generator": {
"properties": {
"generator-version": {
@@ -212,6 +239,13 @@
},
"type": "object",
"description": "Features is the features for the ledger"
+ },
+ "exporters": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Exporters are the exporter to bound to this ledger"
}
},
"additionalProperties": false,
@@ -530,6 +564,23 @@
"disable-upgrade": {
"type": "boolean",
"description": "DisableUpgrade is whether to disable upgrades for the database"
+ },
+ "service": {
+ "$ref": "#/$defs/StorageService",
+ "description": "Service is the service configuration for the database"
+ }
+ },
+ "additionalProperties": false,
+ "type": "object"
+ },
+ "StorageService": {
+ "properties": {
+ "annotations": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Annotations is the annotations for the service"
}
},
"additionalProperties": false,
diff --git a/deployments/pulumi/docs/schema.md b/deployments/pulumi/docs/schema.md
index 46f4c6a222..b37b6d0e6a 100644
--- a/deployments/pulumi/docs/schema.md
+++ b/deployments/pulumi/docs/schema.md
@@ -28,6 +28,7 @@ No description provided for this model.
| termination-grace-period-seconds | `integer` | | integer | TerminationGracePeriodSeconds is the termination grace period in seconds |
| experimental-features | `boolean` | | boolean | ExperimentalFeatures is whether to enable experimental features |
| experimental-numscript-interpreter | `boolean` | | boolean | ExperimentalNumscriptInterpreter is whether to enable the experimental numscript interpreter |
+| experimental-exporters | `boolean` | | boolean | ExperimentalExporters is whether to enable experimental exporter |
## Config
@@ -47,6 +48,7 @@ No description provided for this model.
| storage | `object` | | [Storage](#storage) | Storage is the storage configuration for the ledger |
| api | `object` | | [API](#api) | API is the API configuration for the ledger |
| worker | `object` | | [Worker](#worker) | Worker is the worker configuration for the ledger |
+| exporters | `object` | | [Exporters](#exporters) | Exporters is the exporters configuration for the ledger |
| ingress | `object` | | [Ingress](#ingress) | Ingress is the ingress configuration for the ledger |
| provision | `object` | | [Provision](#provision) | Provision is the initialization configuration for the ledger |
| timeout | `integer` | | integer | Timeout is the timeout for the ledger |
@@ -69,6 +71,25 @@ No description provided for this model.
| conn-max-idle-time | `integer` | | integer | ConnMaxIdleTime is the maximum idle time for a connection |
| options | `object` | | object | Options is the options for the Postgres database to pass on the dsn |
+## Exporter
+
+No description provided for this model.
+
+#### Type: `object`
+
+> ⚠️ Additional properties are not allowed.
+
+| Property | Type | Required | Possible values | Description |
+| -------- | ---- | -------- | --------------- | ----------- |
+| driver | `string` | | string | Driver is the driver for the exporter |
+| config | `None` | | None | Config is the configuration for the exporter |
+
+## Exporters
+
+No description provided for this model.
+
+#### Type: `object`
+
## Generator
No description provided for this model.
@@ -125,6 +146,7 @@ No description provided for this model.
| bucket | `string` | | string | Bucket is the bucket for the ledger |
| metadata | `object` | | object | Metadata is the metadata for the ledger |
| features | `object` | | object | Features is the features for the ledger |
+| exporters | `array` | | string | Exporters are the exporter to bound to this ledger |
## Monitoring
@@ -329,6 +351,19 @@ No description provided for this model.
| postgres | `object` | | [PostgresDatabase](#postgresdatabase) | Postgres is the Postgres configuration for the database |
| connectivity | `object` | | [ConnectivityDatabase](#connectivitydatabase) | Connectivity is the connectivity configuration for the database |
| disable-upgrade | `boolean` | | boolean | DisableUpgrade is whether to disable upgrades for the database |
+| service | `object` | | [StorageService](#storageservice) | Service is the service configuration for the database |
+
+## StorageService
+
+No description provided for this model.
+
+#### Type: `object`
+
+> ⚠️ Additional properties are not allowed.
+
+| Property | Type | Required | Possible values | Description |
+| -------- | ---- | -------- | --------------- | ----------- |
+| annotations | `object` | | object | Annotations is the annotations for the service |
## Worker
diff --git a/deployments/pulumi/examples/stack1.yaml b/deployments/pulumi/examples/stack1.yaml
deleted file mode 100644
index 701de238b0..0000000000
--- a/deployments/pulumi/examples/stack1.yaml
+++ /dev/null
@@ -1,51 +0,0 @@
-config:
- version: "v2.1.6"
- storage:
- connectivity:
- options:
- sslmode: "disable"
- provision:
- provisioner-version: latest
- ledgers:
- ledger1: {}
- ledger2: {}
- generator:
- generator-version: latest
- ledgers:
- ledger1: &build-test
- vus: 50
- until-log-id: 1000
- script: |
- const plain = `vars {
- account $order
- account $seller
- }
- send [USD/2 100] (
- source = @world
- destination = $order
- )
- send [USD/2 1] (
- source = $order
- destination = @fees
- )
- send [USD/2 99] (
- source = $order
- destination = $seller
- )`
-
- function next(iteration) {
- return [{
- action: 'CREATE_TRANSACTION',
- data: {
- script: {
- plain,
- vars: {
- order: `orders:${uuid()}`,
- seller: `sellers:${iteration % 5}`
- }
- }
- }
- }]
- }
- ledger2: *build-test
-
diff --git a/deployments/pulumi/main.go b/deployments/pulumi/main.go
index ae01088cdf..439216288d 100644
--- a/deployments/pulumi/main.go
+++ b/deployments/pulumi/main.go
@@ -4,6 +4,8 @@ import (
"github.com/formancehq/ledger/deployments/pulumi/pkg"
"github.com/formancehq/ledger/deployments/pulumi/pkg/config"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
+
+ _ "github.com/formancehq/ledger/deployments/pulumi/pkg/exporters/clickhouse"
)
func main() {
diff --git a/deployments/pulumi/pkg/api/component.go b/deployments/pulumi/pkg/api/component.go
index 1623c68617..74235a205c 100644
--- a/deployments/pulumi/pkg/api/component.go
+++ b/deployments/pulumi/pkg/api/component.go
@@ -5,6 +5,7 @@ import (
"fmt"
"github.com/formancehq/ledger/deployments/pulumi/pkg/common"
"github.com/formancehq/ledger/deployments/pulumi/pkg/storage"
+ "github.com/formancehq/ledger/deployments/pulumi/pkg/worker"
appsv1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/apps/v1"
corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
@@ -44,6 +45,7 @@ type ComponentArgs struct {
Args
Storage *storage.Component
Ingress *IngressArgs
+ Worker *worker.Component
}
func NewComponent(ctx *pulumi.Context, name string, args ComponentArgs, opts ...pulumi.ResourceOption) (*Component, error) {
@@ -57,6 +59,7 @@ func NewComponent(ctx *pulumi.Context, name string, args ComponentArgs, opts ...
CommonArgs: args.CommonArgs,
Args: args.Args,
Database: args.Storage,
+ Worker: args.Worker,
}, pulumi.Parent(cmp))
if err != nil {
return nil, fmt.Errorf("creating deployment: %w", err)
diff --git a/deployments/pulumi/pkg/api/deployment.go b/deployments/pulumi/pkg/api/deployment.go
index bae52df19e..00fcb5307c 100644
--- a/deployments/pulumi/pkg/api/deployment.go
+++ b/deployments/pulumi/pkg/api/deployment.go
@@ -2,9 +2,10 @@ package api
import (
"fmt"
- common "github.com/formancehq/ledger/deployments/pulumi/pkg/common"
+ "github.com/formancehq/ledger/deployments/pulumi/pkg/common"
"github.com/formancehq/ledger/deployments/pulumi/pkg/storage"
"github.com/formancehq/ledger/deployments/pulumi/pkg/utils"
+ "github.com/formancehq/ledger/deployments/pulumi/pkg/worker"
appsv1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/apps/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"
@@ -23,6 +24,7 @@ type Args struct {
TerminationGracePeriodSeconds pulumix.Input[*int]
ExperimentalFeatures pulumix.Input[bool]
ExperimentalNumscriptInterpreter pulumix.Input[bool]
+ ExperimentalExporters pulumix.Input[bool]
}
func (args *Args) SetDefaults() {
@@ -59,6 +61,7 @@ type createDeploymentArgs struct {
common.CommonArgs
Args
Database *storage.Component
+ Worker *worker.Component
}
func createDeployment(ctx *pulumi.Context, args createDeploymentArgs, resourceOptions ...pulumi.ResourceOption) (*appsv1.Deployment, error) {
@@ -116,6 +119,10 @@ func createDeployment(ctx *pulumi.Context, args createDeploymentArgs, resourceOp
Name: pulumi.String("EXPERIMENTAL_FEATURES"),
Value: utils.BoolToString(args.ExperimentalFeatures).Untyped().(pulumi.StringOutput),
},
+ corev1.EnvVarArgs{
+ Name: pulumi.String("EXPERIMENTAL_EXPORTERS"),
+ Value: utils.BoolToString(args.ExperimentalExporters).Untyped().(pulumi.StringOutput),
+ },
corev1.EnvVarArgs{
Name: pulumi.String("GRACE_PERIOD"),
Value: pulumix.Apply(args.GracePeriod, time.Duration.String).
@@ -128,6 +135,13 @@ func createDeployment(ctx *pulumi.Context, args createDeploymentArgs, resourceOp
envVars = append(envVars, args.Monitoring.GetEnvVars(ctx)...)
}
+ if args.Worker != nil {
+ envVars = append(envVars, corev1.EnvVarArgs{
+ Name: pulumi.String("WORKER_GRPC_ADDRESS"),
+ Value: pulumi.Sprintf("%s:%d", args.Worker.Service.Metadata.Name().Elem(), 8081),
+ })
+ }
+
return appsv1.NewDeployment(ctx, "ledger-api", &appsv1.DeploymentArgs{
Metadata: &metav1.ObjectMetaArgs{
Namespace: args.Namespace.ToOutput(ctx.Context()).Untyped().(pulumi.StringOutput),
diff --git a/deployments/pulumi/pkg/component.go b/deployments/pulumi/pkg/component.go
index fd132a56e5..76bea8b758 100644
--- a/deployments/pulumi/pkg/component.go
+++ b/deployments/pulumi/pkg/component.go
@@ -5,6 +5,7 @@ import (
"github.com/formancehq/ledger/deployments/pulumi/pkg/api"
"github.com/formancehq/ledger/deployments/pulumi/pkg/common"
"github.com/formancehq/ledger/deployments/pulumi/pkg/devbox"
+ "github.com/formancehq/ledger/deployments/pulumi/pkg/exporters"
"github.com/formancehq/ledger/deployments/pulumi/pkg/generator"
"github.com/formancehq/ledger/deployments/pulumi/pkg/provision"
"github.com/formancehq/ledger/deployments/pulumi/pkg/storage"
@@ -23,6 +24,7 @@ type ComponentArgs struct {
Storage storage.Args
Ingress *api.IngressArgs
API api.Args
+ Exporters exporters.Args
Worker worker.Args
Provision provision.Args
Generator *generator.Args
@@ -42,10 +44,11 @@ type Component struct {
pulumi.ResourceState
API *api.Component
- Worker *worker.Component
+ Worker *worker.Component
Storage *storage.Component
Namespace *corev1.Namespace
Devbox *devbox.Component
+ Exporters *exporters.Component
Provision *provision.Component
Generator *generator.Component
}
@@ -63,6 +66,9 @@ func NewComponent(ctx *pulumi.Context, name string, args ComponentArgs, opts ...
pulumi.Parent(cmp),
}
+ // todo: need to add on options to retains on delete
+ // otherwise, even if the retains on delete option is set on the installed resources,
+ // the pulumi will still delete the resources
cmp.Namespace, err = corev1.NewNamespace(ctx, "namespace", &corev1.NamespaceArgs{
Metadata: &metav1.ObjectMetaArgs{
Name: args.Namespace.
@@ -95,31 +101,42 @@ func NewComponent(ctx *pulumi.Context, name string, args ComponentArgs, opts ...
cmp.Storage.Service,
}))
- cmp.API, err = api.NewComponent(ctx, "api", api.ComponentArgs{
+ cmp.Worker, err = worker.NewComponent(ctx, "worker", worker.ComponentArgs{
CommonArgs: args.CommonArgs,
- Args: args.API,
- Storage: cmp.Storage,
- Ingress: args.Ingress,
+ Args: args.Worker,
+ Database: cmp.Storage,
}, options...)
if err != nil {
return nil, err
}
- cmp.Worker, err = worker.NewComponent(ctx, "worker", worker.ComponentArgs{
+ cmp.API, err = api.NewComponent(ctx, "api", api.ComponentArgs{
CommonArgs: args.CommonArgs,
- Args: args.Worker,
- Database: cmp.Storage,
- API: cmp.API,
+ Args: args.API,
+ Storage: cmp.Storage,
+ Ingress: args.Ingress,
+ Worker: cmp.Worker,
}, options...)
if err != nil {
return nil, err
}
- if len(args.Provision.Ledgers) > 0 {
+ if len(args.Exporters.Exporters) > 0 {
+ cmp.Exporters, err = exporters.NewComponent(ctx, "exporters", exporters.ComponentArgs{
+ CommonArgs: args.CommonArgs,
+ Args: args.Exporters,
+ })
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ if len(args.Provision.Ledgers) > 0 || cmp.Exporters != nil {
cmp.Provision, err = provision.NewComponent(ctx, "provisioner", provision.ComponentArgs{
CommonArgs: args.CommonArgs,
API: cmp.API,
Args: args.Provision,
+ Exporters: cmp.Exporters,
}, options...)
if err != nil {
return nil, err
@@ -154,6 +171,7 @@ func NewComponent(ctx *pulumi.Context, name string, args ComponentArgs, opts ...
CommonArgs: args.CommonArgs,
Storage: cmp.Storage,
API: cmp.API,
+ Exporters: cmp.Exporters,
}, options...)
if err != nil {
return nil, err
diff --git a/deployments/pulumi/pkg/config/config.go b/deployments/pulumi/pkg/config/config.go
index edcfb2b813..31cfc35237 100644
--- a/deployments/pulumi/pkg/config/config.go
+++ b/deployments/pulumi/pkg/config/config.go
@@ -8,6 +8,7 @@ import (
pulumi_ledger "github.com/formancehq/ledger/deployments/pulumi/pkg"
"github.com/formancehq/ledger/deployments/pulumi/pkg/api"
"github.com/formancehq/ledger/deployments/pulumi/pkg/common"
+ "github.com/formancehq/ledger/deployments/pulumi/pkg/exporters"
"github.com/formancehq/ledger/deployments/pulumi/pkg/generator"
"github.com/formancehq/ledger/deployments/pulumi/pkg/monitoring"
"github.com/formancehq/ledger/deployments/pulumi/pkg/provision"
@@ -18,6 +19,7 @@ import (
"github.com/pulumi/pulumi/sdk/v3/go/pulumi/config"
"github.com/pulumi/pulumi/sdk/v3/go/pulumix"
"gopkg.in/yaml.v3"
+ "reflect"
"time"
)
@@ -323,6 +325,9 @@ type API struct {
// ExperimentalNumscriptInterpreter is whether to enable the experimental numscript interpreter
ExperimentalNumscriptInterpreter bool `json:"experimental-numscript-interpreter" yaml:"experimental-numscript-interpreter"`
+
+ // ExperimentalExporters is whether to enable experimental exporter
+ ExperimentalExporters bool `json:"experimental-exporters" yaml:"experimental-exporters"`
}
func (d API) toInput() api.Args {
@@ -336,6 +341,64 @@ func (d API) toInput() api.Args {
TerminationGracePeriodSeconds: pulumix.Val(d.TerminationGracePeriodSeconds),
ExperimentalFeatures: pulumix.Val(d.ExperimentalFeatures),
ExperimentalNumscriptInterpreter: pulumix.Val(d.ExperimentalNumscriptInterpreter),
+ ExperimentalExporters: pulumix.Val(d.ExperimentalExporters),
+ }
+}
+
+type Exporter struct {
+ // Driver is the driver for the exporter
+ Driver string `json:"driver" yaml:"driver"`
+
+ // Config is the configuration for the exporter
+ Config any `json:"config" yaml:"config"`
+}
+
+func (c Exporter) toInput() exporters.ExporterArgs {
+ return exporters.ExporterArgs{
+ Driver: c.Driver,
+ Config: c.Config,
+ }
+}
+
+type Exporters map[string]Exporter
+
+func (c *Exporters) UnmarshalJSON(data []byte) error {
+ asMap := make(map[string]json.RawMessage, 0)
+ if err := json.Unmarshal(data, &asMap); err != nil {
+ return fmt.Errorf("error unmarshalling exporters into an array: %w", err)
+ }
+
+ *c = make(map[string]Exporter)
+ for id, elem := range asMap {
+ type def struct {
+ Driver string `json:"driver" yaml:"driver"`
+ }
+ d := def{}
+ if err := json.Unmarshal(elem, &d); err != nil {
+ return fmt.Errorf("error unmarshalling exporter definition %s: %w", id, err)
+ }
+
+ cfg, err := exporters.GetExporterConfig(d.Driver)
+ if err != nil {
+ return err
+ }
+
+ if err := json.Unmarshal(elem, cfg); err != nil {
+ return fmt.Errorf("error unmarshalling exporter config %s: %w", id, err)
+ }
+
+ (*c)[id] = Exporter{
+ Driver: d.Driver,
+ Config: reflect.ValueOf(cfg).Elem().Interface(),
+ }
+ }
+
+ return nil
+}
+
+func (c *Exporters) toInput() exporters.Args {
+ return exporters.Args{
+ Exporters: ConvertMap(*c, Exporter.toInput),
}
}
@@ -533,13 +596,17 @@ type LedgerConfig struct {
// Features is the features for the ledger
Features map[string]string `json:"features" yaml:"features"`
+
+ // Exporters are the exporter to bound to this ledger
+ Exporters []string `json:"exporters" yaml:"exporters"`
}
func (c LedgerConfig) toInput() provision.LedgerConfigArgs {
return provision.LedgerConfigArgs{
- Bucket: c.Bucket,
- Metadata: c.Metadata,
- Features: c.Features,
+ Bucket: c.Bucket,
+ Metadata: c.Metadata,
+ Features: c.Features,
+ Exporters: c.Exporters,
}
}
@@ -620,6 +687,9 @@ type Config struct {
// Worker is the worker configuration for the ledger
Worker *Worker `json:"worker,omitempty" yaml:"worker,omitempty"`
+ // Exporters is the exporters configuration for the ledger
+ Exporters Exporters `json:"exporters" yaml:"exporters"`
+
// Ingress is the ingress configuration for the ledger
Ingress *Ingress `json:"ingress,omitempty" yaml:"ingress,omitempty"`
@@ -646,6 +716,7 @@ func (cfg Config) ToInput() pulumi_ledger.ComponentArgs {
Ingress: cfg.Ingress.toInput(),
InstallDevBox: pulumix.Val(cfg.InstallDevBox),
Provision: cfg.Provision.toInput(),
+ Exporters: cfg.Exporters.toInput(),
Generator: cfg.Generator.toInput(),
}
}
@@ -691,6 +762,13 @@ func Load(ctx *pulumi.Context) (*Config, error) {
}
}
+ exporters := Exporters{}
+ if err := config.GetObject(ctx, "exporters", &exporters); err != nil {
+ if !errors.Is(err, config.ErrMissingVar) {
+ return nil, err
+ }
+ }
+
monitoring := &Monitoring{}
if err := cfg.GetObject("monitoring", monitoring); err != nil {
if !errors.Is(err, config.ErrMissingVar) {
@@ -732,6 +810,7 @@ func Load(ctx *pulumi.Context) (*Config, error) {
API: api,
Worker: worker,
Ingress: ingress,
+ Exporters: exporters,
Provision: provision,
Generator: generator,
}, nil
diff --git a/deployments/pulumi/pkg/devbox/component.go b/deployments/pulumi/pkg/devbox/component.go
index 069ab1d6e3..f2f78084b7 100644
--- a/deployments/pulumi/pkg/devbox/component.go
+++ b/deployments/pulumi/pkg/devbox/component.go
@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/formancehq/ledger/deployments/pulumi/pkg/api"
"github.com/formancehq/ledger/deployments/pulumi/pkg/common"
+ "github.com/formancehq/ledger/deployments/pulumi/pkg/exporters"
"github.com/formancehq/ledger/deployments/pulumi/pkg/storage"
appsv1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/apps/v1"
corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1"
@@ -19,7 +20,8 @@ type Component struct {
type ComponentArgs struct {
common.CommonArgs
Storage *storage.Component
- API *api.Component
+ API *api.Component
+ Exporters *exporters.Component
}
func NewComponent(ctx *pulumi.Context, name string, args ComponentArgs, opts ...pulumi.ResourceOption) (*Component, error) {
@@ -34,6 +36,12 @@ func NewComponent(ctx *pulumi.Context, name string, args ComponentArgs, opts ...
args.API.GetDevBoxContainer(ctx.Context()),
}
+ if args.Exporters != nil {
+ for _, exporter := range args.Exporters.Exporters {
+ containers = append(containers, exporter.Component.GetDevBoxContainer(ctx.Context()))
+ }
+ }
+
cmp.Deployment, err = appsv1.NewDeployment(ctx, "ledger-devbox", &appsv1.DeploymentArgs{
Metadata: &metav1.ObjectMetaArgs{
Namespace: args.Namespace.ToOutput(ctx.Context()).Untyped().(pulumi.StringOutput),
diff --git a/deployments/pulumi/pkg/exporters/clickhouse/component_external.go b/deployments/pulumi/pkg/exporters/clickhouse/component_external.go
new file mode 100644
index 0000000000..dad4c2aad3
--- /dev/null
+++ b/deployments/pulumi/pkg/exporters/clickhouse/component_external.go
@@ -0,0 +1,39 @@
+package clickhouse
+
+import (
+ "fmt"
+ "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
+ "github.com/pulumi/pulumi/sdk/v3/go/pulumix"
+)
+
+type externalComponent struct {
+ pulumi.ResourceState
+
+ DSN pulumix.Output[string]
+}
+
+func (e *externalComponent) GetDSN() pulumix.Output[string] {
+ return e.DSN
+}
+
+type externalComponentArgs struct {
+ DSN pulumi.String
+}
+
+func newExternalComponent(ctx *pulumi.Context, name string, args externalComponentArgs, opts ...pulumi.ResourceOption) (*externalComponent, error) {
+ cmp := &externalComponent{}
+ err := ctx.RegisterComponentResource("Formance:Ledger:Clickhouse:External", name, cmp, opts...)
+ if err != nil {
+ return nil, err
+ }
+
+ cmp.DSN = args.DSN.ToOutput(ctx.Context())
+
+ if err := ctx.RegisterResourceOutputs(cmp, pulumi.Map{}); err != nil {
+ return nil, fmt.Errorf("registering outputs: %w", err)
+ }
+
+ return cmp, nil
+}
+
+var _ dsnProvider = (*externalComponent)(nil)
diff --git a/deployments/pulumi/pkg/exporters/clickhouse/component_facade.go b/deployments/pulumi/pkg/exporters/clickhouse/component_facade.go
new file mode 100644
index 0000000000..4f90279b2e
--- /dev/null
+++ b/deployments/pulumi/pkg/exporters/clickhouse/component_facade.go
@@ -0,0 +1,51 @@
+package clickhouse
+
+import (
+ "context"
+ corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1"
+ "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
+ "github.com/pulumi/pulumi/sdk/v3/go/pulumix"
+)
+
+type dsnProvider interface {
+ GetDSN() pulumix.Output[string]
+}
+
+type componentFacade struct {
+ cmp dsnProvider
+}
+
+func (e *componentFacade) GetConfig() pulumi.AnyOutput {
+ return pulumix.Apply(e.cmp.GetDSN(), func(dsn string) any {
+ return map[string]any{
+ "dsn": dsn,
+ }
+ }).Untyped().(pulumi.AnyOutput)
+}
+
+func (b *componentFacade) GetDevBoxContainer(ctx context.Context) corev1.ContainerInput {
+ return corev1.ContainerArgs{
+ Name: pulumi.String("clickhouse"),
+ Image: pulumi.String("clickhouse:25.1"),
+ Command: pulumi.StringArray{
+ pulumi.String("sleep"),
+ },
+ Args: pulumi.StringArray{
+ pulumi.String("infinity"),
+ },
+ Env: corev1.EnvVarArray{
+ corev1.EnvVarArgs{
+ Name: pulumi.String("CLICKHOUSE_URL"),
+ Value: b.cmp.GetDSN().
+ ToOutput(ctx).
+ Untyped().(pulumi.StringOutput),
+ },
+ },
+ }
+}
+
+func newComponentFacade(cmp dsnProvider) *componentFacade {
+ return &componentFacade{
+ cmp: cmp,
+ }
+}
diff --git a/deployments/pulumi/pkg/exporters/clickhouse/component_internal.go b/deployments/pulumi/pkg/exporters/clickhouse/component_internal.go
new file mode 100644
index 0000000000..47f80420d1
--- /dev/null
+++ b/deployments/pulumi/pkg/exporters/clickhouse/component_internal.go
@@ -0,0 +1,142 @@
+package clickhouse
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "github.com/formancehq/ledger/deployments/pulumi/pkg/common"
+ corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1"
+ helm "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v4"
+ "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
+ "github.com/pulumi/pulumi/sdk/v3/go/pulumi/internals"
+ "github.com/pulumi/pulumi/sdk/v3/go/pulumix"
+)
+
+type internalComponent struct {
+ pulumi.ResourceState
+
+ DSN pulumix.Output[string]
+ Chart *helm.Chart
+ Service *corev1.Service
+}
+
+func (e *internalComponent) GetDSN() pulumix.Output[string] {
+ return pulumix.Apply2(
+ e.Service.Metadata.Name().Elem(),
+ e.Service.Metadata.Namespace().Elem(),
+ func(name string, namespace string) string {
+ return fmt.Sprintf(
+ "clickhouse://default:password@%s.%s.svc.cluster.local:%d",
+ name,
+ namespace,
+ 9000,
+ )
+ },
+ )
+}
+
+type internalComponentArgs struct {
+ common.CommonArgs
+ Config pulumi.MapInput
+ RetainsOnDelete bool
+}
+
+func newInternalComponent(ctx *pulumi.Context, name string, args internalComponentArgs, opts ...pulumi.ResourceOption) (*internalComponent, error) {
+ cmp := &internalComponent{}
+ err := ctx.RegisterComponentResource("Formance:Ledger:Clickhouse:Internal", name, cmp, opts...)
+ if err != nil {
+ return nil, err
+ }
+
+ chartOptions := []pulumi.ResourceOption{
+ pulumi.Parent(cmp),
+ }
+ if args.RetainsOnDelete {
+ chartOptions = append(chartOptions,
+ pulumi.RetainOnDelete(true),
+ // see https://github.com/pulumi/pulumi-kubernetes/issues/3065
+ pulumi.Transforms([]pulumi.ResourceTransform{
+ func(ctx context.Context, args *pulumi.ResourceTransformArgs) *pulumi.ResourceTransformResult {
+ args.Opts.RetainOnDelete = true
+ return &pulumi.ResourceTransformResult{
+ Props: args.Props,
+ Opts: args.Opts,
+ }
+ },
+ }),
+ )
+ }
+
+ cmp.Chart, err = helm.NewChart(ctx, "clickhouse", &helm.ChartArgs{
+ Chart: pulumi.String("oci://registry-1.docker.io/bitnamicharts/clickhouse"),
+ Version: pulumi.String("8.0.6"),
+ Name: pulumi.String("clickhouse"),
+ Namespace: args.Namespace.ToOutput(ctx.Context()).Untyped().(pulumi.StringOutput),
+ Values: pulumix.Apply(args.Config.ToMapOutput(), func(values map[string]any) map[string]any {
+ // Add sane default for development
+ if values == nil {
+ values = map[string]any{}
+ }
+ if values["replicaCount"] == nil {
+ values["replicaCount"] = 1
+ }
+ if values["shards"] == nil {
+ values["shards"] = 1
+ }
+ if values["zookeeper"] == nil {
+ values["zookeeper"] = map[string]any{
+ "enabled": false,
+ }
+ }
+ if values["auth"] == nil {
+ values["auth"] = map[string]any{
+ "password": "password",
+ }
+ }
+ return values
+ }).Untyped().(pulumi.MapOutput),
+ }, chartOptions...)
+ if err != nil {
+ return nil, err
+ }
+
+ ret, err := internals.UnsafeAwaitOutput(ctx.Context(), pulumix.ApplyErr(cmp.Chart.Resources, func(resources []any) (*corev1.Service, error) {
+ for _, resource := range resources {
+ service, ok := resource.(*corev1.Service)
+ if !ok {
+ continue
+ }
+ ret, err := internals.UnsafeAwaitOutput(ctx.Context(), pulumix.Apply2(
+ service.Spec.Type().Elem(),
+ service.Spec.ClusterIP().Elem(),
+ func(serviceType, clusterIP string) *corev1.Service {
+ // find the first service with a cluster ip address
+ if clusterIP == "None" {
+ return nil
+ }
+ return service
+ },
+ ))
+ if err != nil {
+ return nil, err
+ }
+ if ret.Value != nil {
+ return ret.Value.(*corev1.Service), nil
+ }
+ return service, nil
+ }
+ return nil, errors.New("not found")
+ }))
+ if err != nil {
+ return nil, err
+ }
+ cmp.Service = ret.Value.(*corev1.Service)
+
+ if err := ctx.RegisterResourceOutputs(cmp, pulumi.Map{}); err != nil {
+ return nil, fmt.Errorf("registering outputs: %w", err)
+ }
+
+ return cmp, nil
+}
+
+var _ dsnProvider = (*internalComponent)(nil)
diff --git a/deployments/pulumi/pkg/exporters/clickhouse/factory.go b/deployments/pulumi/pkg/exporters/clickhouse/factory.go
new file mode 100644
index 0000000000..fe0dc2ced3
--- /dev/null
+++ b/deployments/pulumi/pkg/exporters/clickhouse/factory.go
@@ -0,0 +1,55 @@
+package clickhouse
+
+import (
+ "errors"
+ "github.com/formancehq/ledger/deployments/pulumi/pkg/common"
+ "github.com/formancehq/ledger/deployments/pulumi/pkg/exporters"
+ "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
+)
+
+type InstallConfiguration struct {
+ Configuration map[string]any `json:"configuration" yaml:"configuration"`
+ RetainsOnDelete bool `json:"retains-on-delete" yaml:"retains-on-delete"`
+}
+
+type Configuration struct {
+ DSN string `json:"dsn" yaml:"dsn"`
+ Install *InstallConfiguration `json:"install" yaml:"install"`
+}
+
+type Factory struct{}
+
+func (f Factory) Name() string {
+ return "clickhouse"
+}
+
+func (f Factory) Setup(ctx *pulumi.Context, args common.CommonArgs, config Configuration, options []pulumi.ResourceOption) (exporters.ExporterComponent, error) {
+ var (
+ cmp dsnProvider
+ err error
+ )
+ if config.DSN != "" {
+ cmp, err = newExternalComponent(ctx, "clickhouse", externalComponentArgs{
+ DSN: pulumi.String(config.DSN),
+ }, options...)
+ } else if config.Install != nil {
+ cmp, err = newInternalComponent(ctx, "clickhouse", internalComponentArgs{
+ CommonArgs: args,
+ Config: pulumi.ToMap(config.Install.Configuration),
+ RetainsOnDelete: config.Install.RetainsOnDelete,
+ }, options...)
+ } else {
+ return nil, errors.New("either DSN or Install configuration must be provided")
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ return newComponentFacade(cmp), nil
+}
+
+var _ exporters.Factory[Configuration] = (*Factory)(nil)
+
+func init() {
+ exporters.RegisterExporterFactory(Factory{})
+}
diff --git a/deployments/pulumi/pkg/exporters/component.go b/deployments/pulumi/pkg/exporters/component.go
new file mode 100644
index 0000000000..913f96bb77
--- /dev/null
+++ b/deployments/pulumi/pkg/exporters/component.go
@@ -0,0 +1,75 @@
+package exporters
+
+import (
+ "fmt"
+ "github.com/formancehq/ledger/deployments/pulumi/pkg/common"
+ "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
+ "reflect"
+)
+
+type ExporterArgs struct {
+ Driver string
+ Config any
+}
+
+type Args struct {
+ Exporters map[string]ExporterArgs
+}
+
+type Exporter struct {
+ Driver string
+ Component ExporterComponent
+}
+
+type Component struct {
+ pulumi.ResourceState
+
+ Exporters map[string]Exporter
+}
+
+type ComponentArgs struct {
+ common.CommonArgs
+ Args
+}
+
+func NewComponent(ctx *pulumi.Context, name string, args ComponentArgs, opts ...pulumi.ResourceOption) (*Component, error) {
+ cmp := &Component{
+ Exporters: map[string]Exporter{},
+ }
+ err := ctx.RegisterComponentResource("Formance:Ledger:Exporters", name, cmp, opts...)
+ if err != nil {
+ return nil, err
+ }
+
+ for id, exporter := range args.Exporters {
+ factory, ok := exporterFactories[exporter.Driver]
+ if !ok {
+ return nil, fmt.Errorf("exporter %s not found", name)
+ }
+
+ m := reflect.ValueOf(factory).
+ MethodByName("Setup").
+ Call([]reflect.Value{
+ reflect.ValueOf(ctx),
+ reflect.ValueOf(args.CommonArgs),
+ reflect.ValueOf(exporter.Config),
+ reflect.ValueOf([]pulumi.ResourceOption{
+ pulumi.Parent(cmp),
+ }),
+ })
+ if !m[1].IsZero() {
+ return nil, m[1].Interface().(error)
+ }
+
+ cmp.Exporters[id] = Exporter{
+ Driver: exporter.Driver,
+ Component: m[0].Interface().(ExporterComponent),
+ }
+ }
+
+ if err := ctx.RegisterResourceOutputs(cmp, pulumi.Map{}); err != nil {
+ return nil, fmt.Errorf("registering outputs: %w", err)
+ }
+
+ return cmp, nil
+}
diff --git a/deployments/pulumi/pkg/exporters/connector.go b/deployments/pulumi/pkg/exporters/connector.go
new file mode 100644
index 0000000000..420096bcf7
--- /dev/null
+++ b/deployments/pulumi/pkg/exporters/connector.go
@@ -0,0 +1,38 @@
+package exporters
+
+import (
+ "context"
+ "fmt"
+ "github.com/formancehq/ledger/deployments/pulumi/pkg/common"
+ corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1"
+ "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
+ "reflect"
+)
+
+type ExporterComponent interface {
+ GetConfig() pulumi.AnyOutput
+ GetDevBoxContainer(context context.Context) corev1.ContainerInput
+}
+
+type Factory[CONFIG any] interface {
+ Name() string
+ Setup(ctx *pulumi.Context, args common.CommonArgs, config CONFIG, options []pulumi.ResourceOption) (ExporterComponent, error)
+}
+
+var exporterFactories = map[string]any{}
+
+func RegisterExporterFactory[CONFIG any](exporter Factory[CONFIG]) {
+ exporterFactories[exporter.Name()] = exporter
+}
+
+func GetExporterConfig(name string) (any, error) {
+ exporter, ok := exporterFactories[name]
+ if !ok {
+ return nil, fmt.Errorf("exporter %s not found", name)
+ }
+
+ m, _ := reflect.TypeOf(exporter).MethodByName("Setup")
+ cfg := m.Type.In(3)
+
+ return reflect.New(cfg).Interface(), nil
+}
diff --git a/deployments/pulumi/pkg/provision/component.go b/deployments/pulumi/pkg/provision/component.go
index c49caf21ed..d2a0cf267e 100644
--- a/deployments/pulumi/pkg/provision/component.go
+++ b/deployments/pulumi/pkg/provision/component.go
@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/formancehq/ledger/deployments/pulumi/pkg/api"
"github.com/formancehq/ledger/deployments/pulumi/pkg/common"
+ "github.com/formancehq/ledger/deployments/pulumi/pkg/exporters"
"github.com/formancehq/ledger/deployments/pulumi/pkg/utils"
batchv1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/batch/v1"
corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1"
@@ -20,7 +21,8 @@ type Component struct {
type LedgerConfigArgs struct {
Bucket string `json:"bucket"`
Metadata map[string]string `json:"metadata"`
- Features map[string]string `json:"features"`
+ Features map[string]string `json:"features"`
+ Exporters []string `json:"exporters"`
}
type Args struct {
@@ -30,7 +32,8 @@ type Args struct {
type ComponentArgs struct {
common.CommonArgs
- API *api.Component
+ API *api.Component
+ Exporters *exporters.Component
Args
}
diff --git a/deployments/pulumi/pkg/provision/configmap.go b/deployments/pulumi/pkg/provision/configmap.go
index 8afdd830b2..c31f2e6bb7 100644
--- a/deployments/pulumi/pkg/provision/configmap.go
+++ b/deployments/pulumi/pkg/provision/configmap.go
@@ -7,14 +7,30 @@ import (
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/pulumi/internals"
)
func createConfigMap(ctx *pulumi.Context, cmp *Component, args ComponentArgs) (*corev1.ConfigMap, error) {
+ exporters := make(map[string]any)
+ if args.Exporters != nil && args.Exporters.Exporters != nil {
+ for exporterName, exporterComponent := range args.Exporters.Exporters {
+ config, err := internals.UnsafeAwaitOutput(ctx.Context(), exporterComponent.Component.GetConfig())
+ if err != nil {
+ return nil, err
+ }
+ exporters[exporterName] = map[string]any{
+ "driver": exporterComponent.Driver,
+ "config": config.Value,
+ }
+ }
+ }
marshalledConfig, err := json.Marshal(struct {
- Ledgers map[string]LedgerConfigArgs `json:"ledgers"`
+ Ledgers map[string]LedgerConfigArgs `json:"ledgers"`
+ Exporters map[string]any `json:"exporters"`
}{
- Ledgers: args.Ledgers,
+ Ledgers: args.Ledgers,
+ Exporters: exporters,
})
if err != nil {
return nil, err
diff --git a/deployments/pulumi/pkg/worker/component.go b/deployments/pulumi/pkg/worker/component.go
index c06ef8fbfe..845b04ad19 100644
--- a/deployments/pulumi/pkg/worker/component.go
+++ b/deployments/pulumi/pkg/worker/component.go
@@ -2,13 +2,10 @@ package worker
import (
"fmt"
- "github.com/formancehq/ledger/deployments/pulumi/pkg/api"
"github.com/formancehq/ledger/deployments/pulumi/pkg/common"
"github.com/formancehq/ledger/deployments/pulumi/pkg/storage"
- "github.com/formancehq/ledger/deployments/pulumi/pkg/utils"
appsv1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/apps/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"
)
@@ -34,7 +31,6 @@ type ComponentArgs struct {
common.CommonArgs
Args
Database *storage.Component
- API *api.Component
}
func NewComponent(ctx *pulumi.Context, name string, args ComponentArgs, opts ...pulumi.ResourceOption) (*Component, error) {
@@ -44,56 +40,14 @@ func NewComponent(ctx *pulumi.Context, name string, args ComponentArgs, opts ...
return nil, err
}
- envVars := corev1.EnvVarArray{}
- envVars = append(envVars, corev1.EnvVarArgs{
- Name: pulumi.String("DEBUG"),
- Value: utils.BoolToString(args.Debug).Untyped().(pulumi.StringOutput),
- })
-
- envVars = append(envVars, args.Database.GetEnvVars()...)
- if otel := args.Monitoring; otel != nil {
- envVars = append(envVars, args.Monitoring.GetEnvVars(ctx)...)
+ cmp.Deployment, err = createDeployment(ctx, args, pulumi.Parent(cmp))
+ if err != nil {
+ return nil, fmt.Errorf("creating deployment: %w", err)
}
- cmp.Deployment, err = appsv1.NewDeployment(ctx, "ledger-worker", &appsv1.DeploymentArgs{
- Metadata: &metav1.ObjectMetaArgs{
- Namespace: args.Namespace.ToOutput(ctx.Context()).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-worker"),
- },
- },
- Template: &corev1.PodTemplateSpecArgs{
- Metadata: &metav1.ObjectMetaArgs{
- Labels: pulumi.StringMap{
- "com.formance.stack/app": pulumi.String("ledger-worker"),
- },
- },
- Spec: corev1.PodSpecArgs{
- TerminationGracePeriodSeconds: args.TerminationGracePeriodSeconds.ToOutput(ctx.Context()).Untyped().(pulumi.IntPtrOutput),
- Containers: corev1.ContainerArray{
- corev1.ContainerArgs{
- Name: pulumi.String("worker"),
- Image: utils.GetMainImage(args.Tag),
- ImagePullPolicy: args.ImagePullPolicy.ToOutput(ctx.Context()).Untyped().(pulumi.StringOutput),
- Args: pulumi.StringArray{
- pulumi.String("worker"),
- },
- Env: envVars,
- },
- },
- },
- },
- },
- }, pulumi.Parent(cmp))
+ cmp.Service, err = createService(ctx, args, cmp.Deployment, pulumi.Parent(cmp))
if err != nil {
- return nil, fmt.Errorf("creating deployment: %w", err)
+ return nil, fmt.Errorf("creating service: %w", err)
}
if err := ctx.RegisterResourceOutputs(cmp, pulumi.Map{}); err != nil {
@@ -101,4 +55,4 @@ func NewComponent(ctx *pulumi.Context, name string, args ComponentArgs, opts ...
}
return cmp, nil
-}
+}
\ No newline at end of file
diff --git a/deployments/pulumi/pkg/worker/deployment.go b/deployments/pulumi/pkg/worker/deployment.go
new file mode 100644
index 0000000000..f68da208b3
--- /dev/null
+++ b/deployments/pulumi/pkg/worker/deployment.go
@@ -0,0 +1,60 @@
+package worker
+
+import (
+ "github.com/formancehq/ledger/deployments/pulumi/pkg/utils"
+ appsv1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/apps/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"
+)
+
+func createDeployment(ctx *pulumi.Context, args ComponentArgs, resourceOptions ...pulumi.ResourceOption) (*appsv1.Deployment, error) {
+ envVars := corev1.EnvVarArray{}
+ envVars = append(envVars, corev1.EnvVarArgs{
+ Name: pulumi.String("DEBUG"),
+ Value: utils.BoolToString(args.Debug).Untyped().(pulumi.StringOutput),
+ })
+
+ envVars = append(envVars, args.Database.GetEnvVars()...)
+ if otel := args.Monitoring; otel != nil {
+ envVars = append(envVars, args.Monitoring.GetEnvVars(ctx)...)
+ }
+
+ return appsv1.NewDeployment(ctx, "ledger-worker", &appsv1.DeploymentArgs{
+ Metadata: &metav1.ObjectMetaArgs{
+ Namespace: args.Namespace.ToOutput(ctx.Context()).Untyped().(pulumi.StringOutput),
+ Labels: pulumi.StringMap{
+ "com.formance.stack/app": pulumi.String("ledger-worker"),
+ },
+ },
+ Spec: appsv1.DeploymentSpecArgs{
+ Replicas: pulumi.Int(1),
+ Selector: &metav1.LabelSelectorArgs{
+ MatchLabels: pulumi.StringMap{
+ "com.formance.stack/app": pulumi.String("ledger-worker"),
+ },
+ },
+ Template: &corev1.PodTemplateSpecArgs{
+ Metadata: &metav1.ObjectMetaArgs{
+ Labels: pulumi.StringMap{
+ "com.formance.stack/app": pulumi.String("ledger-worker"),
+ },
+ },
+ Spec: corev1.PodSpecArgs{
+ TerminationGracePeriodSeconds: args.TerminationGracePeriodSeconds.ToOutput(ctx.Context()).Untyped().(pulumi.IntPtrOutput),
+ Containers: corev1.ContainerArray{
+ corev1.ContainerArgs{
+ Name: pulumi.String("worker"),
+ Image: utils.GetMainImage(args.Tag),
+ ImagePullPolicy: args.ImagePullPolicy.ToOutput(ctx.Context()).Untyped().(pulumi.StringOutput),
+ Args: pulumi.StringArray{
+ pulumi.String("worker"),
+ },
+ Env: envVars,
+ },
+ },
+ },
+ },
+ },
+ }, resourceOptions...)
+}
diff --git a/deployments/pulumi/pkg/worker/service.go b/deployments/pulumi/pkg/worker/service.go
new file mode 100644
index 0000000000..2fc0b685db
--- /dev/null
+++ b/deployments/pulumi/pkg/worker/service.go
@@ -0,0 +1,28 @@
+package worker
+
+import (
+ appsv1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/apps/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"
+)
+
+func createService(ctx *pulumi.Context, args ComponentArgs, deployment *appsv1.Deployment, opts ...pulumi.ResourceOption) (*corev1.Service, error) {
+ return corev1.NewService(ctx, "ledger-worker", &corev1.ServiceArgs{
+ Metadata: &metav1.ObjectMetaArgs{
+ Namespace: args.Namespace.ToOutput(ctx.Context()).Untyped().(pulumi.StringOutput),
+ },
+ Spec: &corev1.ServiceSpecArgs{
+ Selector: deployment.Spec.Selector().MatchLabels(),
+ Type: pulumi.String("ClusterIP"),
+ Ports: corev1.ServicePortArray{
+ corev1.ServicePortArgs{
+ Port: pulumi.Int(8081),
+ TargetPort: pulumi.Int(8081),
+ Protocol: pulumi.String("TCP"),
+ Name: pulumi.String("grpc"),
+ },
+ },
+ },
+ }, opts...)
+}
diff --git a/deployments/pulumi/script.js b/deployments/pulumi/script.js
new file mode 100644
index 0000000000..550b6a7ce4
--- /dev/null
+++ b/deployments/pulumi/script.js
@@ -0,0 +1,84 @@
+const nbPsps = 10;
+const nbOrganizations = 100;
+const nbSellers = 1000;
+const nbUsers = 10000;
+const nbAssets = 3;
+
+const plain = `
+vars {
+ account $order
+ account $buyer
+ account $seller
+ account $psp
+ account $fees_account
+
+ monetary $amount
+ portion $fees
+ string $due_date
+ string $status
+}
+
+send $amount (
+ source = $psp allowing unbounded overdraft
+ destination = $buyer
+)
+
+send $amount (
+ source = $buyer
+ destination = $order
+)
+
+send $amount (
+ source = $order
+ destination = {
+ $fees to $fees_account
+ remaining to $seller
+ }
+)
+
+set_account_meta($order, "due_date", $due_date)
+set_tx_meta("status", $status)
+`
+
+function next(iteration) {
+ const dueDate = new Date();
+ const offset = Math.floor((Math.random() - 0.5) * 100000);
+ dueDate.setSeconds(dueDate.getSeconds() + offset);
+
+ const orderID = uuid();
+ const organizationID = Math.floor(Math.random() * nbOrganizations);
+ const userID = Math.floor(Math.random() * nbUsers);
+ const sellerID = Math.floor(Math.random() * nbSellers);
+
+ const status = offset > 0 ? 'PENDING' : 'COMPLETED';
+ const psp = `organizations:${organizationID}:psp:${Math.floor(Math.random() * nbPsps)}`;
+ const order = `organizations:${organizationID}:orders:${orderID}`;
+ const buyer = `organizations:${organizationID}:users:${userID}`;
+ const seller = `organizations:${organizationID}:sellers:${sellerID}`;
+ const amount = `ASSET${Math.floor(Math.random() * nbAssets)} ${Math.floor(Math.random() * 10000000000)}`;
+ const fees = `${Math.floor(Math.random() * 10)}%`;
+ const fees_account = `organizations:${organizationID}:fees`;
+
+ return [{
+ action: 'CREATE_TRANSACTION',
+ data: {
+ script: {
+ plain,
+ vars: {
+ order,
+ buyer,
+ seller,
+ psp,
+ fees_account,
+ amount,
+ fees,
+ due_date: dueDate.toISOString(),
+ status
+ }
+ },
+ metadata: {
+ "iteration": `${iteration}`
+ }
+ }
+ }]
+}
\ No newline at end of file
diff --git a/docs/api/README.md b/docs/api/README.md
index d364eb1eaa..36845350ed 100644
--- a/docs/api/README.md
+++ b/docs/api/README.md
@@ -2079,8 +2079,829 @@ To perform this operation, you must be authenticated by means of one of the foll
Authorization ( Scopes: ledger:write )
+## List exporters
+
+
+
+> Code samples
+
+```http
+GET http://localhost:8080/v2/_/exporters HTTP/1.1
+Host: localhost:8080
+Accept: application/json
+
+```
+
+`GET /v2/_/exporters`
+
+> Example responses
+
+> 200 Response
+
+```json
+{
+ "cursor": {
+ "cursor": {
+ "pageSize": 15,
+ "hasMore": false,
+ "previous": "YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol=",
+ "next": "",
+ "data": [
+ {
+ "driver": "string",
+ "config": {},
+ "id": "string",
+ "createdAt": "2019-08-24T14:15:22Z"
+ }
+ ]
+ },
+ "data": [
+ {
+ "driver": "string",
+ "config": {},
+ "id": "string",
+ "createdAt": "2019-08-24T14:15:22Z"
+ }
+ ]
+ }
+}
+```
+
+
Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Exporters list|Inline|
+|default|Default|Error|[V2ErrorResponse](#schemav2errorresponse)|
+
+Response Schema
+
+Status Code **200**
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|» cursor|any|false|none|none|
+
+*allOf*
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|»» *anonymous*|[V2ExportersCursorResponse](#schemav2exporterscursorresponse)|false|none|none|
+|»»» cursor|object|true|none|none|
+|»»»» pageSize|integer(int64)|true|none|none|
+|»»»» hasMore|boolean|true|none|none|
+|»»»» previous|string|false|none|none|
+|»»»» next|string|false|none|none|
+|»»»» data|[allOf]|true|none|none|
+
+*allOf*
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|»»»»» *anonymous*|[V2ExporterConfiguration](#schemav2exporterconfiguration)|false|none|none|
+|»»»»»» driver|string|true|none|none|
+|»»»»»» config|object|true|none|none|
+
+*and*
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|»»»»» *anonymous*|object|false|none|none|
+|»»»»»» id|string|true|none|none|
+|»»»»»» createdAt|string(date-time)|true|none|none|
+
+*and*
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|»» *anonymous*|object|false|none|none|
+|»»» data|[allOf]|false|none|none|
+
+
+
+## Create exporter
+
+
+
+> Code samples
+
+```http
+POST http://localhost:8080/v2/_/exporters HTTP/1.1
+Host: localhost:8080
+Content-Type: application/json
+Accept: application/json
+
+```
+
+`POST /v2/_/exporters`
+
+> Body parameter
+
+```json
+{
+ "driver": "string",
+ "config": {}
+}
+```
+
+Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|body|body|[V2ExporterConfiguration](#schemav2exporterconfiguration)|true|none|
+
+> Example responses
+
+> 201 Response
+
+```json
+{
+ "data": {
+ "driver": "string",
+ "config": {},
+ "id": "string",
+ "createdAt": "2019-08-24T14:15:22Z"
+ }
+}
+```
+
+Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created exporter|Inline|
+|default|Default|Error|[V2ErrorResponse](#schemav2errorresponse)|
+
+Response Schema
+
+Status Code **201**
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|» data|[V2Exporter](#schemav2exporter)|true|none|none|
+
+*allOf*
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|»» *anonymous*|[V2ExporterConfiguration](#schemav2exporterconfiguration)|false|none|none|
+|»»» driver|string|true|none|none|
+|»»» config|object|true|none|none|
+
+*and*
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|»» *anonymous*|object|false|none|none|
+|»»» id|string|true|none|none|
+|»»» createdAt|string(date-time)|true|none|none|
+
+
+
+## Get exporter state
+
+
+
+> Code samples
+
+```http
+GET http://localhost:8080/v2/_/exporters/{exporterID} HTTP/1.1
+Host: localhost:8080
+Accept: application/json
+
+```
+
+`GET /v2/_/exporters/{exporterID}`
+
+Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|exporterID|path|string|true|The exporter id|
+
+> Example responses
+
+> 200 Response
+
+```json
+{
+ "data": {
+ "driver": "string",
+ "config": {},
+ "id": "string",
+ "createdAt": "2019-08-24T14:15:22Z"
+ }
+}
+```
+
+Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Exporter information|Inline|
+|default|Default|Error|[V2ErrorResponse](#schemav2errorresponse)|
+
+Response Schema
+
+Status Code **200**
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|» data|[V2Exporter](#schemav2exporter)|true|none|none|
+
+*allOf*
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|»» *anonymous*|[V2ExporterConfiguration](#schemav2exporterconfiguration)|false|none|none|
+|»»» driver|string|true|none|none|
+|»»» config|object|true|none|none|
+
+*and*
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|»» *anonymous*|object|false|none|none|
+|»»» id|string|true|none|none|
+|»»» createdAt|string(date-time)|true|none|none|
+
+
+
+## Delete exporter
+
+
+
+> Code samples
+
+```http
+DELETE http://localhost:8080/v2/_/exporters/{exporterID} HTTP/1.1
+Host: localhost:8080
+Accept: application/json
+
+```
+
+`DELETE /v2/_/exporters/{exporterID}`
+
+Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|exporterID|path|string|true|The exporter id|
+
+> Example responses
+
+> default Response
+
+```json
+{
+ "errorCode": "VALIDATION",
+ "errorMessage": "[VALIDATION] invalid 'cursor' query param",
+ "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"
+}
+```
+
+Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|204|[No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5)|Exporter deleted|None|
+|default|Default|Error|[V2ErrorResponse](#schemav2errorresponse)|
+
+
+
+## List pipelines
+
+
+
+> Code samples
+
+```http
+GET http://localhost:8080/v2/{ledger}/pipelines HTTP/1.1
+Host: localhost:8080
+Accept: application/json
+
+```
+
+`GET /v2/{ledger}/pipelines`
+
+Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|ledger|path|string|true|Name of the ledger.|
+
+> Example responses
+
+> 200 Response
+
+```json
+{
+ "cursor": {
+ "cursor": {
+ "pageSize": 15,
+ "hasMore": false,
+ "previous": "YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol=",
+ "next": "",
+ "data": [
+ {
+ "id": "string",
+ "createdAt": "2019-08-24T14:15:22Z",
+ "lastLogID": 0,
+ "enabled": true
+ }
+ ]
+ },
+ "data": [
+ {
+ "id": "string",
+ "createdAt": "2019-08-24T14:15:22Z",
+ "lastLogID": 0,
+ "enabled": true
+ }
+ ]
+ }
+}
+```
+
+Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Pipelines list|Inline|
+|default|Default|Error|[V2ErrorResponse](#schemav2errorresponse)|
+
+Response Schema
+
+Status Code **200**
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|» cursor|any|false|none|none|
+
+*allOf*
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|»» *anonymous*|[V2PipelinesCursorResponse](#schemav2pipelinescursorresponse)|false|none|none|
+|»»» cursor|object|true|none|none|
+|»»»» pageSize|integer(int64)|true|none|none|
+|»»»» hasMore|boolean|true|none|none|
+|»»»» previous|string|false|none|none|
+|»»»» next|string|false|none|none|
+|»»»» data|[allOf]|true|none|none|
+
+*allOf*
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|»»»»» *anonymous*|object|false|none|none|
+|»»»»»» ledger|string|true|none|none|
+|»»»»»» exporterID|string|true|none|none|
+
+*and*
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|»»»»» *anonymous*|object|false|none|none|
+|»»»»»» id|string|true|none|none|
+|»»»»»» createdAt|string(date-time)|true|none|none|
+|»»»»»» lastLogID|integer|false|none|none|
+|»»»»»» enabled|boolean|false|none|none|
+
+*and*
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|»» *anonymous*|object|false|none|none|
+|»»» data|[allOf]|false|none|none|
+
+
+
+## Create pipeline
+
+
+
+> Code samples
+
+```http
+POST http://localhost:8080/v2/{ledger}/pipelines HTTP/1.1
+Host: localhost:8080
+Content-Type: application/json
+Accept: application/json
+
+```
+
+`POST /v2/{ledger}/pipelines`
+
+> Body parameter
+
+```json
+{
+ "exporterID": "string"
+}
+```
+
+Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|body|body|[V2CreatePipelineRequest](#schemav2createpipelinerequest)|false|none|
+|ledger|path|string|true|Name of the ledger.|
+
+> Example responses
+
+> 201 Response
+
+```json
+{
+ "data": {
+ "id": "string",
+ "createdAt": "2019-08-24T14:15:22Z",
+ "lastLogID": 0,
+ "enabled": true
+ }
+}
+```
+
+Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created ipeline|Inline|
+|default|Default|Error|[V2ErrorResponse](#schemav2errorresponse)|
+
+Response Schema
+
+Status Code **201**
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|» data|any|true|none|none|
+
+*allOf*
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|»» *anonymous*|object|false|none|none|
+|»»» ledger|string|true|none|none|
+|»»» exporterID|string|true|none|none|
+
+*and*
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|»» *anonymous*|object|false|none|none|
+|»»» id|string|true|none|none|
+|»»» createdAt|string(date-time)|true|none|none|
+|»»» lastLogID|integer|false|none|none|
+|»»» enabled|boolean|false|none|none|
+
+
+
+## Get pipeline state
+
+
+
+> Code samples
+
+```http
+GET http://localhost:8080/v2/{ledger}/pipelines/{pipelineID} HTTP/1.1
+Host: localhost:8080
+Accept: application/json
+
+```
+
+`GET /v2/{ledger}/pipelines/{pipelineID}`
+
+Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|ledger|path|string|true|Name of the ledger.|
+|pipelineID|path|string|true|The pipeline id|
+
+> Example responses
+
+> 200 Response
+
+```json
+{
+ "data": {
+ "id": "string",
+ "createdAt": "2019-08-24T14:15:22Z",
+ "lastLogID": 0,
+ "enabled": true
+ }
+}
+```
+
+Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Pipeline information|Inline|
+|default|Default|Error|[V2ErrorResponse](#schemav2errorresponse)|
+
+Response Schema
+
+Status Code **200**
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|» data|any|true|none|none|
+
+*allOf*
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|»» *anonymous*|object|false|none|none|
+|»»» ledger|string|true|none|none|
+|»»» exporterID|string|true|none|none|
+
+*and*
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|»» *anonymous*|object|false|none|none|
+|»»» id|string|true|none|none|
+|»»» createdAt|string(date-time)|true|none|none|
+|»»» lastLogID|integer|false|none|none|
+|»»» enabled|boolean|false|none|none|
+
+
+
+## Delete pipeline
+
+
+
+> Code samples
+
+```http
+DELETE http://localhost:8080/v2/{ledger}/pipelines/{pipelineID} HTTP/1.1
+Host: localhost:8080
+Accept: application/json
+
+```
+
+`DELETE /v2/{ledger}/pipelines/{pipelineID}`
+
+Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|ledger|path|string|true|Name of the ledger.|
+|pipelineID|path|string|true|The pipeline id|
+
+> Example responses
+
+> default Response
+
+```json
+{
+ "errorCode": "VALIDATION",
+ "errorMessage": "[VALIDATION] invalid 'cursor' query param",
+ "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"
+}
+```
+
+Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|204|[No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5)|Pipeline deleted|None|
+|default|Default|Error|[V2ErrorResponse](#schemav2errorresponse)|
+
+
+
+## Reset pipeline
+
+
+
+> Code samples
+
+```http
+POST http://localhost:8080/v2/{ledger}/pipelines/{pipelineID}/reset HTTP/1.1
+Host: localhost:8080
+Accept: application/json
+
+```
+
+`POST /v2/{ledger}/pipelines/{pipelineID}/reset`
+
+Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|ledger|path|string|true|Name of the ledger.|
+|pipelineID|path|string|true|The pipeline id|
+
+> Example responses
+
+> default Response
+
+```json
+{
+ "errorCode": "VALIDATION",
+ "errorMessage": "[VALIDATION] invalid 'cursor' query param",
+ "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"
+}
+```
+
+Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|202|[Accepted](https://tools.ietf.org/html/rfc7231#section-6.3.3)|Pipeline reset|None|
+|default|Default|Error|[V2ErrorResponse](#schemav2errorresponse)|
+
+
+
+## Start pipeline
+
+
+
+> Code samples
+
+```http
+POST http://localhost:8080/v2/{ledger}/pipelines/{pipelineID}/start HTTP/1.1
+Host: localhost:8080
+Accept: application/json
+
+```
+
+`POST /v2/{ledger}/pipelines/{pipelineID}/start`
+
+Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|ledger|path|string|true|Name of the ledger.|
+|pipelineID|path|string|true|The pipeline id|
+
+> Example responses
+
+> default Response
+
+```json
+{
+ "errorCode": "VALIDATION",
+ "errorMessage": "[VALIDATION] invalid 'cursor' query param",
+ "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"
+}
+```
+
+Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|202|[Accepted](https://tools.ietf.org/html/rfc7231#section-6.3.3)|Pipeline started|None|
+|default|Default|Error|[V2ErrorResponse](#schemav2errorresponse)|
+
+
+
+## Stop pipeline
+
+
+
+> Code samples
+
+```http
+POST http://localhost:8080/v2/{ledger}/pipelines/{pipelineID}/stop HTTP/1.1
+Host: localhost:8080
+Accept: application/json
+
+```
+
+`POST /v2/{ledger}/pipelines/{pipelineID}/stop`
+
+Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|ledger|path|string|true|Name of the ledger.|
+|pipelineID|path|string|true|The pipeline id|
+
+> Example responses
+
+> default Response
+
+```json
+{
+ "errorCode": "VALIDATION",
+ "errorMessage": "[VALIDATION] invalid 'cursor' query param",
+ "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"
+}
+```
+
+Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|202|[Accepted](https://tools.ietf.org/html/rfc7231#section-6.3.3)|Pipeline stopped|None|
+|default|Default|Error|[V2ErrorResponse](#schemav2errorresponse)|
+
+
+
# Schemas
+V2ExportersCursorResponse
+
+
+
+
+
+
+```json
+{
+ "cursor": {
+ "pageSize": 15,
+ "hasMore": false,
+ "previous": "YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol=",
+ "next": "",
+ "data": [
+ {
+ "driver": "string",
+ "config": {},
+ "id": "string",
+ "createdAt": "2019-08-24T14:15:22Z"
+ }
+ ]
+ }
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|cursor|object|true|none|none|
+|» pageSize|integer(int64)|true|none|none|
+|» hasMore|boolean|true|none|none|
+|» previous|string|false|none|none|
+|» next|string|false|none|none|
+|» data|[[V2Exporter](#schemav2exporter)]|true|none|none|
+
+V2PipelinesCursorResponse
+
+
+
+
+
+
+```json
+{
+ "cursor": {
+ "pageSize": 15,
+ "hasMore": false,
+ "previous": "YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol=",
+ "next": "",
+ "data": [
+ {
+ "id": "string",
+ "createdAt": "2019-08-24T14:15:22Z",
+ "lastLogID": 0,
+ "enabled": true
+ }
+ ]
+ }
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|cursor|object|true|none|none|
+|» pageSize|integer(int64)|true|none|none|
+|» hasMore|boolean|true|none|none|
+|» previous|string|false|none|none|
+|» next|string|false|none|none|
+|» data|[[V2Pipeline](#schemav2pipeline)]|true|none|none|
+
V2AccountsCursorResponse
@@ -4529,3 +5350,154 @@ and
|---|---|---|---|---|
|file|string(binary)|true|none|none|
+V2CreatePipelineRequest
+
+
+
+
+
+
+```json
+{
+ "exporterID": "string"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|exporterID|string|true|none|none|
+
+V2CreateExporterRequest
+
+
+
+
+
+
+```json
+{
+ "driver": "string",
+ "config": {}
+}
+
+```
+
+### Properties
+
+*None*
+
+V2PipelineConfiguration
+
+
+
+
+
+
+```json
+{
+ "ledger": "string",
+ "exporterID": "string"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|ledger|string|true|none|none|
+|exporterID|string|true|none|none|
+
+V2ExporterConfiguration
+
+
+
+
+
+
+```json
+{
+ "driver": "string",
+ "config": {}
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|driver|string|true|none|none|
+|config|object|true|none|none|
+
+V2Exporter
+
+
+
+
+
+
+```json
+{
+ "driver": "string",
+ "config": {},
+ "id": "string",
+ "createdAt": "2019-08-24T14:15:22Z"
+}
+
+```
+
+### Properties
+
+allOf
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|*anonymous*|[V2ExporterConfiguration](#schemav2exporterconfiguration)|false|none|none|
+
+and
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|*anonymous*|object|false|none|none|
+|» id|string|true|none|none|
+|» createdAt|string(date-time)|true|none|none|
+
+V2Pipeline
+
+
+
+
+
+
+```json
+{
+ "id": "string",
+ "createdAt": "2019-08-24T14:15:22Z",
+ "lastLogID": 0,
+ "enabled": true
+}
+
+```
+
+### Properties
+
+allOf
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|*anonymous*|[V2PipelineConfiguration](#schemav2pipelineconfiguration)|false|none|none|
+
+and
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|*anonymous*|object|false|none|none|
+|» id|string|true|none|none|
+|» createdAt|string(date-time)|true|none|none|
+|» lastLogID|integer|false|none|none|
+|» enabled|boolean|false|none|none|
+
diff --git a/docs/events/CommittedTransactions.json b/docs/events/CommittedTransactions.json
index 0be89a80a9..ebc8404854 100644
--- a/docs/events/CommittedTransactions.json
+++ b/docs/events/CommittedTransactions.json
@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://github.com/formancehq/ledger/internal/bus/committed-transactions",
+ "$id": "https://github.com/formancehq/ledger/pkg/events/committed-transactions",
"$ref": "#/$defs/CommittedTransactions",
"$defs": {
"CommittedTransactions": {
diff --git a/docs/events/DeletedMetadata.json b/docs/events/DeletedMetadata.json
index 043315291e..1711f84501 100644
--- a/docs/events/DeletedMetadata.json
+++ b/docs/events/DeletedMetadata.json
@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://github.com/formancehq/ledger/internal/bus/deleted-metadata",
+ "$id": "https://github.com/formancehq/ledger/pkg/events/deleted-metadata",
"$ref": "#/$defs/DeletedMetadata",
"$defs": {
"DeletedMetadata": {
diff --git a/docs/events/RevertedTransaction.json b/docs/events/RevertedTransaction.json
index 28f65f5b77..bad2dab862 100644
--- a/docs/events/RevertedTransaction.json
+++ b/docs/events/RevertedTransaction.json
@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://github.com/formancehq/ledger/internal/bus/reverted-transaction",
+ "$id": "https://github.com/formancehq/ledger/pkg/events/reverted-transaction",
"$ref": "#/$defs/RevertedTransaction",
"$defs": {
"Int": {
diff --git a/docs/events/SavedMetadata.json b/docs/events/SavedMetadata.json
index c0b6f0c31c..be42794d45 100644
--- a/docs/events/SavedMetadata.json
+++ b/docs/events/SavedMetadata.json
@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://github.com/formancehq/ledger/internal/bus/saved-metadata",
+ "$id": "https://github.com/formancehq/ledger/pkg/events/saved-metadata",
"$ref": "#/$defs/SavedMetadata",
"$defs": {
"Metadata": {
diff --git a/flake.nix b/flake.nix
index e251c44be4..77d37e5831 100644
--- a/flake.nix
+++ b/flake.nix
@@ -103,6 +103,9 @@
nodejs_22
self.packages.${system}.speakeasy
goperf
+ protobuf_27
+ protoc-gen-go-grpc
+ protoc-gen-go
];
};
}
diff --git a/go.mod b/go.mod
index 36d5044cda..83db040004 100644
--- a/go.mod
+++ b/go.mod
@@ -28,7 +28,7 @@ require (
github.com/onsi/gomega v1.36.3
github.com/ory/dockertest/v3 v3.12.0
github.com/pborman/uuid v1.2.1
- github.com/pkg/errors v0.9.1 // indirect
+ github.com/pkg/errors v0.9.1
github.com/shomali11/xsql v0.0.0-20190608141458-bf76292144df
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.6
@@ -50,21 +50,35 @@ require (
)
require (
- github.com/formancehq/go-libs/v3 v3.0.0-20250422113236-ec98813a1539
+ github.com/ClickHouse/clickhouse-go/v2 v2.35.0
+ github.com/formancehq/go-libs/v3 v3.0.0-20250422121250-0689c5e8027f
+ github.com/lib/pq v1.10.9
+ github.com/mitchellh/mapstructure v1.5.0
+ github.com/olivere/elastic/v7 v7.0.32
github.com/robfig/cron/v3 v3.0.1
github.com/spf13/viper v1.20.1
+ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0
+ go.vallahaye.net/batcher v0.6.0
gopkg.in/yaml.v3 v3.0.1
)
require (
+ github.com/ClickHouse/ch-go v0.66.0 // indirect
+ github.com/andybalholm/brotli v1.1.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
+ github.com/go-faster/city v1.0.1 // indirect
+ github.com/go-faster/errors v0.7.1 // indirect
github.com/google/go-tpm v0.9.3 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/jackc/pgxlisten v0.0.0-20241106001234-1d6f6656415c // indirect
- github.com/moby/sys/user v0.3.0 // indirect
+ github.com/josharian/intern v1.0.0 // indirect
+ github.com/moby/sys/user v0.4.0 // indirect
+ github.com/paulmach/orb v0.11.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/sagikazarmark/locafero v0.9.0 // indirect
+ github.com/segmentio/asm v1.2.0 // indirect
+ github.com/shopspring/decimal v1.4.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.14.0 // indirect
github.com/spf13/cast v1.8.0 // indirect
@@ -215,7 +229,7 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
- golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
+ golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
@@ -223,8 +237,8 @@ require (
golang.org/x/tools v0.33.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect
- google.golang.org/grpc v1.72.1 // indirect
- google.golang.org/protobuf v1.36.6 // indirect
+ google.golang.org/grpc v1.72.1
+ google.golang.org/protobuf v1.36.6
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 b6b82dbb0b..5eb7301e5f 100644
--- a/go.sum
+++ b/go.sum
@@ -4,6 +4,10 @@ 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-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/ClickHouse/ch-go v0.66.0 h1:hLslxxAVb2PHpbHr4n0d6aP8CEIpUYGMVT1Yj/Q5Img=
+github.com/ClickHouse/ch-go v0.66.0/go.mod h1:noiHWyLMJAZ5wYuq3R/K0TcRhrNA8h7o1AqHX0klEhM=
+github.com/ClickHouse/clickhouse-go/v2 v2.35.0 h1:ZMLZqxu+NiW55f4JS32kzyEbMb7CthGn3ziCcULOvSE=
+github.com/ClickHouse/clickhouse-go/v2 v2.35.0/go.mod h1:O2FFT/rugdpGEW2VKyEGyMUWyQU0ahmenY9/emxLPxs=
github.com/IBM/sarama v1.45.1 h1:nY30XqYpqyXOXSNoe2XCgjj9jklGM1Ye94ierUb1jQ0=
github.com/IBM/sarama v1.45.1/go.mod h1:qifDhA3VWSrQ1TjSMyxDl3nYL3oX2C83u+G6L79sq4w=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
@@ -24,6 +28,8 @@ 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/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
+github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/antithesishq/antithesis-sdk-go v0.4.3-default-no-op h1:+OSa/t11TFhqfrX0EOSqQBDJ0YlpmK0rDSiB19dg9M0=
github.com/antithesishq/antithesis-sdk-go v0.4.3-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E=
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 h1:yL7+Jz0jTC6yykIK/Wh74gnTJnrGr5AyrNMXuA0gves=
@@ -108,8 +114,8 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
-github.com/formancehq/go-libs/v3 v3.0.0-20250422113236-ec98813a1539 h1:6kUkmD2GiZGB7TDpGaPas2ipaAKqP/os3PVk4XFVrpI=
-github.com/formancehq/go-libs/v3 v3.0.0-20250422113236-ec98813a1539/go.mod h1:mRr5/y0I64iJ4I+BXNkUy49izwrh3SA5L+MTWD1d/7Q=
+github.com/formancehq/go-libs/v3 v3.0.0-20250422121250-0689c5e8027f h1:/t3fKq/iXwo1KtFLE+2jtK3Ktm82OHqf6ZhuzHZWOos=
+github.com/formancehq/go-libs/v3 v3.0.0-20250422121250-0689c5e8027f/go.mod h1:T8GpmWRmRrS7Iy6tiz1gHsWMBUEOkCAIVhoXdJFM6Ns=
github.com/formancehq/numscript v0.0.16 h1:kNNpPTmTvhRUrMXonZPMwUXUpJ06Io1WwC56Yf3nr1E=
github.com/formancehq/numscript v0.0.16/go.mod h1:8WhBIqcK6zu27njxy7ZG7CaDX0MHtI9qF9Ggfj07wfU=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
@@ -132,6 +138,10 @@ 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-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
+github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
+github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
+github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
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=
@@ -152,10 +162,14 @@ 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.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
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.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc=
@@ -226,12 +240,18 @@ 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 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+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.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+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/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
@@ -256,12 +276,15 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
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/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
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/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
-github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
+github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
+github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
+github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
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=
@@ -278,6 +301,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/olivere/elastic/v7 v7.0.32 h1:R7CXvbu8Eq+WlsLgxmKVKPox0oOwAE/2T9Si5BnvK6E=
+github.com/olivere/elastic/v7 v7.0.32/go.mod h1:c7PVmLe3Fxq77PIfY/bZmxY/TAamBhCzZ8xDOE09a9k=
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
@@ -290,6 +315,9 @@ github.com/opencontainers/runc v1.2.3 h1:fxE7amCzfZflJO2lHXf4y/y8M1BoAqp+FVmG19o
github.com/opencontainers/runc v1.2.3/go.mod h1:nSxcWUydXrsBZVYNSkTjoQ/N6rcyTtn+1SD5D4+kRIM=
github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw=
github.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE=
+github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU=
+github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
+github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
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/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
@@ -320,6 +348,8 @@ 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/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
+github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
+github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/shirou/gopsutil/v4 v4.25.4 h1:cdtFO363VEOOFrUCjZRh4XVJkb548lyF0q0uTeMqYPw=
github.com/shirou/gopsutil/v4 v4.25.4/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA=
github.com/shomali11/parallelizer v0.0.0-20220717173222-a6776fbf40a9/go.mod h1:QsLM53l8gzX0sQbOjVir85bzOUucuJEF8JgE39wD7w0=
@@ -328,6 +358,8 @@ github.com/shomali11/util v0.0.0-20220717175126-f0771b70947f h1:OM0LVaVycWC+/j5Q
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/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
+github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
@@ -350,6 +382,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
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.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.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
@@ -363,6 +396,7 @@ 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.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
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=
@@ -395,8 +429,10 @@ github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240816141633-0a40785b4f41 h1:rnB8ZLM
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.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
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.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
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=
@@ -408,6 +444,9 @@ 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.8 h1:NwFghJfjaUW7tp+WE5mTLQQCfgseRsvgXjlSvk7x4t4=
github.com/xo/dburl v0.23.8/go.mod h1:uazlaAQxj4gkshhfuuYyvwCBouOmNnG2aDxTCFZpmL4=
+github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
+github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
+github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
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=
@@ -415,8 +454,11 @@ 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.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
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/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
go.opentelemetry.io/contrib/instrumentation/host v0.60.0 h1:LD6TMRg2hfNzkMD36Pq0jeYBcSP9W0aJt41Zmje43Ig=
go.opentelemetry.io/contrib/instrumentation/host v0.60.0/go.mod h1:GN4xnih1u2OQeRs8rNJ13XR8XsTqFopc57e/3Kf0h6c=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
@@ -467,15 +509,18 @@ 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=
+go.vallahaye.net/batcher v0.6.0 h1:aNqUGJyptsAiLYfS1qTPQO5Kh3wf4z57A3w+cpV4o/w=
+go.vallahaye.net/batcher v0.6.0/go.mod h1:7OX9A85hYVWrNgXKWkLjfKRoL6l04wLV0w4a8tNuDsI=
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.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
-golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
-golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
+golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
+golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
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=
@@ -485,6 +530,7 @@ golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
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.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
@@ -495,6 +541,7 @@ golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKl
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-20210220032951-036812b2e83c/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.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
@@ -506,6 +553,7 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w
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-20201204225414-ed752295db88/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-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -525,6 +573,7 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
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.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.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
@@ -549,9 +598,12 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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=
diff --git a/internal/README.md b/internal/README.md
index b47fc8267c..7eeb05b483 100644
--- a/internal/README.md
+++ b/internal/README.md
@@ -21,6 +21,7 @@ import "github.com/formancehq/ledger/internal"
- [type AccountMetadata](<#AccountMetadata>)
- [type AccountsVolumes](<#AccountsVolumes>)
- [type AggregatedVolumes](<#AggregatedVolumes>)
+- [type Balances](<#Balances>)
- [type BalancesByAssets](<#BalancesByAssets>)
- [type BalancesByAssetsByAccounts](<#BalancesByAssetsByAccounts>)
- [type Configuration](<#Configuration>)
@@ -33,12 +34,32 @@ import "github.com/formancehq/ledger/internal"
- [type DeletedMetadata](<#DeletedMetadata>)
- [func \(s DeletedMetadata\) Type\(\) LogType](<#DeletedMetadata.Type>)
- [func \(s \*DeletedMetadata\) UnmarshalJSON\(data \[\]byte\) error](<#DeletedMetadata.UnmarshalJSON>)
+- [type ErrAlreadyStarted](<#ErrAlreadyStarted>)
+ - [func NewErrAlreadyStarted\(id string\) ErrAlreadyStarted](<#NewErrAlreadyStarted>)
+ - [func \(e ErrAlreadyStarted\) Error\(\) string](<#ErrAlreadyStarted.Error>)
+ - [func \(e ErrAlreadyStarted\) Is\(err error\) bool](<#ErrAlreadyStarted.Is>)
+- [type ErrExporterUsed](<#ErrExporterUsed>)
+ - [func NewErrExporterUsed\(id string\) ErrExporterUsed](<#NewErrExporterUsed>)
+ - [func \(e ErrExporterUsed\) Error\(\) string](<#ErrExporterUsed.Error>)
+ - [func \(e ErrExporterUsed\) Is\(err error\) bool](<#ErrExporterUsed.Is>)
- [type ErrInvalidBucketName](<#ErrInvalidBucketName>)
- [func \(e ErrInvalidBucketName\) Error\(\) string](<#ErrInvalidBucketName.Error>)
- [func \(e ErrInvalidBucketName\) Is\(err error\) bool](<#ErrInvalidBucketName.Is>)
- [type ErrInvalidLedgerName](<#ErrInvalidLedgerName>)
- [func \(e ErrInvalidLedgerName\) Error\(\) string](<#ErrInvalidLedgerName.Error>)
- [func \(e ErrInvalidLedgerName\) Is\(err error\) bool](<#ErrInvalidLedgerName.Is>)
+- [type ErrPipelineAlreadyExists](<#ErrPipelineAlreadyExists>)
+ - [func NewErrPipelineAlreadyExists\(pipelineConfiguration PipelineConfiguration\) ErrPipelineAlreadyExists](<#NewErrPipelineAlreadyExists>)
+ - [func \(e ErrPipelineAlreadyExists\) Error\(\) string](<#ErrPipelineAlreadyExists.Error>)
+ - [func \(e ErrPipelineAlreadyExists\) Is\(err error\) bool](<#ErrPipelineAlreadyExists.Is>)
+- [type ErrPipelineNotFound](<#ErrPipelineNotFound>)
+ - [func NewErrPipelineNotFound\(id string\) ErrPipelineNotFound](<#NewErrPipelineNotFound>)
+ - [func \(e ErrPipelineNotFound\) Error\(\) string](<#ErrPipelineNotFound.Error>)
+ - [func \(e ErrPipelineNotFound\) Is\(err error\) bool](<#ErrPipelineNotFound.Is>)
+- [type Exporter](<#Exporter>)
+ - [func NewExporter\(configuration ExporterConfiguration\) Exporter](<#NewExporter>)
+- [type ExporterConfiguration](<#ExporterConfiguration>)
+ - [func NewExporterConfiguration\(driver string, config json.RawMessage\) ExporterConfiguration](<#NewExporterConfiguration>)
- [type Ledger](<#Ledger>)
- [func MustNewWithDefault\(name string\) Ledger](<#MustNewWithDefault>)
- [func New\(name string, configuration Configuration\) \(\*Ledger, error\)](<#New>)
@@ -66,6 +87,11 @@ import "github.com/formancehq/ledger/internal"
- [type Move](<#Move>)
- [type Moves](<#Moves>)
- [func \(m Moves\) ComputePostCommitEffectiveVolumes\(\) PostCommitVolumes](<#Moves.ComputePostCommitEffectiveVolumes>)
+- [type Pipeline](<#Pipeline>)
+ - [func NewPipeline\(pipelineConfiguration PipelineConfiguration\) Pipeline](<#NewPipeline>)
+- [type PipelineConfiguration](<#PipelineConfiguration>)
+ - [func NewPipelineConfiguration\(ledger, exporterID string\) PipelineConfiguration](<#NewPipelineConfiguration>)
+ - [func \(p PipelineConfiguration\) String\(\) string](<#PipelineConfiguration.String>)
- [type PostCommitVolumes](<#PostCommitVolumes>)
- [func \(a PostCommitVolumes\) AddInput\(account, asset string, input \*big.Int\)](<#PostCommitVolumes.AddInput>)
- [func \(a PostCommitVolumes\) AddOutput\(account, asset string, output \*big.Int\)](<#PostCommitVolumes.AddOutput>)
@@ -285,6 +311,15 @@ type AggregatedVolumes struct {
}
```
+
+## type [Balances]()
+
+
+
+```go
+type Balances = map[string]map[string]*big.Int
+```
+
## type [BalancesByAssets]()
@@ -404,8 +439,80 @@ func (s *DeletedMetadata) UnmarshalJSON(data []byte) error
+
+## type [ErrAlreadyStarted]()
+
+
+
+```go
+type ErrAlreadyStarted string
+```
+
+
+### func [NewErrAlreadyStarted]()
+
+```go
+func NewErrAlreadyStarted(id string) ErrAlreadyStarted
+```
+
+
+
+
+### func \(ErrAlreadyStarted\) [Error]()
+
+```go
+func (e ErrAlreadyStarted) Error() string
+```
+
+
+
+
+### func \(ErrAlreadyStarted\) [Is]()
+
+```go
+func (e ErrAlreadyStarted) Is(err error) bool
+```
+
+
+
+
+## type [ErrExporterUsed]()
+
+
+
+```go
+type ErrExporterUsed string
+```
+
+
+### func [NewErrExporterUsed]()
+
+```go
+func NewErrExporterUsed(id string) ErrExporterUsed
+```
+
+
+
+
+### func \(ErrExporterUsed\) [Error]()
+
+```go
+func (e ErrExporterUsed) Error() string
+```
+
+
+
+
+### func \(ErrExporterUsed\) [Is]()
+
+```go
+func (e ErrExporterUsed) Is(err error) bool
+```
+
+
+
-## type [ErrInvalidBucketName]()
+## type [ErrInvalidBucketName]()
@@ -416,7 +523,7 @@ type ErrInvalidBucketName struct {
```
-### func \(ErrInvalidBucketName\) [Error]()
+### func \(ErrInvalidBucketName\) [Error]()
```go
func (e ErrInvalidBucketName) Error() string
@@ -425,7 +532,7 @@ func (e ErrInvalidBucketName) Error() string
-### func \(ErrInvalidBucketName\) [Is]()
+### func \(ErrInvalidBucketName\) [Is]()
```go
func (e ErrInvalidBucketName) Is(err error) bool
@@ -434,7 +541,7 @@ func (e ErrInvalidBucketName) Is(err error) bool
-## type [ErrInvalidLedgerName]()
+## type [ErrInvalidLedgerName]()
@@ -445,7 +552,7 @@ type ErrInvalidLedgerName struct {
```
-### func \(ErrInvalidLedgerName\) [Error]()
+### func \(ErrInvalidLedgerName\) [Error]()
```go
func (e ErrInvalidLedgerName) Error() string
@@ -454,7 +561,7 @@ func (e ErrInvalidLedgerName) Error() string
-### func \(ErrInvalidLedgerName\) [Is]()
+### func \(ErrInvalidLedgerName\) [Is]()
```go
func (e ErrInvalidLedgerName) Is(err error) bool
@@ -462,6 +569,123 @@ func (e ErrInvalidLedgerName) Is(err error) bool
+
+## type [ErrPipelineAlreadyExists]()
+
+ErrPipelineAlreadyExists denotes a pipeline already created The store is in charge of returning this error on a failing call on Store.CreatePipeline
+
+```go
+type ErrPipelineAlreadyExists PipelineConfiguration
+```
+
+
+### func [NewErrPipelineAlreadyExists]()
+
+```go
+func NewErrPipelineAlreadyExists(pipelineConfiguration PipelineConfiguration) ErrPipelineAlreadyExists
+```
+
+
+
+
+### func \(ErrPipelineAlreadyExists\) [Error]()
+
+```go
+func (e ErrPipelineAlreadyExists) Error() string
+```
+
+
+
+
+### func \(ErrPipelineAlreadyExists\) [Is]()
+
+```go
+func (e ErrPipelineAlreadyExists) Is(err error) bool
+```
+
+
+
+
+## type [ErrPipelineNotFound]()
+
+
+
+```go
+type ErrPipelineNotFound string
+```
+
+
+### func [NewErrPipelineNotFound]()
+
+```go
+func NewErrPipelineNotFound(id string) ErrPipelineNotFound
+```
+
+
+
+
+### func \(ErrPipelineNotFound\) [Error]()
+
+```go
+func (e ErrPipelineNotFound) Error() string
+```
+
+
+
+
+### func \(ErrPipelineNotFound\) [Is]()
+
+```go
+func (e ErrPipelineNotFound) Is(err error) bool
+```
+
+
+
+
+## type [Exporter]()
+
+
+
+```go
+type Exporter struct {
+ bun.BaseModel `bun:"table:_system.exporters"`
+
+ ID string `json:"id" bun:"id,pk"`
+ CreatedAt time.Time `json:"createdAt" bun:"created_at"`
+ ExporterConfiguration
+}
+```
+
+
+### func [NewExporter]()
+
+```go
+func NewExporter(configuration ExporterConfiguration) Exporter
+```
+
+
+
+
+## type [ExporterConfiguration]()
+
+
+
+```go
+type ExporterConfiguration struct {
+ Driver string `json:"driver" bun:"driver"`
+ Config json.RawMessage `json:"config" bun:"config"`
+}
+```
+
+
+### func [NewExporterConfiguration]()
+
+```go
+func NewExporterConfiguration(driver string, config json.RawMessage) ExporterConfiguration
+```
+
+
+
## type [Ledger]()
@@ -752,6 +976,63 @@ func (m Moves) ComputePostCommitEffectiveVolumes() PostCommitVolumes
+
+## type [Pipeline]()
+
+
+
+```go
+type Pipeline struct {
+ bun.BaseModel `bun:"table:_system.pipelines"`
+
+ PipelineConfiguration
+ CreatedAt time.Time `json:"createdAt" bun:"created_at"`
+ ID string `json:"id" bun:"id,pk"`
+ Enabled bool `json:"enabled" bun:"enabled"`
+ LastLogID *uint64 `json:"lastLogID,omitempty" bun:"last_log_id"`
+ Error string `json:"error,omitempty" bun:"error"`
+}
+```
+
+
+### func [NewPipeline]()
+
+```go
+func NewPipeline(pipelineConfiguration PipelineConfiguration) Pipeline
+```
+
+
+
+
+## type [PipelineConfiguration]()
+
+
+
+```go
+type PipelineConfiguration struct {
+ Ledger string `json:"ledger" bun:"ledger"`
+ ExporterID string `json:"exporterID" bun:"exporter_id"`
+}
+```
+
+
+### func [NewPipelineConfiguration]()
+
+```go
+func NewPipelineConfiguration(ledger, exporterID string) PipelineConfiguration
+```
+
+
+
+
+### func \(PipelineConfiguration\) [String]()
+
+```go
+func (p PipelineConfiguration) String() string
+```
+
+
+
## type [PostCommitVolumes]()
diff --git a/internal/api/bulking/handler_json.go b/internal/api/bulking/handler_json.go
index b7c5b47e42..2a1bd1011a 100644
--- a/internal/api/bulking/handler_json.go
+++ b/internal/api/bulking/handler_json.go
@@ -9,6 +9,7 @@ import (
"github.com/formancehq/go-libs/v3/pointer"
"github.com/formancehq/ledger/internal/api/common"
ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
"net/http"
"slices"
)
@@ -103,7 +104,7 @@ func writeJSONResponse(w http.ResponseWriter, actions []string, results []BulkEl
errorCode = common.ErrMetadataOverride
case errors.Is(result.Error, ledgercontroller.ErrNoPostings):
errorCode = common.ErrNoPostings
- case errors.Is(result.Error, ledgercontroller.ErrTransactionReferenceConflict{}):
+ case errors.Is(result.Error, ledgerstore.ErrTransactionReferenceConflict{}):
errorCode = common.ErrConflict
case errors.Is(result.Error, ledgercontroller.ErrParsing{}):
errorCode = common.ErrInterpreterParse
diff --git a/internal/api/bulking/mocks_ledger_controller_test.go b/internal/api/bulking/mocks_ledger_controller_test.go
index b4e923b861..36d6e040b6 100644
--- a/internal/api/bulking/mocks_ledger_controller_test.go
+++ b/internal/api/bulking/mocks_ledger_controller_test.go
@@ -17,6 +17,7 @@ import (
ledger "github.com/formancehq/ledger/internal"
ledger0 "github.com/formancehq/ledger/internal/controller/ledger"
common "github.com/formancehq/ledger/internal/storage/common"
+ ledger1 "github.com/formancehq/ledger/internal/storage/ledger"
bun "github.com/uptrace/bun"
gomock "go.uber.org/mock/gomock"
)
@@ -181,7 +182,7 @@ func (mr *LedgerControllerMockRecorder) GetAccount(ctx, query any) *gomock.Call
}
// GetAggregatedBalances mocks base method.
-func (m *LedgerController) GetAggregatedBalances(ctx context.Context, q common.ResourceQuery[ledger0.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) {
+func (m *LedgerController) GetAggregatedBalances(ctx context.Context, q common.ResourceQuery[ledger1.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAggregatedBalances", ctx, q)
ret0, _ := ret[0].(ledger.BalancesByAssets)
@@ -241,7 +242,7 @@ func (mr *LedgerControllerMockRecorder) GetTransaction(ctx, query any) *gomock.C
}
// GetVolumesWithBalances mocks base method.
-func (m *LedgerController) GetVolumesWithBalances(ctx context.Context, q common.OffsetPaginatedQuery[ledger0.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) {
+func (m *LedgerController) GetVolumesWithBalances(ctx context.Context, q common.OffsetPaginatedQuery[ledger1.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])
@@ -269,6 +270,20 @@ func (mr *LedgerControllerMockRecorder) Import(ctx, stream any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Import", reflect.TypeOf((*LedgerController)(nil).Import), ctx, stream)
}
+// Info mocks base method.
+func (m *LedgerController) Info() ledger.Ledger {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Info")
+ ret0, _ := ret[0].(ledger.Ledger)
+ return ret0
+}
+
+// Info indicates an expected call of Info.
+func (mr *LedgerControllerMockRecorder) Info() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*LedgerController)(nil).Info))
+}
+
// IsDatabaseUpToDate mocks base method.
func (m *LedgerController) IsDatabaseUpToDate(ctx context.Context) (bool, error) {
m.ctrl.T.Helper()
diff --git a/internal/api/common/mocks_ledger_controller_test.go b/internal/api/common/mocks_ledger_controller_test.go
index 15cfa1949e..621ee926c8 100644
--- a/internal/api/common/mocks_ledger_controller_test.go
+++ b/internal/api/common/mocks_ledger_controller_test.go
@@ -17,6 +17,7 @@ import (
ledger "github.com/formancehq/ledger/internal"
ledger0 "github.com/formancehq/ledger/internal/controller/ledger"
common "github.com/formancehq/ledger/internal/storage/common"
+ ledger1 "github.com/formancehq/ledger/internal/storage/ledger"
bun "github.com/uptrace/bun"
gomock "go.uber.org/mock/gomock"
)
@@ -181,7 +182,7 @@ func (mr *LedgerControllerMockRecorder) GetAccount(ctx, query any) *gomock.Call
}
// GetAggregatedBalances mocks base method.
-func (m *LedgerController) GetAggregatedBalances(ctx context.Context, q common.ResourceQuery[ledger0.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) {
+func (m *LedgerController) GetAggregatedBalances(ctx context.Context, q common.ResourceQuery[ledger1.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAggregatedBalances", ctx, q)
ret0, _ := ret[0].(ledger.BalancesByAssets)
@@ -241,7 +242,7 @@ func (mr *LedgerControllerMockRecorder) GetTransaction(ctx, query any) *gomock.C
}
// GetVolumesWithBalances mocks base method.
-func (m *LedgerController) GetVolumesWithBalances(ctx context.Context, q common.OffsetPaginatedQuery[ledger0.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) {
+func (m *LedgerController) GetVolumesWithBalances(ctx context.Context, q common.OffsetPaginatedQuery[ledger1.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])
@@ -269,6 +270,20 @@ func (mr *LedgerControllerMockRecorder) Import(ctx, stream any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Import", reflect.TypeOf((*LedgerController)(nil).Import), ctx, stream)
}
+// Info mocks base method.
+func (m *LedgerController) Info() ledger.Ledger {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Info")
+ ret0, _ := ret[0].(ledger.Ledger)
+ return ret0
+}
+
+// Info indicates an expected call of Info.
+func (mr *LedgerControllerMockRecorder) Info() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*LedgerController)(nil).Info))
+}
+
// IsDatabaseUpToDate mocks base method.
func (m *LedgerController) IsDatabaseUpToDate(ctx context.Context) (bool, error) {
m.ctrl.T.Helper()
diff --git a/internal/api/common/mocks_system_controller_test.go b/internal/api/common/mocks_system_controller_test.go
index e8ded33b63..d5d5d16b20 100644
--- a/internal/api/common/mocks_system_controller_test.go
+++ b/internal/api/common/mocks_system_controller_test.go
@@ -15,9 +15,194 @@ import (
ledger "github.com/formancehq/ledger/internal"
ledger0 "github.com/formancehq/ledger/internal/controller/ledger"
common "github.com/formancehq/ledger/internal/storage/common"
+ system "github.com/formancehq/ledger/internal/storage/system"
gomock "go.uber.org/mock/gomock"
)
+// MockReplicationBackend is a mock of ReplicationBackend interface.
+type MockReplicationBackend struct {
+ ctrl *gomock.Controller
+ recorder *MockReplicationBackendMockRecorder
+ isgomock struct{}
+}
+
+// MockReplicationBackendMockRecorder is the mock recorder for MockReplicationBackend.
+type MockReplicationBackendMockRecorder struct {
+ mock *MockReplicationBackend
+}
+
+// NewMockReplicationBackend creates a new mock instance.
+func NewMockReplicationBackend(ctrl *gomock.Controller) *MockReplicationBackend {
+ mock := &MockReplicationBackend{ctrl: ctrl}
+ mock.recorder = &MockReplicationBackendMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockReplicationBackend) EXPECT() *MockReplicationBackendMockRecorder {
+ return m.recorder
+}
+
+// CreateExporter mocks base method.
+func (m *MockReplicationBackend) CreateExporter(ctx context.Context, configuration ledger.ExporterConfiguration) (*ledger.Exporter, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "CreateExporter", ctx, configuration)
+ ret0, _ := ret[0].(*ledger.Exporter)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// CreateExporter indicates an expected call of CreateExporter.
+func (mr *MockReplicationBackendMockRecorder) CreateExporter(ctx, configuration any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateExporter", reflect.TypeOf((*MockReplicationBackend)(nil).CreateExporter), ctx, configuration)
+}
+
+// CreatePipeline mocks base method.
+func (m *MockReplicationBackend) CreatePipeline(ctx context.Context, pipelineConfiguration ledger.PipelineConfiguration) (*ledger.Pipeline, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "CreatePipeline", ctx, pipelineConfiguration)
+ ret0, _ := ret[0].(*ledger.Pipeline)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// CreatePipeline indicates an expected call of CreatePipeline.
+func (mr *MockReplicationBackendMockRecorder) CreatePipeline(ctx, pipelineConfiguration any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePipeline", reflect.TypeOf((*MockReplicationBackend)(nil).CreatePipeline), ctx, pipelineConfiguration)
+}
+
+// DeleteExporter mocks base method.
+func (m *MockReplicationBackend) DeleteExporter(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "DeleteExporter", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// DeleteExporter indicates an expected call of DeleteExporter.
+func (mr *MockReplicationBackendMockRecorder) DeleteExporter(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteExporter", reflect.TypeOf((*MockReplicationBackend)(nil).DeleteExporter), ctx, id)
+}
+
+// DeletePipeline mocks base method.
+func (m *MockReplicationBackend) DeletePipeline(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "DeletePipeline", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// DeletePipeline indicates an expected call of DeletePipeline.
+func (mr *MockReplicationBackendMockRecorder) DeletePipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePipeline", reflect.TypeOf((*MockReplicationBackend)(nil).DeletePipeline), ctx, id)
+}
+
+// GetExporter mocks base method.
+func (m *MockReplicationBackend) GetExporter(ctx context.Context, id string) (*ledger.Exporter, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetExporter", ctx, id)
+ ret0, _ := ret[0].(*ledger.Exporter)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetExporter indicates an expected call of GetExporter.
+func (mr *MockReplicationBackendMockRecorder) GetExporter(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetExporter", reflect.TypeOf((*MockReplicationBackend)(nil).GetExporter), ctx, id)
+}
+
+// GetPipeline mocks base method.
+func (m *MockReplicationBackend) GetPipeline(ctx context.Context, id string) (*ledger.Pipeline, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetPipeline", ctx, id)
+ ret0, _ := ret[0].(*ledger.Pipeline)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetPipeline indicates an expected call of GetPipeline.
+func (mr *MockReplicationBackendMockRecorder) GetPipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPipeline", reflect.TypeOf((*MockReplicationBackend)(nil).GetPipeline), ctx, id)
+}
+
+// ListExporters mocks base method.
+func (m *MockReplicationBackend) ListExporters(ctx context.Context) (*bunpaginate.Cursor[ledger.Exporter], error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ListExporters", ctx)
+ ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Exporter])
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ListExporters indicates an expected call of ListExporters.
+func (mr *MockReplicationBackendMockRecorder) ListExporters(ctx any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListExporters", reflect.TypeOf((*MockReplicationBackend)(nil).ListExporters), ctx)
+}
+
+// ListPipelines mocks base method.
+func (m *MockReplicationBackend) ListPipelines(ctx context.Context) (*bunpaginate.Cursor[ledger.Pipeline], error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ListPipelines", ctx)
+ ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Pipeline])
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ListPipelines indicates an expected call of ListPipelines.
+func (mr *MockReplicationBackendMockRecorder) ListPipelines(ctx any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPipelines", reflect.TypeOf((*MockReplicationBackend)(nil).ListPipelines), ctx)
+}
+
+// ResetPipeline mocks base method.
+func (m *MockReplicationBackend) ResetPipeline(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ResetPipeline", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// ResetPipeline indicates an expected call of ResetPipeline.
+func (mr *MockReplicationBackendMockRecorder) ResetPipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResetPipeline", reflect.TypeOf((*MockReplicationBackend)(nil).ResetPipeline), ctx, id)
+}
+
+// StartPipeline mocks base method.
+func (m *MockReplicationBackend) StartPipeline(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "StartPipeline", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// StartPipeline indicates an expected call of StartPipeline.
+func (mr *MockReplicationBackendMockRecorder) StartPipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartPipeline", reflect.TypeOf((*MockReplicationBackend)(nil).StartPipeline), ctx, id)
+}
+
+// StopPipeline mocks base method.
+func (m *MockReplicationBackend) StopPipeline(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "StopPipeline", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// StopPipeline indicates an expected call of StopPipeline.
+func (mr *MockReplicationBackendMockRecorder) StopPipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StopPipeline", reflect.TypeOf((*MockReplicationBackend)(nil).StopPipeline), ctx, id)
+}
+
// SystemController is a mock of Controller interface.
type SystemController struct {
ctrl *gomock.Controller
@@ -42,6 +227,21 @@ func (m *SystemController) EXPECT() *SystemControllerMockRecorder {
return m.recorder
}
+// CreateExporter mocks base method.
+func (m *SystemController) CreateExporter(ctx context.Context, configuration ledger.ExporterConfiguration) (*ledger.Exporter, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "CreateExporter", ctx, configuration)
+ ret0, _ := ret[0].(*ledger.Exporter)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// CreateExporter indicates an expected call of CreateExporter.
+func (mr *SystemControllerMockRecorder) CreateExporter(ctx, configuration any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateExporter", reflect.TypeOf((*SystemController)(nil).CreateExporter), ctx, configuration)
+}
+
// CreateLedger mocks base method.
func (m *SystemController) CreateLedger(ctx context.Context, name string, configuration ledger.Configuration) error {
m.ctrl.T.Helper()
@@ -56,6 +256,35 @@ func (mr *SystemControllerMockRecorder) CreateLedger(ctx, name, configuration an
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateLedger", reflect.TypeOf((*SystemController)(nil).CreateLedger), ctx, name, configuration)
}
+// CreatePipeline mocks base method.
+func (m *SystemController) CreatePipeline(ctx context.Context, pipelineConfiguration ledger.PipelineConfiguration) (*ledger.Pipeline, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "CreatePipeline", ctx, pipelineConfiguration)
+ ret0, _ := ret[0].(*ledger.Pipeline)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// CreatePipeline indicates an expected call of CreatePipeline.
+func (mr *SystemControllerMockRecorder) CreatePipeline(ctx, pipelineConfiguration any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePipeline", reflect.TypeOf((*SystemController)(nil).CreatePipeline), ctx, pipelineConfiguration)
+}
+
+// DeleteExporter mocks base method.
+func (m *SystemController) DeleteExporter(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "DeleteExporter", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// DeleteExporter indicates an expected call of DeleteExporter.
+func (mr *SystemControllerMockRecorder) DeleteExporter(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteExporter", reflect.TypeOf((*SystemController)(nil).DeleteExporter), ctx, id)
+}
+
// DeleteLedgerMetadata mocks base method.
func (m *SystemController) DeleteLedgerMetadata(ctx context.Context, param, key string) error {
m.ctrl.T.Helper()
@@ -70,6 +299,35 @@ func (mr *SystemControllerMockRecorder) DeleteLedgerMetadata(ctx, param, key any
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLedgerMetadata", reflect.TypeOf((*SystemController)(nil).DeleteLedgerMetadata), ctx, param, key)
}
+// DeletePipeline mocks base method.
+func (m *SystemController) DeletePipeline(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "DeletePipeline", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// DeletePipeline indicates an expected call of DeletePipeline.
+func (mr *SystemControllerMockRecorder) DeletePipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePipeline", reflect.TypeOf((*SystemController)(nil).DeletePipeline), ctx, id)
+}
+
+// GetExporter mocks base method.
+func (m *SystemController) GetExporter(ctx context.Context, id string) (*ledger.Exporter, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetExporter", ctx, id)
+ ret0, _ := ret[0].(*ledger.Exporter)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetExporter indicates an expected call of GetExporter.
+func (mr *SystemControllerMockRecorder) GetExporter(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetExporter", reflect.TypeOf((*SystemController)(nil).GetExporter), ctx, id)
+}
+
// GetLedger mocks base method.
func (m *SystemController) GetLedger(ctx context.Context, name string) (*ledger.Ledger, error) {
m.ctrl.T.Helper()
@@ -100,8 +358,38 @@ func (mr *SystemControllerMockRecorder) GetLedgerController(ctx, name any) *gomo
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLedgerController", reflect.TypeOf((*SystemController)(nil).GetLedgerController), ctx, name)
}
+// GetPipeline mocks base method.
+func (m *SystemController) GetPipeline(ctx context.Context, id string) (*ledger.Pipeline, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetPipeline", ctx, id)
+ ret0, _ := ret[0].(*ledger.Pipeline)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetPipeline indicates an expected call of GetPipeline.
+func (mr *SystemControllerMockRecorder) GetPipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPipeline", reflect.TypeOf((*SystemController)(nil).GetPipeline), ctx, id)
+}
+
+// ListExporters mocks base method.
+func (m *SystemController) ListExporters(ctx context.Context) (*bunpaginate.Cursor[ledger.Exporter], error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ListExporters", ctx)
+ ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Exporter])
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ListExporters indicates an expected call of ListExporters.
+func (mr *SystemControllerMockRecorder) ListExporters(ctx any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListExporters", reflect.TypeOf((*SystemController)(nil).ListExporters), ctx)
+}
+
// ListLedgers mocks base method.
-func (m *SystemController) ListLedgers(ctx context.Context, query common.ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Ledger], error) {
+func (m *SystemController) ListLedgers(ctx context.Context, query common.ColumnPaginatedQuery[system.ListLedgersQueryPayload]) (*bunpaginate.Cursor[ledger.Ledger], error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListLedgers", ctx, query)
ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Ledger])
@@ -115,6 +403,63 @@ func (mr *SystemControllerMockRecorder) ListLedgers(ctx, query any) *gomock.Call
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLedgers", reflect.TypeOf((*SystemController)(nil).ListLedgers), ctx, query)
}
+// ListPipelines mocks base method.
+func (m *SystemController) ListPipelines(ctx context.Context) (*bunpaginate.Cursor[ledger.Pipeline], error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ListPipelines", ctx)
+ ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Pipeline])
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ListPipelines indicates an expected call of ListPipelines.
+func (mr *SystemControllerMockRecorder) ListPipelines(ctx any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPipelines", reflect.TypeOf((*SystemController)(nil).ListPipelines), ctx)
+}
+
+// ResetPipeline mocks base method.
+func (m *SystemController) ResetPipeline(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ResetPipeline", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// ResetPipeline indicates an expected call of ResetPipeline.
+func (mr *SystemControllerMockRecorder) ResetPipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResetPipeline", reflect.TypeOf((*SystemController)(nil).ResetPipeline), ctx, id)
+}
+
+// StartPipeline mocks base method.
+func (m *SystemController) StartPipeline(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "StartPipeline", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// StartPipeline indicates an expected call of StartPipeline.
+func (mr *SystemControllerMockRecorder) StartPipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartPipeline", reflect.TypeOf((*SystemController)(nil).StartPipeline), ctx, id)
+}
+
+// StopPipeline mocks base method.
+func (m *SystemController) StopPipeline(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "StopPipeline", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// StopPipeline indicates an expected call of StopPipeline.
+func (mr *SystemControllerMockRecorder) StopPipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StopPipeline", reflect.TypeOf((*SystemController)(nil).StopPipeline), ctx, id)
+}
+
// UpdateLedgerMetadata mocks base method.
func (m_2 *SystemController) UpdateLedgerMetadata(ctx context.Context, name string, m map[string]string) error {
m_2.ctrl.T.Helper()
diff --git a/internal/api/common/utils.go b/internal/api/common/utils.go
new file mode 100644
index 0000000000..8c7cf2016b
--- /dev/null
+++ b/internal/api/common/utils.go
@@ -0,0 +1,17 @@
+package common
+
+import (
+ "encoding/json"
+ "github.com/formancehq/go-libs/v3/api"
+ "net/http"
+)
+
+func WithBody[V any](w http.ResponseWriter, r *http.Request, fn func(v V)) {
+ var v V
+ if err := json.NewDecoder(r.Body).Decode(&v); err != nil {
+ api.BadRequest(w, "VALIDATION", err)
+ return
+ }
+
+ fn(v)
+}
diff --git a/internal/api/module.go b/internal/api/module.go
index 739e4416bd..59bb218a66 100644
--- a/internal/api/module.go
+++ b/internal/api/module.go
@@ -22,6 +22,7 @@ type Config struct {
Debug bool
Bulk BulkConfig
Pagination common.PaginationConfig
+ Exporters bool
}
func Module(cfg Config) fx.Option {
@@ -43,6 +44,7 @@ func Module(cfg Config) fx.Option {
bulking.WithTracer(tracerProvider.Tracer("api.bulking")),
)),
WithPaginationConfiguration(cfg.Pagination),
+ WithExporters(cfg.Exporters),
)
}),
health.Module(),
diff --git a/internal/api/router.go b/internal/api/router.go
index 28845acb2a..130054c636 100644
--- a/internal/api/router.go
+++ b/internal/api/router.go
@@ -82,6 +82,7 @@ func NewRouter(
v2.WithBulkerFactory(routerOptions.bulkerFactory),
v2.WithDefaultBulkHandlerFactories(routerOptions.bulkMaxSize),
v2.WithPaginationConfig(routerOptions.paginationConfig),
+ v2.WithExporters(routerOptions.exporters),
)
mux.Handle("/v2*", http.StripPrefix("/v2", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
chi.RouteContext(r.Context()).Reset()
@@ -103,6 +104,7 @@ type routerOptions struct {
bulkMaxSize int
bulkerFactory bulking.BulkerFactory
paginationConfig common.PaginationConfig
+ exporters bool
}
type RouterOption func(ro *routerOptions)
@@ -131,6 +133,12 @@ func WithPaginationConfiguration(paginationConfig common.PaginationConfig) Route
}
}
+func WithExporters(v bool) RouterOption {
+ return func(ro *routerOptions) {
+ ro.exporters = v
+ }
+}
+
var defaultRouterOptions = []RouterOption{
WithTracer(nooptracer.Tracer{}),
WithBulkMaxSize(DefaultBulkMaxSize),
diff --git a/internal/api/v1/controllers_accounts_count.go b/internal/api/v1/controllers_accounts_count.go
index e87daa15e4..4fa2a49839 100644
--- a/internal/api/v1/controllers_accounts_count.go
+++ b/internal/api/v1/controllers_accounts_count.go
@@ -3,12 +3,12 @@ package v1
import (
"fmt"
storagecommon "github.com/formancehq/ledger/internal/storage/common"
+ "github.com/formancehq/ledger/internal/storage/ledger"
"net/http"
"errors"
"github.com/formancehq/go-libs/v3/api"
"github.com/formancehq/ledger/internal/api/common"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
)
func countAccounts(w http.ResponseWriter, r *http.Request) {
@@ -29,7 +29,7 @@ func countAccounts(w http.ResponseWriter, r *http.Request) {
count, err := l.CountAccounts(r.Context(), *rq)
if err != nil {
switch {
- case errors.Is(err, storagecommon.ErrInvalidQuery{}) || errors.Is(err, ledgercontroller.ErrMissingFeature{}):
+ case errors.Is(err, storagecommon.ErrInvalidQuery{}) || errors.Is(err, ledger.ErrMissingFeature{}):
api.BadRequest(w, common.ErrValidation, err)
default:
common.HandleCommonErrors(w, r, err)
diff --git a/internal/api/v1/controllers_accounts_count_test.go b/internal/api/v1/controllers_accounts_count_test.go
index 8e8299d6e0..e8c587f5ec 100644
--- a/internal/api/v1/controllers_accounts_count_test.go
+++ b/internal/api/v1/controllers_accounts_count_test.go
@@ -3,6 +3,7 @@ package v1
import (
"github.com/formancehq/ledger/internal/api/common"
storagecommon "github.com/formancehq/ledger/internal/storage/common"
+ "github.com/formancehq/ledger/internal/storage/ledger"
"net/http"
"net/http/httptest"
"net/url"
@@ -14,7 +15,6 @@ import (
"github.com/formancehq/go-libs/v3/auth"
"github.com/formancehq/go-libs/v3/query"
"github.com/formancehq/go-libs/v3/time"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
@@ -89,7 +89,7 @@ func TestAccountsCount(t *testing.T) {
expectStatusCode: http.StatusBadRequest,
expectedErrorCode: common.ErrValidation,
expectBackendCall: true,
- returnErr: ledgercontroller.ErrMissingFeature{},
+ returnErr: ledger.ErrMissingFeature{},
expectQuery: storagecommon.ResourceQuery[any]{},
},
{
diff --git a/internal/api/v1/controllers_accounts_list.go b/internal/api/v1/controllers_accounts_list.go
index 4d70eb1cdd..fce7e6f8bd 100644
--- a/internal/api/v1/controllers_accounts_list.go
+++ b/internal/api/v1/controllers_accounts_list.go
@@ -1,12 +1,12 @@
package v1
import (
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
"net/http"
"errors"
"github.com/formancehq/go-libs/v3/api"
"github.com/formancehq/ledger/internal/api/common"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
)
func listAccounts(w http.ResponseWriter, r *http.Request) {
@@ -27,7 +27,7 @@ func listAccounts(w http.ResponseWriter, r *http.Request) {
cursor, err := l.ListAccounts(r.Context(), *rq)
if err != nil {
switch {
- case errors.Is(err, ledgercontroller.ErrMissingFeature{}):
+ case errors.Is(err, ledgerstore.ErrMissingFeature{}):
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 c60242d11c..89af5b0eb7 100644
--- a/internal/api/v1/controllers_accounts_list_test.go
+++ b/internal/api/v1/controllers_accounts_list_test.go
@@ -3,6 +3,7 @@ package v1
import (
"github.com/formancehq/ledger/internal/api/common"
storagecommon "github.com/formancehq/ledger/internal/storage/common"
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
"net/http"
"net/http/httptest"
"net/url"
@@ -15,7 +16,6 @@ import (
"github.com/formancehq/go-libs/v3/metadata"
"github.com/formancehq/go-libs/v3/query"
ledger "github.com/formancehq/ledger/internal"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
@@ -119,7 +119,7 @@ func TestAccountsList(t *testing.T) {
name: "with missing feature",
expectStatusCode: http.StatusBadRequest,
expectedErrorCode: common.ErrValidation,
- returnErr: ledgercontroller.ErrMissingFeature{},
+ returnErr: ledgerstore.ErrMissingFeature{},
expectBackendCall: true,
expectQuery: storagecommon.OffsetPaginatedQuery[any]{
PageSize: DefaultPageSize,
diff --git a/internal/api/v1/controllers_balances_aggregates.go b/internal/api/v1/controllers_balances_aggregates.go
index f60e4b3630..0b910e9da4 100644
--- a/internal/api/v1/controllers_balances_aggregates.go
+++ b/internal/api/v1/controllers_balances_aggregates.go
@@ -1,12 +1,12 @@
package v1
import (
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
"net/http"
"github.com/formancehq/go-libs/v3/api"
"github.com/formancehq/go-libs/v3/query"
"github.com/formancehq/ledger/internal/api/common"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
)
func buildAggregatedBalancesQuery(r *http.Request) query.Builder {
@@ -18,7 +18,7 @@ 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 {
+ rq, err := getResourceQuery[ledgerstore.GetAggregatedVolumesOptions](r, func(q *ledgerstore.GetAggregatedVolumesOptions) error {
q.UseInsertionDate = true
return nil
diff --git a/internal/api/v1/controllers_balances_aggregates_test.go b/internal/api/v1/controllers_balances_aggregates_test.go
index f954f12ecb..8c9f41d2c6 100644
--- a/internal/api/v1/controllers_balances_aggregates_test.go
+++ b/internal/api/v1/controllers_balances_aggregates_test.go
@@ -2,6 +2,7 @@ package v1
import (
storagecommon "github.com/formancehq/ledger/internal/storage/common"
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
"math/big"
"net/http"
"net/http/httptest"
@@ -9,8 +10,6 @@ import (
"os"
"testing"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
-
"github.com/formancehq/go-libs/v3/api"
"github.com/formancehq/go-libs/v3/auth"
"github.com/formancehq/go-libs/v3/query"
@@ -25,14 +24,14 @@ func TestBalancesAggregates(t *testing.T) {
type testCase struct {
name string
queryParams url.Values
- expectQuery storagecommon.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]
+ expectQuery storagecommon.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]
}
testCases := []testCase{
{
name: "nominal",
- expectQuery: storagecommon.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{
- Opts: ledgercontroller.GetAggregatedVolumesOptions{
+ expectQuery: storagecommon.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{
+ Opts: ledgerstore.GetAggregatedVolumesOptions{
UseInsertionDate: true,
},
},
@@ -42,8 +41,8 @@ func TestBalancesAggregates(t *testing.T) {
queryParams: url.Values{
"address": []string{"foo"},
},
- expectQuery: storagecommon.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{
- Opts: ledgercontroller.GetAggregatedVolumesOptions{
+ expectQuery: storagecommon.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{
+ Opts: ledgerstore.GetAggregatedVolumesOptions{
UseInsertionDate: true,
},
Builder: query.Match("address", "foo"),
diff --git a/internal/api/v1/controllers_config.go b/internal/api/v1/controllers_config.go
index 89cf49c42d..46d5de8fc7 100644
--- a/internal/api/v1/controllers_config.go
+++ b/internal/api/v1/controllers_config.go
@@ -5,9 +5,9 @@ import (
_ "embed"
"github.com/formancehq/ledger/internal/api/common"
storagecommon "github.com/formancehq/ledger/internal/storage/common"
+ systemstore "github.com/formancehq/ledger/internal/storage/system"
"net/http"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
"github.com/formancehq/ledger/internal/controller/system"
"github.com/formancehq/go-libs/v3/bun/bunpaginate"
@@ -37,8 +37,8 @@ func GetInfo(systemController system.Controller, version string) func(w http.Res
return func(w http.ResponseWriter, r *http.Request) {
ledgerNames := make([]string, 0)
- if err := bunpaginate.Iterate(r.Context(), ledgercontroller.NewListLedgersQuery(100),
- func(ctx context.Context, q storagecommon.ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Ledger], error) {
+ if err := bunpaginate.Iterate(r.Context(), systemstore.NewListLedgersQuery(100),
+ func(ctx context.Context, q storagecommon.ColumnPaginatedQuery[systemstore.ListLedgersQueryPayload]) (*bunpaginate.Cursor[ledger.Ledger], error) {
return systemController.ListLedgers(ctx, q)
},
func(cursor *bunpaginate.Cursor[ledger.Ledger]) error {
diff --git a/internal/api/v1/controllers_transactions_create.go b/internal/api/v1/controllers_transactions_create.go
index f61e278744..ffa829d222 100644
--- a/internal/api/v1/controllers_transactions_create.go
+++ b/internal/api/v1/controllers_transactions_create.go
@@ -3,6 +3,7 @@ package v1
import (
"encoding/json"
"fmt"
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
"math/big"
"net/http"
@@ -98,7 +99,7 @@ func createTransaction(w http.ResponseWriter, r *http.Request) {
case errors.Is(err, ledgercontroller.ErrNoPostings) ||
errors.Is(err, ledgercontroller.ErrInvalidIdempotencyInput{}):
api.BadRequest(w, common.ErrValidation, err)
- case errors.Is(err, ledgercontroller.ErrTransactionReferenceConflict{}):
+ case errors.Is(err, ledgerstore.ErrTransactionReferenceConflict{}):
api.WriteErrorResponse(w, http.StatusConflict, common.ErrConflict, err)
default:
common.HandleCommonWriteErrors(w, r, err)
@@ -133,7 +134,7 @@ func createTransaction(w http.ResponseWriter, r *http.Request) {
errors.Is(err, ledgercontroller.ErrInvalidIdempotencyInput{}) ||
errors.Is(err, ledgercontroller.ErrNoPostings):
api.BadRequest(w, common.ErrValidation, err)
- case errors.Is(err, ledgercontroller.ErrTransactionReferenceConflict{}):
+ case errors.Is(err, ledgerstore.ErrTransactionReferenceConflict{}):
api.WriteErrorResponse(w, http.StatusConflict, common.ErrConflict, err)
default:
common.HandleCommonWriteErrors(w, r, err)
diff --git a/internal/api/v1/mocks_ledger_controller_test.go b/internal/api/v1/mocks_ledger_controller_test.go
index a93a8f8aa8..2948054f06 100644
--- a/internal/api/v1/mocks_ledger_controller_test.go
+++ b/internal/api/v1/mocks_ledger_controller_test.go
@@ -17,6 +17,7 @@ import (
ledger "github.com/formancehq/ledger/internal"
ledger0 "github.com/formancehq/ledger/internal/controller/ledger"
common "github.com/formancehq/ledger/internal/storage/common"
+ ledger1 "github.com/formancehq/ledger/internal/storage/ledger"
bun "github.com/uptrace/bun"
gomock "go.uber.org/mock/gomock"
)
@@ -181,7 +182,7 @@ func (mr *LedgerControllerMockRecorder) GetAccount(ctx, query any) *gomock.Call
}
// GetAggregatedBalances mocks base method.
-func (m *LedgerController) GetAggregatedBalances(ctx context.Context, q common.ResourceQuery[ledger0.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) {
+func (m *LedgerController) GetAggregatedBalances(ctx context.Context, q common.ResourceQuery[ledger1.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAggregatedBalances", ctx, q)
ret0, _ := ret[0].(ledger.BalancesByAssets)
@@ -241,7 +242,7 @@ func (mr *LedgerControllerMockRecorder) GetTransaction(ctx, query any) *gomock.C
}
// GetVolumesWithBalances mocks base method.
-func (m *LedgerController) GetVolumesWithBalances(ctx context.Context, q common.OffsetPaginatedQuery[ledger0.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) {
+func (m *LedgerController) GetVolumesWithBalances(ctx context.Context, q common.OffsetPaginatedQuery[ledger1.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])
@@ -269,6 +270,20 @@ func (mr *LedgerControllerMockRecorder) Import(ctx, stream any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Import", reflect.TypeOf((*LedgerController)(nil).Import), ctx, stream)
}
+// Info mocks base method.
+func (m *LedgerController) Info() ledger.Ledger {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Info")
+ ret0, _ := ret[0].(ledger.Ledger)
+ return ret0
+}
+
+// Info indicates an expected call of Info.
+func (mr *LedgerControllerMockRecorder) Info() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*LedgerController)(nil).Info))
+}
+
// IsDatabaseUpToDate mocks base method.
func (m *LedgerController) IsDatabaseUpToDate(ctx context.Context) (bool, error) {
m.ctrl.T.Helper()
diff --git a/internal/api/v1/mocks_system_controller_test.go b/internal/api/v1/mocks_system_controller_test.go
index cc772b5eec..10eeaf56c9 100644
--- a/internal/api/v1/mocks_system_controller_test.go
+++ b/internal/api/v1/mocks_system_controller_test.go
@@ -15,9 +15,194 @@ import (
ledger "github.com/formancehq/ledger/internal"
ledger0 "github.com/formancehq/ledger/internal/controller/ledger"
common "github.com/formancehq/ledger/internal/storage/common"
+ system "github.com/formancehq/ledger/internal/storage/system"
gomock "go.uber.org/mock/gomock"
)
+// MockReplicationBackend is a mock of ReplicationBackend interface.
+type MockReplicationBackend struct {
+ ctrl *gomock.Controller
+ recorder *MockReplicationBackendMockRecorder
+ isgomock struct{}
+}
+
+// MockReplicationBackendMockRecorder is the mock recorder for MockReplicationBackend.
+type MockReplicationBackendMockRecorder struct {
+ mock *MockReplicationBackend
+}
+
+// NewMockReplicationBackend creates a new mock instance.
+func NewMockReplicationBackend(ctrl *gomock.Controller) *MockReplicationBackend {
+ mock := &MockReplicationBackend{ctrl: ctrl}
+ mock.recorder = &MockReplicationBackendMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockReplicationBackend) EXPECT() *MockReplicationBackendMockRecorder {
+ return m.recorder
+}
+
+// CreateExporter mocks base method.
+func (m *MockReplicationBackend) CreateExporter(ctx context.Context, configuration ledger.ExporterConfiguration) (*ledger.Exporter, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "CreateExporter", ctx, configuration)
+ ret0, _ := ret[0].(*ledger.Exporter)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// CreateExporter indicates an expected call of CreateExporter.
+func (mr *MockReplicationBackendMockRecorder) CreateExporter(ctx, configuration any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateExporter", reflect.TypeOf((*MockReplicationBackend)(nil).CreateExporter), ctx, configuration)
+}
+
+// CreatePipeline mocks base method.
+func (m *MockReplicationBackend) CreatePipeline(ctx context.Context, pipelineConfiguration ledger.PipelineConfiguration) (*ledger.Pipeline, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "CreatePipeline", ctx, pipelineConfiguration)
+ ret0, _ := ret[0].(*ledger.Pipeline)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// CreatePipeline indicates an expected call of CreatePipeline.
+func (mr *MockReplicationBackendMockRecorder) CreatePipeline(ctx, pipelineConfiguration any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePipeline", reflect.TypeOf((*MockReplicationBackend)(nil).CreatePipeline), ctx, pipelineConfiguration)
+}
+
+// DeleteExporter mocks base method.
+func (m *MockReplicationBackend) DeleteExporter(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "DeleteExporter", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// DeleteExporter indicates an expected call of DeleteExporter.
+func (mr *MockReplicationBackendMockRecorder) DeleteExporter(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteExporter", reflect.TypeOf((*MockReplicationBackend)(nil).DeleteExporter), ctx, id)
+}
+
+// DeletePipeline mocks base method.
+func (m *MockReplicationBackend) DeletePipeline(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "DeletePipeline", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// DeletePipeline indicates an expected call of DeletePipeline.
+func (mr *MockReplicationBackendMockRecorder) DeletePipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePipeline", reflect.TypeOf((*MockReplicationBackend)(nil).DeletePipeline), ctx, id)
+}
+
+// GetExporter mocks base method.
+func (m *MockReplicationBackend) GetExporter(ctx context.Context, id string) (*ledger.Exporter, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetExporter", ctx, id)
+ ret0, _ := ret[0].(*ledger.Exporter)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetExporter indicates an expected call of GetExporter.
+func (mr *MockReplicationBackendMockRecorder) GetExporter(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetExporter", reflect.TypeOf((*MockReplicationBackend)(nil).GetExporter), ctx, id)
+}
+
+// GetPipeline mocks base method.
+func (m *MockReplicationBackend) GetPipeline(ctx context.Context, id string) (*ledger.Pipeline, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetPipeline", ctx, id)
+ ret0, _ := ret[0].(*ledger.Pipeline)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetPipeline indicates an expected call of GetPipeline.
+func (mr *MockReplicationBackendMockRecorder) GetPipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPipeline", reflect.TypeOf((*MockReplicationBackend)(nil).GetPipeline), ctx, id)
+}
+
+// ListExporters mocks base method.
+func (m *MockReplicationBackend) ListExporters(ctx context.Context) (*bunpaginate.Cursor[ledger.Exporter], error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ListExporters", ctx)
+ ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Exporter])
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ListExporters indicates an expected call of ListExporters.
+func (mr *MockReplicationBackendMockRecorder) ListExporters(ctx any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListExporters", reflect.TypeOf((*MockReplicationBackend)(nil).ListExporters), ctx)
+}
+
+// ListPipelines mocks base method.
+func (m *MockReplicationBackend) ListPipelines(ctx context.Context) (*bunpaginate.Cursor[ledger.Pipeline], error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ListPipelines", ctx)
+ ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Pipeline])
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ListPipelines indicates an expected call of ListPipelines.
+func (mr *MockReplicationBackendMockRecorder) ListPipelines(ctx any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPipelines", reflect.TypeOf((*MockReplicationBackend)(nil).ListPipelines), ctx)
+}
+
+// ResetPipeline mocks base method.
+func (m *MockReplicationBackend) ResetPipeline(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ResetPipeline", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// ResetPipeline indicates an expected call of ResetPipeline.
+func (mr *MockReplicationBackendMockRecorder) ResetPipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResetPipeline", reflect.TypeOf((*MockReplicationBackend)(nil).ResetPipeline), ctx, id)
+}
+
+// StartPipeline mocks base method.
+func (m *MockReplicationBackend) StartPipeline(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "StartPipeline", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// StartPipeline indicates an expected call of StartPipeline.
+func (mr *MockReplicationBackendMockRecorder) StartPipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartPipeline", reflect.TypeOf((*MockReplicationBackend)(nil).StartPipeline), ctx, id)
+}
+
+// StopPipeline mocks base method.
+func (m *MockReplicationBackend) StopPipeline(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "StopPipeline", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// StopPipeline indicates an expected call of StopPipeline.
+func (mr *MockReplicationBackendMockRecorder) StopPipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StopPipeline", reflect.TypeOf((*MockReplicationBackend)(nil).StopPipeline), ctx, id)
+}
+
// SystemController is a mock of Controller interface.
type SystemController struct {
ctrl *gomock.Controller
@@ -42,6 +227,21 @@ func (m *SystemController) EXPECT() *SystemControllerMockRecorder {
return m.recorder
}
+// CreateExporter mocks base method.
+func (m *SystemController) CreateExporter(ctx context.Context, configuration ledger.ExporterConfiguration) (*ledger.Exporter, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "CreateExporter", ctx, configuration)
+ ret0, _ := ret[0].(*ledger.Exporter)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// CreateExporter indicates an expected call of CreateExporter.
+func (mr *SystemControllerMockRecorder) CreateExporter(ctx, configuration any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateExporter", reflect.TypeOf((*SystemController)(nil).CreateExporter), ctx, configuration)
+}
+
// CreateLedger mocks base method.
func (m *SystemController) CreateLedger(ctx context.Context, name string, configuration ledger.Configuration) error {
m.ctrl.T.Helper()
@@ -56,6 +256,35 @@ func (mr *SystemControllerMockRecorder) CreateLedger(ctx, name, configuration an
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateLedger", reflect.TypeOf((*SystemController)(nil).CreateLedger), ctx, name, configuration)
}
+// CreatePipeline mocks base method.
+func (m *SystemController) CreatePipeline(ctx context.Context, pipelineConfiguration ledger.PipelineConfiguration) (*ledger.Pipeline, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "CreatePipeline", ctx, pipelineConfiguration)
+ ret0, _ := ret[0].(*ledger.Pipeline)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// CreatePipeline indicates an expected call of CreatePipeline.
+func (mr *SystemControllerMockRecorder) CreatePipeline(ctx, pipelineConfiguration any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePipeline", reflect.TypeOf((*SystemController)(nil).CreatePipeline), ctx, pipelineConfiguration)
+}
+
+// DeleteExporter mocks base method.
+func (m *SystemController) DeleteExporter(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "DeleteExporter", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// DeleteExporter indicates an expected call of DeleteExporter.
+func (mr *SystemControllerMockRecorder) DeleteExporter(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteExporter", reflect.TypeOf((*SystemController)(nil).DeleteExporter), ctx, id)
+}
+
// DeleteLedgerMetadata mocks base method.
func (m *SystemController) DeleteLedgerMetadata(ctx context.Context, param, key string) error {
m.ctrl.T.Helper()
@@ -70,6 +299,35 @@ func (mr *SystemControllerMockRecorder) DeleteLedgerMetadata(ctx, param, key any
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLedgerMetadata", reflect.TypeOf((*SystemController)(nil).DeleteLedgerMetadata), ctx, param, key)
}
+// DeletePipeline mocks base method.
+func (m *SystemController) DeletePipeline(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "DeletePipeline", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// DeletePipeline indicates an expected call of DeletePipeline.
+func (mr *SystemControllerMockRecorder) DeletePipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePipeline", reflect.TypeOf((*SystemController)(nil).DeletePipeline), ctx, id)
+}
+
+// GetExporter mocks base method.
+func (m *SystemController) GetExporter(ctx context.Context, id string) (*ledger.Exporter, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetExporter", ctx, id)
+ ret0, _ := ret[0].(*ledger.Exporter)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetExporter indicates an expected call of GetExporter.
+func (mr *SystemControllerMockRecorder) GetExporter(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetExporter", reflect.TypeOf((*SystemController)(nil).GetExporter), ctx, id)
+}
+
// GetLedger mocks base method.
func (m *SystemController) GetLedger(ctx context.Context, name string) (*ledger.Ledger, error) {
m.ctrl.T.Helper()
@@ -100,8 +358,38 @@ func (mr *SystemControllerMockRecorder) GetLedgerController(ctx, name any) *gomo
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLedgerController", reflect.TypeOf((*SystemController)(nil).GetLedgerController), ctx, name)
}
+// GetPipeline mocks base method.
+func (m *SystemController) GetPipeline(ctx context.Context, id string) (*ledger.Pipeline, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetPipeline", ctx, id)
+ ret0, _ := ret[0].(*ledger.Pipeline)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetPipeline indicates an expected call of GetPipeline.
+func (mr *SystemControllerMockRecorder) GetPipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPipeline", reflect.TypeOf((*SystemController)(nil).GetPipeline), ctx, id)
+}
+
+// ListExporters mocks base method.
+func (m *SystemController) ListExporters(ctx context.Context) (*bunpaginate.Cursor[ledger.Exporter], error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ListExporters", ctx)
+ ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Exporter])
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ListExporters indicates an expected call of ListExporters.
+func (mr *SystemControllerMockRecorder) ListExporters(ctx any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListExporters", reflect.TypeOf((*SystemController)(nil).ListExporters), ctx)
+}
+
// ListLedgers mocks base method.
-func (m *SystemController) ListLedgers(ctx context.Context, query common.ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Ledger], error) {
+func (m *SystemController) ListLedgers(ctx context.Context, query common.ColumnPaginatedQuery[system.ListLedgersQueryPayload]) (*bunpaginate.Cursor[ledger.Ledger], error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListLedgers", ctx, query)
ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Ledger])
@@ -115,6 +403,63 @@ func (mr *SystemControllerMockRecorder) ListLedgers(ctx, query any) *gomock.Call
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLedgers", reflect.TypeOf((*SystemController)(nil).ListLedgers), ctx, query)
}
+// ListPipelines mocks base method.
+func (m *SystemController) ListPipelines(ctx context.Context) (*bunpaginate.Cursor[ledger.Pipeline], error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ListPipelines", ctx)
+ ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Pipeline])
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ListPipelines indicates an expected call of ListPipelines.
+func (mr *SystemControllerMockRecorder) ListPipelines(ctx any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPipelines", reflect.TypeOf((*SystemController)(nil).ListPipelines), ctx)
+}
+
+// ResetPipeline mocks base method.
+func (m *SystemController) ResetPipeline(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ResetPipeline", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// ResetPipeline indicates an expected call of ResetPipeline.
+func (mr *SystemControllerMockRecorder) ResetPipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResetPipeline", reflect.TypeOf((*SystemController)(nil).ResetPipeline), ctx, id)
+}
+
+// StartPipeline mocks base method.
+func (m *SystemController) StartPipeline(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "StartPipeline", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// StartPipeline indicates an expected call of StartPipeline.
+func (mr *SystemControllerMockRecorder) StartPipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartPipeline", reflect.TypeOf((*SystemController)(nil).StartPipeline), ctx, id)
+}
+
+// StopPipeline mocks base method.
+func (m *SystemController) StopPipeline(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "StopPipeline", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// StopPipeline indicates an expected call of StopPipeline.
+func (mr *SystemControllerMockRecorder) StopPipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StopPipeline", reflect.TypeOf((*SystemController)(nil).StopPipeline), ctx, id)
+}
+
// UpdateLedgerMetadata mocks base method.
func (m_2 *SystemController) UpdateLedgerMetadata(ctx context.Context, name string, m map[string]string) error {
m_2.ctrl.T.Helper()
diff --git a/internal/api/v2/common.go b/internal/api/v2/common.go
index 2e3a22f366..9d35d22562 100644
--- a/internal/api/v2/common.go
+++ b/internal/api/v2/common.go
@@ -4,6 +4,7 @@ import (
. "github.com/formancehq/go-libs/v3/collectionutils"
"github.com/formancehq/ledger/internal/api/common"
storagecommon "github.com/formancehq/ledger/internal/storage/common"
+ "github.com/go-chi/chi/v5"
"io"
"net/http"
"strings"
@@ -38,6 +39,14 @@ func getOOT(r *http.Request) (*time.Time, error) {
return getDate(r, "oot")
}
+func getPipelineID(r *http.Request) string {
+ return chi.URLParam(r, "pipelineID")
+}
+
+func getExporterID(r *http.Request) string {
+ return chi.URLParam(r, "exporterID")
+}
+
func getQueryBuilder(r *http.Request) (query.Builder, error) {
q := r.URL.Query().Get("query")
if q == "" {
diff --git a/internal/api/v2/controllers_accounts_add_metadata.go b/internal/api/v2/controllers_accounts_add_metadata.go
index d337d46b4f..2495c70425 100644
--- a/internal/api/v2/controllers_accounts_add_metadata.go
+++ b/internal/api/v2/controllers_accounts_add_metadata.go
@@ -1,14 +1,11 @@
package v2
import (
- "encoding/json"
"net/http"
"net/url"
"github.com/formancehq/ledger/internal/controller/ledger"
- "errors"
-
"github.com/formancehq/go-libs/v3/api"
"github.com/formancehq/go-libs/v3/metadata"
"github.com/formancehq/ledger/internal/api/common"
@@ -24,20 +21,16 @@ func addAccountMetadata(w http.ResponseWriter, r *http.Request) {
return
}
- var m metadata.Metadata
- if err := json.NewDecoder(r.Body).Decode(&m); err != nil {
- api.BadRequest(w, common.ErrValidation, errors.New("invalid metadata format"))
- return
- }
-
- _, err = l.SaveAccountMetadata(r.Context(), getCommandParameters(r, ledger.SaveAccountMetadata{
- Address: address,
- Metadata: m,
- }))
- if err != nil {
- common.HandleCommonWriteErrors(w, r, err)
- return
- }
-
- api.NoContent(w)
+ common.WithBody(w, r, func(m metadata.Metadata) {
+ _, err = l.SaveAccountMetadata(r.Context(), getCommandParameters(r, ledger.SaveAccountMetadata{
+ Address: address,
+ Metadata: m,
+ }))
+ if err != nil {
+ common.HandleCommonWriteErrors(w, r, err)
+ return
+ }
+
+ api.NoContent(w)
+ })
}
diff --git a/internal/api/v2/controllers_accounts_count.go b/internal/api/v2/controllers_accounts_count.go
index 90dee23237..6ea9e98a6c 100644
--- a/internal/api/v2/controllers_accounts_count.go
+++ b/internal/api/v2/controllers_accounts_count.go
@@ -3,12 +3,12 @@ package v2
import (
"fmt"
storagecommon "github.com/formancehq/ledger/internal/storage/common"
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
"net/http"
"errors"
"github.com/formancehq/go-libs/v3/api"
"github.com/formancehq/ledger/internal/api/common"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
)
func countAccounts(w http.ResponseWriter, r *http.Request) {
@@ -23,7 +23,7 @@ func countAccounts(w http.ResponseWriter, r *http.Request) {
count, err := l.CountAccounts(r.Context(), *rq)
if err != nil {
switch {
- case errors.Is(err, storagecommon.ErrInvalidQuery{}) || errors.Is(err, ledgercontroller.ErrMissingFeature{}):
+ case errors.Is(err, storagecommon.ErrInvalidQuery{}) || errors.Is(err, ledgerstore.ErrMissingFeature{}):
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 6fb120aaee..3e18504d1a 100644
--- a/internal/api/v2/controllers_accounts_count_test.go
+++ b/internal/api/v2/controllers_accounts_count_test.go
@@ -4,6 +4,7 @@ import (
"bytes"
"github.com/formancehq/ledger/internal/api/common"
storagecommon "github.com/formancehq/ledger/internal/storage/common"
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
"net/http"
"net/http/httptest"
"net/url"
@@ -14,7 +15,6 @@ import (
"github.com/formancehq/go-libs/v3/auth"
"github.com/formancehq/go-libs/v3/query"
"github.com/formancehq/go-libs/v3/time"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
@@ -105,7 +105,7 @@ func TestAccountsCount(t *testing.T) {
expectStatusCode: http.StatusBadRequest,
expectedErrorCode: common.ErrValidation,
expectBackendCall: true,
- returnErr: ledgercontroller.ErrMissingFeature{},
+ returnErr: ledgerstore.ErrMissingFeature{},
expectQuery: storagecommon.ResourceQuery[any]{
PIT: &before,
Expand: make([]string, 0),
diff --git a/internal/api/v2/controllers_accounts_list.go b/internal/api/v2/controllers_accounts_list.go
index 77de5006e1..c78bbfd3ae 100644
--- a/internal/api/v2/controllers_accounts_list.go
+++ b/internal/api/v2/controllers_accounts_list.go
@@ -6,8 +6,8 @@ import (
"github.com/formancehq/go-libs/v3/bun/bunpaginate"
ledger "github.com/formancehq/ledger/internal"
"github.com/formancehq/ledger/internal/api/common"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
storagecommon "github.com/formancehq/ledger/internal/storage/common"
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
"net/http"
)
@@ -24,7 +24,7 @@ func listAccounts(paginationConfig common.PaginationConfig) http.HandlerFunc {
cursor, err := l.ListAccounts(r.Context(), *query)
if err != nil {
switch {
- case errors.Is(err, storagecommon.ErrInvalidQuery{}) || errors.Is(err, ledgercontroller.ErrMissingFeature{}):
+ case errors.Is(err, storagecommon.ErrInvalidQuery{}) || errors.Is(err, ledgerstore.ErrMissingFeature{}):
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 c257b911b0..c326809e95 100644
--- a/internal/api/v2/controllers_accounts_list_test.go
+++ b/internal/api/v2/controllers_accounts_list_test.go
@@ -4,6 +4,7 @@ import (
"bytes"
"github.com/formancehq/ledger/internal/api/common"
storagecommon "github.com/formancehq/ledger/internal/storage/common"
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
"net/http"
"net/http/httptest"
"net/url"
@@ -17,7 +18,6 @@ import (
"github.com/formancehq/go-libs/v3/query"
"github.com/formancehq/go-libs/v3/time"
ledger "github.com/formancehq/ledger/internal"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
@@ -170,7 +170,7 @@ func TestAccountsList(t *testing.T) {
expectStatusCode: http.StatusBadRequest,
expectedErrorCode: common.ErrValidation,
expectBackendCall: true,
- returnErr: ledgercontroller.ErrMissingFeature{},
+ returnErr: ledgerstore.ErrMissingFeature{},
expectQuery: storagecommon.OffsetPaginatedQuery[any]{
PageSize: bunpaginate.QueryDefaultPageSize,
Options: storagecommon.ResourceQuery[any]{
diff --git a/internal/api/v2/controllers_balances.go b/internal/api/v2/controllers_balances.go
index 2003059102..8c2320c80e 100644
--- a/internal/api/v2/controllers_balances.go
+++ b/internal/api/v2/controllers_balances.go
@@ -2,18 +2,17 @@ package v2
import (
storagecommon "github.com/formancehq/ledger/internal/storage/common"
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
"net/http"
"errors"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
-
"github.com/formancehq/go-libs/v3/api"
"github.com/formancehq/ledger/internal/api/common"
)
func readBalancesAggregated(w http.ResponseWriter, r *http.Request) {
- rq, err := getResourceQuery[ledgercontroller.GetAggregatedVolumesOptions](r, func(options *ledgercontroller.GetAggregatedVolumesOptions) error {
+ rq, err := getResourceQuery[ledgerstore.GetAggregatedVolumesOptions](r, func(options *ledgerstore.GetAggregatedVolumesOptions) error {
options.UseInsertionDate = api.QueryParamBool(r, "use_insertion_date") || api.QueryParamBool(r, "useInsertionDate")
return nil
@@ -26,7 +25,7 @@ func readBalancesAggregated(w http.ResponseWriter, r *http.Request) {
balances, err := common.LedgerFromContext(r.Context()).GetAggregatedBalances(r.Context(), *rq)
if err != nil {
switch {
- case errors.Is(err, storagecommon.ErrInvalidQuery{}) || errors.Is(err, ledgercontroller.ErrMissingFeature{}):
+ case errors.Is(err, storagecommon.ErrInvalidQuery{}) || errors.Is(err, ledgerstore.ErrMissingFeature{}):
api.BadRequest(w, common.ErrValidation, err)
default:
common.HandleCommonErrors(w, r, err)
diff --git a/internal/api/v2/controllers_balances_test.go b/internal/api/v2/controllers_balances_test.go
index 08d69ad72b..5da652374d 100644
--- a/internal/api/v2/controllers_balances_test.go
+++ b/internal/api/v2/controllers_balances_test.go
@@ -3,14 +3,13 @@ package v2
import (
"bytes"
storagecommon "github.com/formancehq/ledger/internal/storage/common"
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
"math/big"
"net/http"
"net/http/httptest"
"net/url"
"testing"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
-
"github.com/formancehq/go-libs/v3/time"
"github.com/formancehq/go-libs/v3/api"
@@ -28,7 +27,7 @@ func TestBalancesAggregates(t *testing.T) {
name string
queryParams url.Values
body string
- expectQuery storagecommon.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]
+ expectQuery storagecommon.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]
}
now := time.Now()
@@ -36,8 +35,8 @@ func TestBalancesAggregates(t *testing.T) {
testCases := []testCase{
{
name: "nominal",
- expectQuery: storagecommon.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{
- Opts: ledgercontroller.GetAggregatedVolumesOptions{},
+ expectQuery: storagecommon.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{
+ Opts: ledgerstore.GetAggregatedVolumesOptions{},
PIT: &now,
Expand: make([]string, 0),
},
@@ -45,8 +44,8 @@ func TestBalancesAggregates(t *testing.T) {
{
name: "using address",
body: `{"$match": {"address": "foo"}}`,
- expectQuery: storagecommon.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{
- Opts: ledgercontroller.GetAggregatedVolumesOptions{},
+ expectQuery: storagecommon.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{
+ Opts: ledgerstore.GetAggregatedVolumesOptions{},
PIT: &now,
Builder: query.Match("address", "foo"),
Expand: make([]string, 0),
@@ -55,8 +54,8 @@ func TestBalancesAggregates(t *testing.T) {
{
name: "using exists metadata filter",
body: `{"$exists": {"metadata": "foo"}}`,
- expectQuery: storagecommon.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{
- Opts: ledgercontroller.GetAggregatedVolumesOptions{},
+ expectQuery: storagecommon.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{
+ Opts: ledgerstore.GetAggregatedVolumesOptions{},
PIT: &now,
Builder: query.Exists("metadata", "foo"),
Expand: make([]string, 0),
@@ -67,8 +66,8 @@ func TestBalancesAggregates(t *testing.T) {
queryParams: url.Values{
"pit": []string{now.Format(time.RFC3339Nano)},
},
- expectQuery: storagecommon.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{
- Opts: ledgercontroller.GetAggregatedVolumesOptions{},
+ expectQuery: storagecommon.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{
+ Opts: ledgerstore.GetAggregatedVolumesOptions{},
PIT: &now,
Expand: make([]string, 0),
},
@@ -79,8 +78,8 @@ func TestBalancesAggregates(t *testing.T) {
"pit": []string{now.Format(time.RFC3339Nano)},
"useInsertionDate": []string{"true"},
},
- expectQuery: storagecommon.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{
- Opts: ledgercontroller.GetAggregatedVolumesOptions{
+ expectQuery: storagecommon.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{
+ Opts: ledgerstore.GetAggregatedVolumesOptions{
UseInsertionDate: true,
},
PIT: &now,
diff --git a/internal/api/v2/controllers_exporters_create.go b/internal/api/v2/controllers_exporters_create.go
new file mode 100644
index 0000000000..5cde59e20b
--- /dev/null
+++ b/internal/api/v2/controllers_exporters_create.go
@@ -0,0 +1,29 @@
+package v2
+
+import (
+ "errors"
+ "github.com/formancehq/go-libs/v3/api"
+ ledger "github.com/formancehq/ledger/internal"
+ "github.com/formancehq/ledger/internal/api/common"
+ systemcontroller "github.com/formancehq/ledger/internal/controller/system"
+ "net/http"
+)
+
+func createExporter(systemController systemcontroller.Controller) func(w http.ResponseWriter, r *http.Request) {
+ return func(w http.ResponseWriter, r *http.Request) {
+ common.WithBody[ledger.ExporterConfiguration](w, r, func(req ledger.ExporterConfiguration) {
+ exporter, err := systemController.CreateExporter(r.Context(), req)
+ if err != nil {
+ switch {
+ case errors.Is(err, systemcontroller.ErrInvalidDriverConfiguration{}):
+ api.BadRequest(w, "VALIDATION", err)
+ default:
+ api.InternalServerError(w, r, err)
+ }
+ return
+ }
+
+ api.Created(w, exporter)
+ })
+ }
+}
diff --git a/internal/api/v2/controllers_exporters_create_test.go b/internal/api/v2/controllers_exporters_create_test.go
new file mode 100644
index 0000000000..d9bd1877d6
--- /dev/null
+++ b/internal/api/v2/controllers_exporters_create_test.go
@@ -0,0 +1,88 @@
+package v2
+
+import (
+ "bytes"
+ "encoding/json"
+ sharedapi "github.com/formancehq/go-libs/v3/api"
+ "github.com/formancehq/go-libs/v3/auth"
+ ledger "github.com/formancehq/ledger/internal"
+ "go.uber.org/mock/gomock"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/pkg/errors"
+
+ "github.com/formancehq/go-libs/v3/logging"
+ "github.com/stretchr/testify/require"
+)
+
+func TestCreateExporter(t *testing.T) {
+ t.Parallel()
+
+ ctx := logging.TestingContext()
+
+ type testCase struct {
+ name string
+ returnError error
+ expectErrorStatusCode int
+ expectErrorCode string
+ exporterConfiguration ledger.ExporterConfiguration
+ }
+ for _, testCase := range []testCase{
+ {
+ name: "nominal",
+ exporterConfiguration: ledger.ExporterConfiguration{
+ Driver: "exporter1",
+ Config: json.RawMessage("{}"),
+ },
+ },
+ {
+ name: "invalid exporter configuration",
+ exporterConfiguration: ledger.ExporterConfiguration{
+ Driver: "exporter1",
+ Config: json.RawMessage(`{"batching":{"flushInterval":"-1"}}`),
+ },
+ },
+ {
+ name: "unknown error",
+ exporterConfiguration: ledger.ExporterConfiguration{
+ Driver: "exporter1",
+ Config: json.RawMessage("{}"),
+ },
+ expectErrorCode: "INTERNAL",
+ expectErrorStatusCode: http.StatusInternalServerError,
+ returnError: errors.New("any error"),
+ },
+ } {
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ systemController, _ := newTestingSystemController(t, false)
+ systemController.EXPECT().
+ CreateExporter(gomock.Any(), testCase.exporterConfiguration).
+ Return(nil, testCase.returnError)
+
+ router := NewRouter(systemController, auth.NewNoAuth(), "develop", WithExporters(true))
+
+ data, err := json.Marshal(testCase.exporterConfiguration)
+ require.NoError(t, err)
+
+ req := httptest.NewRequest(http.MethodPost, "/_/exporters", bytes.NewBuffer(data))
+ req = req.WithContext(ctx)
+ rsp := httptest.NewRecorder()
+
+ router.ServeHTTP(rsp, req)
+
+ require.Equal(t, "application/json", rsp.Header().Get("Content-Type"))
+ if testCase.expectErrorCode != "" {
+ require.Equal(t, testCase.expectErrorStatusCode, rsp.Code)
+ errorResponse := sharedapi.ErrorResponse{}
+ require.NoError(t, json.NewDecoder(rsp.Body).Decode(&errorResponse))
+ require.Equal(t, testCase.expectErrorCode, errorResponse.ErrorCode)
+ } else {
+ require.Equal(t, http.StatusCreated, rsp.Code)
+ }
+ })
+ }
+}
diff --git a/internal/api/v2/controllers_exporters_delete.go b/internal/api/v2/controllers_exporters_delete.go
new file mode 100644
index 0000000000..9fb219d33e
--- /dev/null
+++ b/internal/api/v2/controllers_exporters_delete.go
@@ -0,0 +1,25 @@
+package v2
+
+import (
+ "errors"
+ "github.com/formancehq/go-libs/v3/api"
+ systemcontroller "github.com/formancehq/ledger/internal/controller/system"
+ "net/http"
+)
+
+func deleteExporter(systemController systemcontroller.Controller) func(w http.ResponseWriter, r *http.Request) {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if err := systemController.DeleteExporter(r.Context(), getExporterID(r)); err != nil {
+ switch {
+ case errors.Is(err, systemcontroller.ErrExporterNotFound("")):
+ api.NotFound(w, err)
+ case errors.Is(err, systemcontroller.ErrExporterUsed("")):
+ api.BadRequest(w, "VALIDATION", err)
+ default:
+ api.InternalServerError(w, r, err)
+ }
+ return
+ }
+ w.WriteHeader(http.StatusNoContent)
+ }
+}
diff --git a/internal/api/v2/controllers_exporters_delete_test.go b/internal/api/v2/controllers_exporters_delete_test.go
new file mode 100644
index 0000000000..69c9b6a419
--- /dev/null
+++ b/internal/api/v2/controllers_exporters_delete_test.go
@@ -0,0 +1,82 @@
+package v2
+
+import (
+ "encoding/json"
+ "github.com/formancehq/go-libs/v3/auth"
+ "github.com/formancehq/go-libs/v3/logging"
+ systemcontroller "github.com/formancehq/ledger/internal/controller/system"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/google/uuid"
+ "github.com/pkg/errors"
+
+ sharedapi "github.com/formancehq/go-libs/v3/api"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/mock/gomock"
+)
+
+func TestDeleteExporter(t *testing.T) {
+ t.Parallel()
+
+ ctx := logging.TestingContext()
+
+ type testCase struct {
+ name string
+ returnError error
+ expectErrorStatusCode int
+ expectErrorCode string
+ }
+ for _, testCase := range []testCase{
+ {
+ name: "nominal",
+ },
+ {
+ name: "not found",
+ returnError: systemcontroller.NewErrExporterNotFound(""),
+ expectErrorStatusCode: http.StatusNotFound,
+ expectErrorCode: "NOT_FOUND",
+ },
+ {
+ name: "exporter used",
+ returnError: systemcontroller.NewErrExporterUsed(""),
+ expectErrorStatusCode: http.StatusBadRequest,
+ expectErrorCode: "VALIDATION",
+ },
+ {
+ name: "unknown error",
+ expectErrorCode: "INTERNAL",
+ expectErrorStatusCode: http.StatusInternalServerError,
+ returnError: errors.New("any error"),
+ },
+ } {
+ testCase := testCase
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ exporterID := uuid.NewString()
+ systemController, _ := newTestingSystemController(t, false)
+ systemController.EXPECT().
+ DeleteExporter(gomock.Any(), exporterID).
+ Return(testCase.returnError)
+
+ router := NewRouter(systemController, auth.NewNoAuth(), "develop", WithExporters(true))
+
+ req := httptest.NewRequest(http.MethodDelete, "/_/exporters/"+exporterID, nil)
+ req = req.WithContext(ctx)
+ rsp := httptest.NewRecorder()
+
+ router.ServeHTTP(rsp, req)
+
+ if testCase.expectErrorCode != "" {
+ require.Equal(t, testCase.expectErrorStatusCode, rsp.Code)
+ errorResponse := sharedapi.ErrorResponse{}
+ require.NoError(t, json.NewDecoder(rsp.Body).Decode(&errorResponse))
+ require.Equal(t, testCase.expectErrorCode, errorResponse.ErrorCode)
+ } else {
+ require.Equal(t, http.StatusNoContent, rsp.Code)
+ }
+ })
+ }
+}
diff --git a/internal/api/v2/controllers_exporters_list.go b/internal/api/v2/controllers_exporters_list.go
new file mode 100644
index 0000000000..017692da90
--- /dev/null
+++ b/internal/api/v2/controllers_exporters_list.go
@@ -0,0 +1,19 @@
+package v2
+
+import (
+ "github.com/formancehq/go-libs/v3/api"
+ systemcontroller "github.com/formancehq/ledger/internal/controller/system"
+ "net/http"
+)
+
+func listExporters(systemController systemcontroller.Controller) func(w http.ResponseWriter, r *http.Request) {
+ return func(w http.ResponseWriter, r *http.Request) {
+ exporters, err := systemController.ListExporters(r.Context())
+ if err != nil {
+ api.InternalServerError(w, r, err)
+ return
+ }
+
+ api.RenderCursor(w, *exporters)
+ }
+}
diff --git a/internal/api/v2/controllers_exporters_list_test.go b/internal/api/v2/controllers_exporters_list_test.go
new file mode 100644
index 0000000000..1dd34c28a5
--- /dev/null
+++ b/internal/api/v2/controllers_exporters_list_test.go
@@ -0,0 +1,39 @@
+package v2
+
+import (
+ "encoding/json"
+ "github.com/formancehq/go-libs/v3/auth"
+ "github.com/formancehq/go-libs/v3/bun/bunpaginate"
+ "github.com/formancehq/go-libs/v3/logging"
+ ledger "github.com/formancehq/ledger/internal"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "go.uber.org/mock/gomock"
+)
+
+func TestListExporters(t *testing.T) {
+ t.Parallel()
+
+ systemController, _ := newTestingSystemController(t, false)
+ systemController.EXPECT().
+ ListExporters(gomock.Any()).
+ Return(&bunpaginate.Cursor[ledger.Exporter]{
+ Data: []ledger.Exporter{
+ ledger.NewExporter(ledger.NewExporterConfiguration("exporter1", json.RawMessage(`{}`))),
+ ledger.NewExporter(ledger.NewExporterConfiguration("exporter2", json.RawMessage(`{}`))),
+ },
+ }, nil)
+
+ router := NewRouter(systemController, auth.NewNoAuth(), "develop", WithExporters(true))
+
+ req := httptest.NewRequest(http.MethodGet, "/_/exporters", nil)
+ rec := httptest.NewRecorder()
+ req = req.WithContext(logging.TestingContext())
+
+ router.ServeHTTP(rec, req)
+
+ require.Equal(t, http.StatusOK, rec.Code)
+}
diff --git a/internal/api/v2/controllers_exporters_read.go b/internal/api/v2/controllers_exporters_read.go
new file mode 100644
index 0000000000..15521fd019
--- /dev/null
+++ b/internal/api/v2/controllers_exporters_read.go
@@ -0,0 +1,25 @@
+package v2
+
+import (
+ "errors"
+ "github.com/formancehq/go-libs/v3/api"
+ systemcontroller "github.com/formancehq/ledger/internal/controller/system"
+ "net/http"
+)
+
+func getExporter(systemController systemcontroller.Controller) func(w http.ResponseWriter, r *http.Request) {
+ return func(w http.ResponseWriter, r *http.Request) {
+ exporter, err := systemController.GetExporter(r.Context(), getExporterID(r))
+ if err != nil {
+ switch {
+ case errors.Is(err, systemcontroller.ErrExporterNotFound("")):
+ api.NotFound(w, err)
+ default:
+ api.InternalServerError(w, r, err)
+ }
+ return
+ }
+
+ api.Ok(w, exporter)
+ }
+}
diff --git a/internal/api/v2/controllers_exporters_read_test.go b/internal/api/v2/controllers_exporters_read_test.go
new file mode 100644
index 0000000000..bf3ab25fe9
--- /dev/null
+++ b/internal/api/v2/controllers_exporters_read_test.go
@@ -0,0 +1,80 @@
+package v2
+
+import (
+ "github.com/formancehq/go-libs/v3/auth"
+ "github.com/formancehq/go-libs/v3/logging"
+ ledger "github.com/formancehq/ledger/internal"
+ systemcontroller "github.com/formancehq/ledger/internal/controller/system"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ sharedapi "github.com/formancehq/go-libs/v3/testing/api"
+ "github.com/google/uuid"
+ "github.com/pkg/errors"
+
+ "github.com/stretchr/testify/require"
+ "go.uber.org/mock/gomock"
+)
+
+func TestReadExporter(t *testing.T) {
+ t.Parallel()
+
+ type testCase struct {
+ name string
+ returnError error
+ expectSuccess bool
+ expectErrorCode string
+ expectStatusCode int
+ }
+
+ for _, testCase := range []testCase{
+ {
+ name: "nominal",
+ expectSuccess: true,
+ },
+ {
+ name: "nominal",
+ expectSuccess: true,
+ },
+ {
+ name: "not found",
+ returnError: systemcontroller.NewErrExporterNotFound(""),
+ expectStatusCode: http.StatusNotFound,
+ expectErrorCode: "NOT_FOUND",
+ },
+ {
+ name: "unknown error",
+ expectErrorCode: "INTERNAL",
+ expectStatusCode: http.StatusInternalServerError,
+ returnError: errors.New("any error"),
+ },
+ } {
+ testCase := testCase
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ exporterID := uuid.NewString()
+ systemController, _ := newTestingSystemController(t, false)
+ systemController.EXPECT().
+ GetExporter(gomock.Any(), exporterID).
+ Return(&ledger.Exporter{}, testCase.returnError)
+
+ router := NewRouter(systemController, auth.NewNoAuth(), "develop", WithExporters(true))
+
+ req := httptest.NewRequest(http.MethodGet, "/_/exporters/"+exporterID, nil)
+ req = req.WithContext(logging.TestingContext())
+ rec := httptest.NewRecorder()
+
+ router.ServeHTTP(rec, req)
+
+ if testCase.expectSuccess {
+ require.Equal(t, http.StatusOK, rec.Code)
+ } else {
+ require.Equal(t, testCase.expectStatusCode, rec.Code)
+ errorResponse := sharedapi.ReadErrorResponse(t, rec.Body)
+ require.Equal(t, testCase.expectErrorCode, errorResponse.ErrorCode)
+ }
+ })
+ }
+}
diff --git a/internal/api/v2/controllers_ledgers_create.go b/internal/api/v2/controllers_ledgers_create.go
index ac1ea9b2f3..feafd0ffc0 100644
--- a/internal/api/v2/controllers_ledgers_create.go
+++ b/internal/api/v2/controllers_ledgers_create.go
@@ -6,7 +6,7 @@ import (
"io"
"net/http"
- "github.com/formancehq/ledger/internal/controller/system"
+ systemcontroller "github.com/formancehq/ledger/internal/controller/system"
ledger "github.com/formancehq/ledger/internal"
@@ -15,7 +15,7 @@ import (
"github.com/go-chi/chi/v5"
)
-func createLedger(systemController system.Controller) http.HandlerFunc {
+func createLedger(systemController systemcontroller.Controller) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
configuration := ledger.Configuration{}
data, err := io.ReadAll(r.Body)
@@ -33,13 +33,13 @@ func createLedger(systemController system.Controller) http.HandlerFunc {
if err := systemController.CreateLedger(r.Context(), chi.URLParam(r, "ledger"), configuration); err != nil {
switch {
- case errors.Is(err, system.ErrInvalidLedgerConfiguration{}) ||
+ case errors.Is(err, systemcontroller.ErrInvalidLedgerConfiguration{}) ||
errors.Is(err, ledger.ErrInvalidLedgerName{}) ||
errors.Is(err, ledger.ErrInvalidBucketName{}):
api.BadRequest(w, common.ErrValidation, err)
- case errors.Is(err, system.ErrBucketOutdated):
+ case errors.Is(err, systemcontroller.ErrBucketOutdated):
api.BadRequest(w, common.ErrOutdatedSchema, err)
- case errors.Is(err, system.ErrLedgerAlreadyExists):
+ case errors.Is(err, systemcontroller.ErrLedgerAlreadyExists):
api.BadRequest(w, common.ErrLedgerAlreadyExists, err)
default:
common.HandleCommonErrors(w, r, err)
diff --git a/internal/api/v2/controllers_ledgers_list.go b/internal/api/v2/controllers_ledgers_list.go
index 2179e913d5..d22d7dbae4 100644
--- a/internal/api/v2/controllers_ledgers_list.go
+++ b/internal/api/v2/controllers_ledgers_list.go
@@ -3,19 +3,20 @@ package v2
import (
"github.com/formancehq/ledger/internal/api/common"
storagecommon "github.com/formancehq/ledger/internal/storage/common"
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
+ systemstore "github.com/formancehq/ledger/internal/storage/system"
"net/http"
"errors"
"github.com/formancehq/go-libs/v3/api"
"github.com/formancehq/go-libs/v3/bun/bunpaginate"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
"github.com/formancehq/ledger/internal/controller/system"
)
func listLedgers(b system.Controller, paginationConfig common.PaginationConfig) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
- rq, err := getColumnPaginatedQuery[any](r, paginationConfig, "id", bunpaginate.OrderAsc)
+ rq, err := getColumnPaginatedQuery[systemstore.ListLedgersQueryPayload](r, paginationConfig, "id", bunpaginate.OrderAsc)
if err != nil {
api.BadRequest(w, common.ErrValidation, err)
return
@@ -24,7 +25,7 @@ func listLedgers(b system.Controller, paginationConfig common.PaginationConfig)
ledgers, err := b.ListLedgers(r.Context(), *rq)
if err != nil {
switch {
- case errors.Is(err, storagecommon.ErrInvalidQuery{}) || errors.Is(err, ledgercontroller.ErrMissingFeature{}):
+ case errors.Is(err, storagecommon.ErrInvalidQuery{}) || errors.Is(err, ledgerstore.ErrMissingFeature{}):
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 449a4974e2..09ebbb6350 100644
--- a/internal/api/v2/controllers_ledgers_list_test.go
+++ b/internal/api/v2/controllers_ledgers_list_test.go
@@ -4,6 +4,8 @@ import (
"encoding/json"
"github.com/formancehq/ledger/internal/api/common"
storagecommon "github.com/formancehq/ledger/internal/storage/common"
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
+ systemstore "github.com/formancehq/ledger/internal/storage/system"
"net/http"
"net/http/httptest"
"net/url"
@@ -15,7 +17,6 @@ import (
"github.com/formancehq/go-libs/v3/bun/bunpaginate"
"github.com/formancehq/go-libs/v3/logging"
ledger "github.com/formancehq/ledger/internal"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
@@ -28,7 +29,7 @@ func TestListLedgers(t *testing.T) {
type testCase struct {
name string
- expectQuery storagecommon.ColumnPaginatedQuery[any]
+ expectQuery storagecommon.ColumnPaginatedQuery[systemstore.ListLedgersQueryPayload]
queryParams url.Values
returnData []ledger.Ledger
returnErr error
@@ -40,7 +41,7 @@ func TestListLedgers(t *testing.T) {
for _, tc := range []testCase{
{
name: "nominal",
- expectQuery: ledgercontroller.NewListLedgersQuery(15),
+ expectQuery: systemstore.NewListLedgersQuery(15),
returnData: []ledger.Ledger{
ledger.MustNewWithDefault(uuid.NewString()),
ledger.MustNewWithDefault(uuid.NewString()),
@@ -49,7 +50,7 @@ func TestListLedgers(t *testing.T) {
},
{
name: "invalid page size",
- expectQuery: ledgercontroller.NewListLedgersQuery(15),
+ expectQuery: systemstore.NewListLedgersQuery(15),
queryParams: url.Values{
"pageSize": {"-1"},
},
@@ -59,7 +60,7 @@ func TestListLedgers(t *testing.T) {
},
{
name: "error from backend",
- expectQuery: ledgercontroller.NewListLedgersQuery(15),
+ expectQuery: systemstore.NewListLedgersQuery(15),
expectedStatusCode: http.StatusInternalServerError,
expectedErrorCode: api.ErrorInternal,
expectBackendCall: true,
@@ -71,15 +72,15 @@ func TestListLedgers(t *testing.T) {
expectedErrorCode: common.ErrValidation,
expectBackendCall: true,
returnErr: storagecommon.ErrInvalidQuery{},
- expectQuery: ledgercontroller.NewListLedgersQuery(bunpaginate.QueryDefaultPageSize),
+ expectQuery: systemstore.NewListLedgersQuery(bunpaginate.QueryDefaultPageSize),
},
{
name: "with missing feature",
expectedStatusCode: http.StatusBadRequest,
expectedErrorCode: common.ErrValidation,
expectBackendCall: true,
- returnErr: ledgercontroller.ErrMissingFeature{},
- expectQuery: ledgercontroller.NewListLedgersQuery(bunpaginate.QueryDefaultPageSize),
+ returnErr: ledgerstore.ErrMissingFeature{},
+ expectQuery: systemstore.NewListLedgersQuery(bunpaginate.QueryDefaultPageSize),
},
} {
t.Run(tc.name, func(t *testing.T) {
@@ -89,7 +90,7 @@ func TestListLedgers(t *testing.T) {
if tc.expectBackendCall {
systemController.EXPECT().
- ListLedgers(gomock.Any(), ledgercontroller.NewListLedgersQuery(15)).
+ ListLedgers(gomock.Any(), systemstore.NewListLedgersQuery(15)).
Return(&bunpaginate.Cursor[ledger.Ledger]{
Data: tc.returnData,
}, tc.returnErr)
diff --git a/internal/api/v2/controllers_ledgers_update_metadata.go b/internal/api/v2/controllers_ledgers_update_metadata.go
index fca8e5dcbf..4a8a8489b5 100644
--- a/internal/api/v2/controllers_ledgers_update_metadata.go
+++ b/internal/api/v2/controllers_ledgers_update_metadata.go
@@ -1,33 +1,25 @@
package v2
import (
- "encoding/json"
+ "github.com/go-chi/chi/v5"
"net/http"
"github.com/formancehq/ledger/internal/api/common"
- "errors"
-
"github.com/formancehq/go-libs/v3/api"
"github.com/formancehq/go-libs/v3/metadata"
systemcontroller "github.com/formancehq/ledger/internal/controller/system"
- "github.com/go-chi/chi/v5"
)
func updateLedgerMetadata(systemController systemcontroller.Controller) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
-
- m := metadata.Metadata{}
- if err := json.NewDecoder(r.Body).Decode(&m); err != nil {
- api.BadRequest(w, common.ErrValidation, errors.New("invalid format"))
- return
- }
-
- if err := systemController.UpdateLedgerMetadata(r.Context(), chi.URLParam(r, "ledger"), m); err != nil {
- common.HandleCommonWriteErrors(w, r, err)
- return
- }
-
- api.NoContent(w)
+ common.WithBody(w, r, func(m metadata.Metadata) {
+ if err := systemController.UpdateLedgerMetadata(r.Context(), chi.URLParam(r, "ledger"), m); err != nil {
+ common.HandleCommonWriteErrors(w, r, err)
+ return
+ }
+
+ api.NoContent(w)
+ })
}
}
diff --git a/internal/api/v2/controllers_pipeline_create.go b/internal/api/v2/controllers_pipeline_create.go
new file mode 100644
index 0000000000..fb7131e9e8
--- /dev/null
+++ b/internal/api/v2/controllers_pipeline_create.go
@@ -0,0 +1,41 @@
+package v2
+
+import (
+ ledger "github.com/formancehq/ledger/internal"
+ "github.com/formancehq/ledger/internal/api/common"
+ ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
+ systemcontroller "github.com/formancehq/ledger/internal/controller/system"
+ "net/http"
+
+ "github.com/pkg/errors"
+
+ "github.com/formancehq/go-libs/v3/api"
+)
+
+type PipelineConfiguration struct {
+ ExporterID string `json:"exporterID"`
+}
+
+func createPipeline(systemController systemcontroller.Controller) func(w http.ResponseWriter, r *http.Request) {
+ return func(w http.ResponseWriter, r *http.Request) {
+ common.WithBody[PipelineConfiguration](w, r, func(req PipelineConfiguration) {
+ p, err := systemController.CreatePipeline(r.Context(), ledger.PipelineConfiguration{
+ ExporterID: req.ExporterID,
+ Ledger: common.LedgerFromContext(r.Context()).Info().Name,
+ })
+ if err != nil {
+ switch {
+ case errors.Is(err, systemcontroller.ErrExporterNotFound("")) ||
+ errors.Is(err, ledger.ErrPipelineAlreadyExists{}) ||
+ errors.Is(err, ledgercontroller.ErrInUsePipeline("")):
+ api.BadRequest(w, "VALIDATION", err)
+ default:
+ api.InternalServerError(w, r, err)
+ }
+ return
+ }
+
+ api.Created(w, p)
+ })
+ }
+}
diff --git a/internal/api/v2/controllers_pipeline_create_test.go b/internal/api/v2/controllers_pipeline_create_test.go
new file mode 100644
index 0000000000..695500332d
--- /dev/null
+++ b/internal/api/v2/controllers_pipeline_create_test.go
@@ -0,0 +1,100 @@
+package v2
+
+import (
+ "encoding/json"
+ "github.com/formancehq/go-libs/v3/auth"
+ ledger "github.com/formancehq/ledger/internal"
+ ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
+ systemcontroller "github.com/formancehq/ledger/internal/controller/system"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/google/uuid"
+
+ sharedapi "github.com/formancehq/go-libs/v3/api"
+ "github.com/pkg/errors"
+
+ "github.com/formancehq/go-libs/v3/logging"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/mock/gomock"
+)
+
+func TestCreatePipeline(t *testing.T) {
+ t.Parallel()
+
+ ctx := logging.TestingContext()
+
+ type testCase struct {
+ name string
+ returnError error
+ expectErrorStatusCode int
+ expectErrorCode string
+ }
+ for _, testCase := range []testCase{
+ {
+ name: "nominal",
+ },
+ {
+ name: "pipeline already exists",
+ returnError: &ledger.ErrPipelineAlreadyExists{},
+ expectErrorStatusCode: http.StatusBadRequest,
+ expectErrorCode: "VALIDATION",
+ },
+ {
+ name: "exporter not available",
+ returnError: systemcontroller.NewErrExporterNotFound("exporter1"),
+ expectErrorStatusCode: http.StatusBadRequest,
+ expectErrorCode: "VALIDATION",
+ },
+ {
+ name: "pipeline actually used",
+ returnError: ledgercontroller.NewErrInUsePipeline(""),
+ expectErrorStatusCode: http.StatusBadRequest,
+ expectErrorCode: "VALIDATION",
+ },
+ {
+ name: "unknown error",
+ returnError: errors.New("unknown error"),
+ expectErrorStatusCode: http.StatusInternalServerError,
+ expectErrorCode: "INTERNAL",
+ },
+ } {
+ testCase := testCase
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ systemController, ledgerController := newTestingSystemController(t, true)
+ router := NewRouter(systemController, auth.NewNoAuth(), "develop", WithExporters(true))
+
+ pipelineConfiguration := ledger.PipelineConfiguration{
+ Ledger: "module1",
+ ExporterID: uuid.NewString(),
+ }
+ req := httptest.NewRequest(http.MethodPost, "/"+pipelineConfiguration.Ledger+"/pipelines", sharedapi.Buffer(t, pipelineConfiguration))
+ req = req.WithContext(ctx)
+ rec := httptest.NewRecorder()
+
+ systemController.EXPECT().
+ CreatePipeline(gomock.Any(), pipelineConfiguration).
+ Return(nil, testCase.returnError)
+
+ ledgerController.EXPECT().
+ Info().
+ Return(ledger.Ledger{
+ Name: pipelineConfiguration.Ledger,
+ })
+
+ router.ServeHTTP(rec, req)
+
+ if testCase.expectErrorCode != "" {
+ require.Equal(t, testCase.expectErrorStatusCode, rec.Code)
+ errorResponse := sharedapi.ErrorResponse{}
+ require.NoError(t, json.NewDecoder(rec.Body).Decode(&errorResponse))
+ require.Equal(t, testCase.expectErrorCode, errorResponse.ErrorCode)
+ } else {
+ require.Equal(t, http.StatusCreated, rec.Code)
+ }
+ })
+ }
+}
diff --git a/internal/api/v2/controllers_pipeline_delete.go b/internal/api/v2/controllers_pipeline_delete.go
new file mode 100644
index 0000000000..e862c07dce
--- /dev/null
+++ b/internal/api/v2/controllers_pipeline_delete.go
@@ -0,0 +1,29 @@
+package v2
+
+import (
+ "github.com/formancehq/ledger/internal"
+ ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
+ systemcontroller "github.com/formancehq/ledger/internal/controller/system"
+ "net/http"
+
+ "github.com/pkg/errors"
+
+ "github.com/formancehq/go-libs/v3/api"
+)
+
+func deletePipeline(systemController systemcontroller.Controller) func(w http.ResponseWriter, r *http.Request) {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if err := systemController.DeletePipeline(r.Context(), getPipelineID(r)); err != nil {
+ switch {
+ case errors.Is(err, ledger.ErrPipelineNotFound("")):
+ api.NotFound(w, err)
+ case errors.Is(err, ledgercontroller.ErrInUsePipeline("")):
+ api.BadRequest(w, "VALIDATION", err)
+ default:
+ api.InternalServerError(w, r, err)
+ }
+ return
+ }
+ w.WriteHeader(http.StatusNoContent)
+ }
+}
diff --git a/internal/api/v2/controllers_pipeline_delete_test.go b/internal/api/v2/controllers_pipeline_delete_test.go
new file mode 100644
index 0000000000..8ad55067af
--- /dev/null
+++ b/internal/api/v2/controllers_pipeline_delete_test.go
@@ -0,0 +1,80 @@
+package v2
+
+import (
+ "encoding/json"
+ "github.com/formancehq/go-libs/v3/auth"
+ ledger "github.com/formancehq/ledger/internal"
+ ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/google/uuid"
+
+ sharedapi "github.com/formancehq/go-libs/v3/api"
+ "github.com/pkg/errors"
+
+ "github.com/stretchr/testify/require"
+ "go.uber.org/mock/gomock"
+)
+
+func TestDeletePipeline(t *testing.T) {
+ t.Parallel()
+
+ type testCase struct {
+ name string
+ returnError error
+ expectErrorStatusCode int
+ expectErrorCode string
+ }
+ for _, testCase := range []testCase{
+ {
+ name: "nominal",
+ },
+ {
+ name: "with pipeline not existing",
+ returnError: ledger.ErrPipelineNotFound(""),
+ expectErrorStatusCode: http.StatusNotFound,
+ expectErrorCode: "NOT_FOUND",
+ },
+ {
+ name: "with unknown error",
+ returnError: errors.New("unknown error"),
+ expectErrorStatusCode: http.StatusInternalServerError,
+ expectErrorCode: "INTERNAL",
+ },
+ {
+ name: "pipeline actually used",
+ returnError: ledgercontroller.NewErrInUsePipeline(""),
+ expectErrorStatusCode: http.StatusBadRequest,
+ expectErrorCode: "VALIDATION",
+ },
+ } {
+ testCase := testCase
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ systemController, _ := newTestingSystemController(t, true)
+ router := NewRouter(systemController, auth.NewNoAuth(), "develop", WithExporters(true))
+
+ exporterID := uuid.NewString()
+ req := httptest.NewRequest(http.MethodDelete, "/xxx/pipelines/"+exporterID, nil)
+ rec := httptest.NewRecorder()
+
+ systemController.EXPECT().
+ DeletePipeline(gomock.Any(), exporterID).
+ Return(testCase.returnError)
+
+ router.ServeHTTP(rec, req)
+
+ if testCase.expectErrorCode != "" {
+ require.Equal(t, testCase.expectErrorStatusCode, rec.Code)
+ errorResponse := sharedapi.ErrorResponse{}
+ require.NoError(t, json.NewDecoder(rec.Body).Decode(&errorResponse))
+ require.Equal(t, testCase.expectErrorCode, errorResponse.ErrorCode)
+ } else {
+ require.Equal(t, http.StatusNoContent, rec.Code)
+ }
+ })
+ }
+}
diff --git a/internal/api/v2/controllers_pipeline_list.go b/internal/api/v2/controllers_pipeline_list.go
new file mode 100644
index 0000000000..bc28bd82a8
--- /dev/null
+++ b/internal/api/v2/controllers_pipeline_list.go
@@ -0,0 +1,21 @@
+package v2
+
+import (
+ systemcontroller "github.com/formancehq/ledger/internal/controller/system"
+ "net/http"
+
+ "github.com/formancehq/go-libs/v3/api"
+)
+
+func listPipelines(systemController systemcontroller.Controller) func(w http.ResponseWriter, r *http.Request) {
+ return func(w http.ResponseWriter, r *http.Request) {
+ pipelines, err := systemController.ListPipelines(r.Context())
+ if err != nil {
+ api.InternalServerError(w, r, err)
+ return
+ }
+
+ api.RenderCursor(w, *pipelines)
+ }
+
+}
diff --git a/internal/api/v2/controllers_pipeline_list_test.go b/internal/api/v2/controllers_pipeline_list_test.go
new file mode 100644
index 0000000000..16a38670cb
--- /dev/null
+++ b/internal/api/v2/controllers_pipeline_list_test.go
@@ -0,0 +1,37 @@
+package v2
+
+import (
+ "github.com/formancehq/go-libs/v3/auth"
+ ledger "github.com/formancehq/ledger/internal"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/formancehq/go-libs/v3/bun/bunpaginate"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/mock/gomock"
+)
+
+func TestListPipelines(t *testing.T) {
+ t.Parallel()
+
+ systemController, _ := newTestingSystemController(t, true)
+ router := NewRouter(systemController, auth.NewNoAuth(), "develop", WithExporters(true))
+
+ req := httptest.NewRequest(http.MethodGet, "/xxx/pipelines", nil)
+ rec := httptest.NewRecorder()
+
+ pipelines := []ledger.Pipeline{
+ ledger.NewPipeline(ledger.NewPipelineConfiguration("module1", "exporter1")),
+ ledger.NewPipeline(ledger.NewPipelineConfiguration("module2", "exporter2")),
+ }
+ systemController.EXPECT().
+ ListPipelines(gomock.Any()).
+ Return(&bunpaginate.Cursor[ledger.Pipeline]{
+ Data: pipelines,
+ }, nil)
+
+ router.ServeHTTP(rec, req)
+
+ require.Equal(t, http.StatusOK, rec.Code)
+}
diff --git a/internal/api/v2/controllers_pipeline_read.go b/internal/api/v2/controllers_pipeline_read.go
new file mode 100644
index 0000000000..aa6dca031a
--- /dev/null
+++ b/internal/api/v2/controllers_pipeline_read.go
@@ -0,0 +1,29 @@
+package v2
+
+import (
+ ledger "github.com/formancehq/ledger/internal"
+ systemcontroller "github.com/formancehq/ledger/internal/controller/system"
+ "net/http"
+
+ "github.com/pkg/errors"
+
+ "github.com/formancehq/go-libs/v3/api"
+)
+
+func readPipeline(systemController systemcontroller.Controller) func(w http.ResponseWriter, r *http.Request) {
+ return func(w http.ResponseWriter, r *http.Request) {
+ pipeline, err := systemController.GetPipeline(r.Context(), getPipelineID(r))
+ if err != nil {
+ switch {
+ case errors.Is(err, ledger.ErrPipelineNotFound("")):
+ api.NotFound(w, err)
+ default:
+ api.InternalServerError(w, r, err)
+ }
+ return
+ }
+
+ api.Ok(w, pipeline)
+ }
+
+}
diff --git a/internal/api/v2/controllers_pipeline_read_test.go b/internal/api/v2/controllers_pipeline_read_test.go
new file mode 100644
index 0000000000..ae398f64a7
--- /dev/null
+++ b/internal/api/v2/controllers_pipeline_read_test.go
@@ -0,0 +1,74 @@
+package v2
+
+import (
+ "github.com/formancehq/go-libs/v3/auth"
+ ledger "github.com/formancehq/ledger/internal"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ sharedapi "github.com/formancehq/go-libs/v3/testing/api"
+ "github.com/google/uuid"
+
+ "github.com/pkg/errors"
+
+ "github.com/stretchr/testify/require"
+ "go.uber.org/mock/gomock"
+)
+
+func TestReadPipeline(t *testing.T) {
+ t.Parallel()
+
+ type testCase struct {
+ name string
+ returnError error
+ expectSuccess bool
+ expectErrorCode string
+ expectCode int
+ }
+
+ for _, testCase := range []testCase{
+ {
+ name: "nominal",
+ expectSuccess: true,
+ },
+ {
+ name: "pipeline not exists",
+ expectErrorCode: "NOT_FOUND",
+ expectCode: http.StatusNotFound,
+ returnError: ledger.ErrPipelineNotFound(""),
+ },
+ {
+ name: "unknown error",
+ expectErrorCode: "INTERNAL",
+ expectCode: http.StatusInternalServerError,
+ returnError: errors.New("internal error"),
+ },
+ } {
+ testCase := testCase
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ systemController, _ := newTestingSystemController(t, true)
+ router := NewRouter(systemController, auth.NewNoAuth(), "develop", WithExporters(true))
+
+ exporterID := uuid.NewString()
+ req := httptest.NewRequest(http.MethodGet, "/xxx/pipelines/"+exporterID, nil)
+ rec := httptest.NewRecorder()
+
+ systemController.EXPECT().
+ GetPipeline(gomock.Any(), exporterID).
+ Return(&ledger.Pipeline{}, testCase.returnError)
+
+ router.ServeHTTP(rec, req)
+
+ if testCase.expectSuccess {
+ require.Equal(t, http.StatusOK, rec.Code)
+ } else {
+ require.Equal(t, testCase.expectCode, rec.Code)
+ errorResponse := sharedapi.ReadErrorResponse(t, rec.Body)
+ require.Equal(t, testCase.expectErrorCode, errorResponse.ErrorCode)
+ }
+ })
+ }
+}
diff --git a/internal/api/v2/controllers_pipeline_reset.go b/internal/api/v2/controllers_pipeline_reset.go
new file mode 100644
index 0000000000..7da5b2734d
--- /dev/null
+++ b/internal/api/v2/controllers_pipeline_reset.go
@@ -0,0 +1,30 @@
+package v2
+
+import (
+ ledger "github.com/formancehq/ledger/internal"
+ ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
+ systemcontroller "github.com/formancehq/ledger/internal/controller/system"
+ "net/http"
+
+ "github.com/formancehq/go-libs/v3/api"
+ "github.com/pkg/errors"
+)
+
+func resetPipeline(systemController systemcontroller.Controller) func(w http.ResponseWriter, r *http.Request) {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if err := systemController.ResetPipeline(r.Context(), getPipelineID(r)); err != nil {
+ switch {
+ case errors.Is(err, ledger.ErrPipelineNotFound("")):
+ api.NotFound(w, err)
+ case errors.Is(err, ledgercontroller.ErrInUsePipeline("")):
+ api.BadRequest(w, "VALIDATION", err)
+ default:
+ api.InternalServerError(w, r, err)
+ }
+ return
+ }
+
+ w.WriteHeader(http.StatusAccepted)
+ }
+
+}
diff --git a/internal/api/v2/controllers_pipeline_reset_test.go b/internal/api/v2/controllers_pipeline_reset_test.go
new file mode 100644
index 0000000000..ea9e44865a
--- /dev/null
+++ b/internal/api/v2/controllers_pipeline_reset_test.go
@@ -0,0 +1,81 @@
+package v2
+
+import (
+ "github.com/formancehq/go-libs/v3/auth"
+ ledger "github.com/formancehq/ledger/internal"
+ ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ sharedapi "github.com/formancehq/go-libs/v3/testing/api"
+ "github.com/google/uuid"
+
+ "github.com/pkg/errors"
+
+ "github.com/stretchr/testify/require"
+ "go.uber.org/mock/gomock"
+)
+
+func TestResetPipeline(t *testing.T) {
+ t.Parallel()
+
+ type testCase struct {
+ name string
+ returnError error
+ expectSuccess bool
+ expectErrorCode string
+ expectCode int
+ }
+
+ for _, testCase := range []testCase{
+ {
+ name: "nominal",
+ expectSuccess: true,
+ },
+ {
+ name: "undefined error",
+ expectErrorCode: "INTERNAL",
+ expectCode: http.StatusInternalServerError,
+ returnError: errors.New("unknown error"),
+ },
+ {
+ name: "pipeline not found",
+ expectErrorCode: "NOT_FOUND",
+ expectCode: http.StatusNotFound,
+ returnError: ledger.ErrPipelineNotFound(""),
+ },
+ {
+ name: "pipeline actually used",
+ returnError: ledgercontroller.NewErrInUsePipeline(""),
+ expectCode: http.StatusBadRequest,
+ expectErrorCode: "VALIDATION",
+ },
+ } {
+ testCase := testCase
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ systemController, _ := newTestingSystemController(t, true)
+ router := NewRouter(systemController, auth.NewNoAuth(), "develop", WithExporters(true))
+
+ exporterID := uuid.NewString()
+ req := httptest.NewRequest(http.MethodPost, "/xxx/pipelines/"+exporterID+"/reset", nil)
+ rec := httptest.NewRecorder()
+
+ systemController.EXPECT().
+ ResetPipeline(gomock.Any(), exporterID).
+ Return(testCase.returnError)
+
+ router.ServeHTTP(rec, req)
+
+ if testCase.expectSuccess {
+ require.Equal(t, http.StatusAccepted, rec.Code)
+ } else {
+ require.Equal(t, testCase.expectCode, rec.Code)
+ errorResponse := sharedapi.ReadErrorResponse(t, rec.Body)
+ require.Equal(t, testCase.expectErrorCode, errorResponse.ErrorCode)
+ }
+ })
+ }
+}
diff --git a/internal/api/v2/controllers_pipeline_start.go b/internal/api/v2/controllers_pipeline_start.go
new file mode 100644
index 0000000000..463f0da4f9
--- /dev/null
+++ b/internal/api/v2/controllers_pipeline_start.go
@@ -0,0 +1,31 @@
+package v2
+
+import (
+ ledger "github.com/formancehq/ledger/internal"
+ ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
+ systemcontroller "github.com/formancehq/ledger/internal/controller/system"
+ "net/http"
+
+ "github.com/formancehq/go-libs/v3/api"
+ "github.com/pkg/errors"
+)
+
+func startPipeline(systemController systemcontroller.Controller) func(w http.ResponseWriter, r *http.Request) {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if err := systemController.StartPipeline(r.Context(), getPipelineID(r)); err != nil {
+ switch {
+ case errors.Is(err, ledger.ErrPipelineNotFound("")):
+ api.NotFound(w, err)
+ case errors.Is(err, ledger.ErrAlreadyStarted("")) ||
+ errors.Is(err, ledgercontroller.ErrInUsePipeline("")):
+ api.BadRequest(w, "VALIDATION", err)
+ default:
+ api.InternalServerError(w, r, err)
+ }
+ return
+ }
+
+ w.WriteHeader(http.StatusAccepted)
+ }
+
+}
diff --git a/internal/api/v2/controllers_pipeline_start_test.go b/internal/api/v2/controllers_pipeline_start_test.go
new file mode 100644
index 0000000000..d67b4e78b4
--- /dev/null
+++ b/internal/api/v2/controllers_pipeline_start_test.go
@@ -0,0 +1,87 @@
+package v2
+
+import (
+ "github.com/formancehq/go-libs/v3/auth"
+ "github.com/formancehq/ledger/internal"
+ ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ sharedapi "github.com/formancehq/go-libs/v3/testing/api"
+ "github.com/google/uuid"
+
+ "github.com/pkg/errors"
+
+ "github.com/stretchr/testify/require"
+ "go.uber.org/mock/gomock"
+)
+
+func TestStartPipeline(t *testing.T) {
+ t.Parallel()
+
+ type testCase struct {
+ name string
+ returnError error
+ expectSuccess bool
+ expectErrorCode string
+ expectCode int
+ }
+
+ for _, testCase := range []testCase{
+ {
+ name: "nominal",
+ expectSuccess: true,
+ },
+ {
+ name: "pipeline not exists",
+ expectErrorCode: "NOT_FOUND",
+ expectCode: http.StatusNotFound,
+ returnError: ledger.ErrPipelineNotFound(""),
+ },
+ {
+ name: "pipeline already started",
+ expectErrorCode: "VALIDATION",
+ expectCode: http.StatusBadRequest,
+ returnError: ledger.ErrAlreadyStarted(""),
+ },
+ {
+ name: "undefined error",
+ expectErrorCode: "INTERNAL",
+ expectCode: http.StatusInternalServerError,
+ returnError: errors.New("unknown error"),
+ },
+ {
+ name: "pipeline actually used",
+ returnError: ledgercontroller.NewErrInUsePipeline(""),
+ expectCode: http.StatusBadRequest,
+ expectErrorCode: "VALIDATION",
+ },
+ } {
+ testCase := testCase
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ systemController, _ := newTestingSystemController(t, true)
+ router := NewRouter(systemController, auth.NewNoAuth(), "develop", WithExporters(true))
+
+ exporterID := uuid.NewString()
+ req := httptest.NewRequest(http.MethodPost, "/xxx/pipelines/"+exporterID+"/start", nil)
+ rec := httptest.NewRecorder()
+
+ systemController.EXPECT().
+ StartPipeline(gomock.Any(), exporterID).
+ Return(testCase.returnError)
+
+ router.ServeHTTP(rec, req)
+
+ if testCase.expectSuccess {
+ require.Equal(t, http.StatusAccepted, rec.Code)
+ } else {
+ require.Equal(t, testCase.expectCode, rec.Code)
+ errorResponse := sharedapi.ReadErrorResponse(t, rec.Body)
+ require.Equal(t, testCase.expectErrorCode, errorResponse.ErrorCode)
+ }
+ })
+ }
+}
diff --git a/internal/api/v2/controllers_pipeline_stop.go b/internal/api/v2/controllers_pipeline_stop.go
new file mode 100644
index 0000000000..fea43a562b
--- /dev/null
+++ b/internal/api/v2/controllers_pipeline_stop.go
@@ -0,0 +1,28 @@
+package v2
+
+import (
+ "github.com/formancehq/go-libs/v3/api"
+ ledger "github.com/formancehq/ledger/internal"
+ ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
+ systemcontroller "github.com/formancehq/ledger/internal/controller/system"
+ "github.com/pkg/errors"
+ "net/http"
+)
+
+func stopPipeline(systemController systemcontroller.Controller) func(w http.ResponseWriter, r *http.Request) {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if err := systemController.StopPipeline(r.Context(), getPipelineID(r)); err != nil {
+ switch {
+ case errors.Is(err, ledger.ErrPipelineNotFound("")):
+ api.NotFound(w, err)
+ case errors.Is(err, ledgercontroller.ErrInUsePipeline("")):
+ api.BadRequest(w, "VALIDATION", err)
+ default:
+ api.InternalServerError(w, r, err)
+ }
+ return
+ }
+
+ w.WriteHeader(http.StatusAccepted)
+ }
+}
diff --git a/internal/api/v2/controllers_pipeline_stop_test.go b/internal/api/v2/controllers_pipeline_stop_test.go
new file mode 100644
index 0000000000..56057bf6e8
--- /dev/null
+++ b/internal/api/v2/controllers_pipeline_stop_test.go
@@ -0,0 +1,84 @@
+package v2
+
+import (
+ "github.com/formancehq/go-libs/v3/auth"
+ ledger "github.com/formancehq/ledger/internal"
+ ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ sharedapi "github.com/formancehq/go-libs/v3/testing/api"
+ "github.com/google/uuid"
+
+ "github.com/pkg/errors"
+
+ "github.com/formancehq/go-libs/v3/logging"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/mock/gomock"
+)
+
+func TestStopPipeline(t *testing.T) {
+ t.Parallel()
+ ctx := logging.TestingContext()
+
+ type testCase struct {
+ name string
+ returnError error
+ expectSuccess bool
+ expectErrorCode string
+ expectCode int
+ }
+
+ for _, testCase := range []testCase{
+ {
+ name: "nominal",
+ expectSuccess: true,
+ },
+ {
+ name: "pipeline not exists",
+ expectErrorCode: "NOT_FOUND",
+ expectCode: http.StatusNotFound,
+ returnError: ledger.ErrPipelineNotFound(""),
+ },
+ {
+ name: "unknown error",
+ expectErrorCode: "INTERNAL",
+ expectCode: http.StatusInternalServerError,
+ returnError: errors.New("internal error"),
+ },
+ {
+ name: "pipeline actually used",
+ returnError: ledgercontroller.NewErrInUsePipeline(""),
+ expectCode: http.StatusBadRequest,
+ expectErrorCode: "VALIDATION",
+ },
+ } {
+ testCase := testCase
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ systemController, _ := newTestingSystemController(t, true)
+ router := NewRouter(systemController, auth.NewNoAuth(), "develop", WithExporters(true))
+
+ exporterID := uuid.NewString()
+ req := httptest.NewRequest(http.MethodPost, "/xxx/pipelines/"+exporterID+"/stop", nil)
+ req = req.WithContext(ctx)
+ rec := httptest.NewRecorder()
+
+ systemController.EXPECT().
+ StopPipeline(gomock.Any(), exporterID).
+ Return(testCase.returnError)
+
+ router.ServeHTTP(rec, req)
+
+ if testCase.expectSuccess {
+ require.Equal(t, http.StatusAccepted, rec.Code)
+ } else {
+ require.Equal(t, testCase.expectCode, rec.Code)
+ errorResponse := sharedapi.ReadErrorResponse(t, rec.Body)
+ require.Equal(t, testCase.expectErrorCode, errorResponse.ErrorCode)
+ }
+ })
+ }
+}
diff --git a/internal/api/v2/controllers_transactions_add_metadata.go b/internal/api/v2/controllers_transactions_add_metadata.go
index 2bb49a371a..b49b9a70b1 100644
--- a/internal/api/v2/controllers_transactions_add_metadata.go
+++ b/internal/api/v2/controllers_transactions_add_metadata.go
@@ -1,7 +1,6 @@
package v2
import (
- "encoding/json"
"net/http"
"strconv"
@@ -18,30 +17,26 @@ import (
func addTransactionMetadata(w http.ResponseWriter, r *http.Request) {
l := common.LedgerFromContext(r.Context())
- var m metadata.Metadata
- if err := json.NewDecoder(r.Body).Decode(&m); err != nil {
- api.BadRequest(w, common.ErrValidation, errors.New("invalid metadata format"))
- return
- }
-
- txID, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 64)
- if err != nil {
- api.BadRequest(w, common.ErrValidation, err)
- return
- }
-
- if _, err := l.SaveTransactionMetadata(r.Context(), getCommandParameters(r, ledgercontroller.SaveTransactionMetadata{
- TransactionID: txID,
- Metadata: m,
- })); err != nil {
- switch {
- case errors.Is(err, ledgercontroller.ErrNotFound):
- api.NotFound(w, err)
- default:
- common.HandleCommonWriteErrors(w, r, err)
+ common.WithBody(w, r, func(m metadata.Metadata) {
+ txID, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 64)
+ if err != nil {
+ api.BadRequest(w, common.ErrValidation, err)
+ return
}
- return
- }
- api.NoContent(w)
+ if _, err := l.SaveTransactionMetadata(r.Context(), getCommandParameters(r, ledgercontroller.SaveTransactionMetadata{
+ TransactionID: txID,
+ Metadata: m,
+ })); err != nil {
+ switch {
+ case errors.Is(err, ledgercontroller.ErrNotFound):
+ api.NotFound(w, err)
+ default:
+ common.HandleCommonWriteErrors(w, r, err)
+ }
+ return
+ }
+
+ api.NoContent(w)
+ })
}
diff --git a/internal/api/v2/controllers_transactions_count.go b/internal/api/v2/controllers_transactions_count.go
index 63a986466f..ef76495970 100644
--- a/internal/api/v2/controllers_transactions_count.go
+++ b/internal/api/v2/controllers_transactions_count.go
@@ -3,12 +3,12 @@ package v2
import (
"fmt"
storagecommon "github.com/formancehq/ledger/internal/storage/common"
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
"net/http"
"errors"
"github.com/formancehq/go-libs/v3/api"
"github.com/formancehq/ledger/internal/api/common"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
)
func countTransactions(w http.ResponseWriter, r *http.Request) {
@@ -22,7 +22,7 @@ func countTransactions(w http.ResponseWriter, r *http.Request) {
count, err := common.LedgerFromContext(r.Context()).CountTransactions(r.Context(), *rq)
if err != nil {
switch {
- case errors.Is(err, storagecommon.ErrInvalidQuery{}) || errors.Is(err, ledgercontroller.ErrMissingFeature{}):
+ case errors.Is(err, storagecommon.ErrInvalidQuery{}) || errors.Is(err, ledgerstore.ErrMissingFeature{}):
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 e699a5b444..dbc6cf0f3c 100644
--- a/internal/api/v2/controllers_transactions_count_test.go
+++ b/internal/api/v2/controllers_transactions_count_test.go
@@ -5,6 +5,7 @@ import (
"fmt"
"github.com/formancehq/ledger/internal/api/common"
storagecommon "github.com/formancehq/ledger/internal/storage/common"
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
"net/http"
"net/http/httptest"
"net/url"
@@ -15,7 +16,6 @@ import (
"github.com/formancehq/go-libs/v3/auth"
"github.com/formancehq/go-libs/v3/query"
"github.com/formancehq/go-libs/v3/time"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
@@ -143,7 +143,7 @@ func TestTransactionsCount(t *testing.T) {
expectStatusCode: http.StatusBadRequest,
expectedErrorCode: common.ErrValidation,
expectBackendCall: true,
- returnErr: ledgercontroller.ErrMissingFeature{},
+ returnErr: ledgerstore.ErrMissingFeature{},
expectQuery: storagecommon.ResourceQuery[any]{
PIT: &before,
Expand: make([]string, 0),
diff --git a/internal/api/v2/controllers_transactions_create.go b/internal/api/v2/controllers_transactions_create.go
index 5a2d37ad97..f4faa19c68 100644
--- a/internal/api/v2/controllers_transactions_create.go
+++ b/internal/api/v2/controllers_transactions_create.go
@@ -1,8 +1,8 @@
package v2
import (
- "encoding/json"
"github.com/formancehq/ledger/internal/api/bulking"
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
"net/http"
ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
@@ -15,58 +15,53 @@ import (
)
func createTransaction(w http.ResponseWriter, r *http.Request) {
- l := common.LedgerFromContext(r.Context())
+ common.WithBody(w, r, func(payload bulking.TransactionRequest) {
+ l := common.LedgerFromContext(r.Context())
- payload := bulking.TransactionRequest{}
- if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
- api.BadRequest(w, common.ErrValidation, errors.New("invalid transaction format"))
- return
- }
-
- if len(payload.Postings) > 0 && payload.Script.Plain != "" {
- 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, common.ErrNoPostings, errors.New("you need to pass either a posting array or a numscript script"))
- return
- }
+ if len(payload.Postings) > 0 && payload.Script.Plain != "" {
+ api.BadRequest(w, common.ErrValidation, errors.New("cannot pass postings and numscript in the same request"))
+ return
+ }
- // nodes(gfyrag): parameter 'force' initially sent using a query param
- // while we still support the feature, we can also send the 'force' parameter
- // in the request payload.
- // it allows to leverage the feature on bulk endpoint
- payload.Force = payload.Force || api.QueryParamBool(r, "force")
+ if len(payload.Postings) == 0 && payload.Script.Plain == "" {
+ api.BadRequest(w, common.ErrNoPostings, errors.New("you need to pass either a posting array or a numscript script"))
+ return
+ }
+ // nodes(gfyrag): parameter 'force' initially sent using a query param
+ // while we still support the feature, we can also send the 'force' parameter
+ // in the request payload.
+ // it allows to leverage the feature on bulk endpoint
+ payload.Force = payload.Force || api.QueryParamBool(r, "force")
- createTransaction, err := payload.ToCore()
- if err != nil {
- api.BadRequest(w, common.ErrValidation, err)
- return
- }
+ createTransaction, err := payload.ToCore()
+ if err != nil {
+ api.BadRequest(w, common.ErrValidation, err)
+ return
+ }
- _, res, err := l.CreateTransaction(r.Context(), getCommandParameters(r, *createTransaction))
- if err != nil {
- switch {
- case errors.Is(err, &ledgercontroller.ErrInsufficientFunds{}):
- api.BadRequest(w, common.ErrInsufficientFund, err)
- case errors.Is(err, &ledgercontroller.ErrInvalidVars{}) || errors.Is(err, ledgercontroller.ErrCompilationFailed{}):
- api.BadRequest(w, common.ErrCompilationFailed, err)
- case errors.Is(err, &ledgercontroller.ErrMetadataOverride{}):
- api.BadRequest(w, common.ErrMetadataOverride, err)
- case errors.Is(err, ledgercontroller.ErrNoPostings):
- api.BadRequest(w, common.ErrNoPostings, err)
- case errors.Is(err, ledgercontroller.ErrTransactionReferenceConflict{}):
- api.WriteErrorResponse(w, http.StatusConflict, common.ErrConflict, err)
- case errors.Is(err, ledgercontroller.ErrParsing{}):
- api.BadRequest(w, common.ErrInterpreterParse, err)
- case errors.Is(err, ledgercontroller.ErrRuntime{}):
- api.BadRequest(w, common.ErrInterpreterRuntime, err)
- default:
- common.HandleCommonWriteErrors(w, r, err)
+ _, res, err := l.CreateTransaction(r.Context(), getCommandParameters(r, *createTransaction))
+ if err != nil {
+ switch {
+ case errors.Is(err, &ledgercontroller.ErrInsufficientFunds{}):
+ api.BadRequest(w, common.ErrInsufficientFund, err)
+ case errors.Is(err, &ledgercontroller.ErrInvalidVars{}) || errors.Is(err, ledgercontroller.ErrCompilationFailed{}):
+ api.BadRequest(w, common.ErrCompilationFailed, err)
+ case errors.Is(err, &ledgercontroller.ErrMetadataOverride{}):
+ api.BadRequest(w, common.ErrMetadataOverride, err)
+ case errors.Is(err, ledgercontroller.ErrNoPostings):
+ api.BadRequest(w, common.ErrNoPostings, err)
+ case errors.Is(err, ledgerstore.ErrTransactionReferenceConflict{}):
+ api.WriteErrorResponse(w, http.StatusConflict, common.ErrConflict, err)
+ case errors.Is(err, ledgercontroller.ErrParsing{}):
+ api.BadRequest(w, common.ErrInterpreterParse, err)
+ case errors.Is(err, ledgercontroller.ErrRuntime{}):
+ api.BadRequest(w, common.ErrInterpreterRuntime, err)
+ default:
+ common.HandleCommonWriteErrors(w, r, err)
+ }
+ return
}
- return
- }
- api.Ok(w, renderTransaction(r, res.Transaction))
-}
\ No newline at end of file
+ api.Ok(w, renderTransaction(r, res.Transaction))
+ })
+}
diff --git a/internal/api/v2/controllers_transactions_list.go b/internal/api/v2/controllers_transactions_list.go
index 6ac20f0cc7..0a103de386 100644
--- a/internal/api/v2/controllers_transactions_list.go
+++ b/internal/api/v2/controllers_transactions_list.go
@@ -6,8 +6,8 @@ import (
"github.com/formancehq/go-libs/v3/bun/bunpaginate"
ledger "github.com/formancehq/ledger/internal"
"github.com/formancehq/ledger/internal/api/common"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
storagecommon "github.com/formancehq/ledger/internal/storage/common"
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
"net/http"
)
@@ -34,7 +34,7 @@ func listTransactions(paginationConfig common.PaginationConfig) http.HandlerFunc
cursor, err := l.ListTransactions(r.Context(), *rq)
if err != nil {
switch {
- case errors.Is(err, storagecommon.ErrInvalidQuery{}) || errors.Is(err, ledgercontroller.ErrMissingFeature{}):
+ case errors.Is(err, storagecommon.ErrInvalidQuery{}) || errors.Is(err, ledgerstore.ErrMissingFeature{}):
api.BadRequest(w, common.ErrValidation, err)
default:
common.HandleCommonErrors(w, r, err)
diff --git a/internal/api/v2/controllers_volumes.go b/internal/api/v2/controllers_volumes.go
index f2bc7e2359..0ab5537261 100644
--- a/internal/api/v2/controllers_volumes.go
+++ b/internal/api/v2/controllers_volumes.go
@@ -6,8 +6,8 @@ import (
"github.com/formancehq/go-libs/v3/bun/bunpaginate"
ledger "github.com/formancehq/ledger/internal"
"github.com/formancehq/ledger/internal/api/common"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
storagecommon "github.com/formancehq/ledger/internal/storage/common"
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
"net/http"
"strconv"
)
@@ -17,7 +17,7 @@ func readVolumes(paginationConfig common.PaginationConfig) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := common.LedgerFromContext(r.Context())
- rq, err := getOffsetPaginatedQuery[ledgercontroller.GetVolumesOptions](r, paginationConfig, func(opts *ledgercontroller.GetVolumesOptions) error {
+ rq, err := getOffsetPaginatedQuery[ledgerstore.GetVolumesOptions](r, paginationConfig, func(opts *ledgerstore.GetVolumesOptions) error {
groupBy := r.URL.Query().Get("groupBy")
if groupBy != "" {
v, err := strconv.ParseInt(groupBy, 10, 64)
@@ -55,7 +55,7 @@ func readVolumes(paginationConfig common.PaginationConfig) http.HandlerFunc {
cursor, err := l.GetVolumesWithBalances(r.Context(), *rq)
if err != nil {
switch {
- case errors.Is(err, storagecommon.ErrInvalidQuery{}) || errors.Is(err, ledgercontroller.ErrMissingFeature{}):
+ case errors.Is(err, storagecommon.ErrInvalidQuery{}) || errors.Is(err, ledgerstore.ErrMissingFeature{}):
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 db6cd05033..5f03272002 100644
--- a/internal/api/v2/controllers_volumes_test.go
+++ b/internal/api/v2/controllers_volumes_test.go
@@ -4,14 +4,13 @@ import (
"bytes"
"github.com/formancehq/ledger/internal/api/common"
storagecommon "github.com/formancehq/ledger/internal/storage/common"
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
"math/big"
"net/http"
"net/http/httptest"
"net/url"
"testing"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
-
"github.com/formancehq/go-libs/v3/auth"
"github.com/formancehq/go-libs/v3/bun/bunpaginate"
"github.com/formancehq/go-libs/v3/time"
@@ -31,7 +30,7 @@ func TestGetVolumes(t *testing.T) {
name string
queryParams url.Values
body string
- expectQuery storagecommon.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]
+ expectQuery storagecommon.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]
expectStatusCode int
expectedErrorCode string
}
@@ -40,9 +39,9 @@ func TestGetVolumes(t *testing.T) {
testCases := []testCase{
{
name: "basic",
- expectQuery: storagecommon.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
+ expectQuery: storagecommon.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
PageSize: bunpaginate.QueryDefaultPageSize,
- Options: storagecommon.ResourceQuery[ledgercontroller.GetVolumesOptions]{
+ Options: storagecommon.ResourceQuery[ledgerstore.GetVolumesOptions]{
PIT: &before,
Expand: make([]string, 0),
},
@@ -51,9 +50,9 @@ func TestGetVolumes(t *testing.T) {
{
name: "using metadata",
body: `{"$match": { "metadata[roles]": "admin" }}`,
- expectQuery: storagecommon.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
+ expectQuery: storagecommon.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
PageSize: bunpaginate.QueryDefaultPageSize,
- Options: storagecommon.ResourceQuery[ledgercontroller.GetVolumesOptions]{
+ Options: storagecommon.ResourceQuery[ledgerstore.GetVolumesOptions]{
PIT: &before,
Builder: query.Match("metadata[roles]", "admin"),
Expand: make([]string, 0),
@@ -63,9 +62,9 @@ func TestGetVolumes(t *testing.T) {
{
name: "using account",
body: `{"$match": { "account": "foo" }}`,
- expectQuery: storagecommon.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
+ expectQuery: storagecommon.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
PageSize: bunpaginate.QueryDefaultPageSize,
- Options: storagecommon.ResourceQuery[ledgercontroller.GetVolumesOptions]{
+ Options: storagecommon.ResourceQuery[ledgerstore.GetVolumesOptions]{
PIT: &before,
Builder: query.Match("account", "foo"),
Expand: make([]string, 0),
@@ -84,12 +83,12 @@ func TestGetVolumes(t *testing.T) {
"pit": []string{before.Format(time.RFC3339Nano)},
"groupBy": []string{"3"},
},
- expectQuery: storagecommon.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
+ expectQuery: storagecommon.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
PageSize: bunpaginate.QueryDefaultPageSize,
- Options: storagecommon.ResourceQuery[ledgercontroller.GetVolumesOptions]{
+ Options: storagecommon.ResourceQuery[ledgerstore.GetVolumesOptions]{
PIT: &before,
Expand: make([]string, 0),
- Opts: ledgercontroller.GetVolumesOptions{
+ Opts: ledgerstore.GetVolumesOptions{
GroupLvl: 3,
},
},
@@ -98,9 +97,9 @@ func TestGetVolumes(t *testing.T) {
{
name: "using Exists metadata filter",
body: `{"$exists": { "metadata": "foo" }}`,
- expectQuery: storagecommon.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
+ expectQuery: storagecommon.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
PageSize: bunpaginate.QueryDefaultPageSize,
- Options: storagecommon.ResourceQuery[ledgercontroller.GetVolumesOptions]{
+ Options: storagecommon.ResourceQuery[ledgerstore.GetVolumesOptions]{
PIT: &before,
Builder: query.Exists("metadata", "foo"),
Expand: make([]string, 0),
@@ -110,9 +109,9 @@ func TestGetVolumes(t *testing.T) {
{
name: "using balance filter",
body: `{"$gte": { "balance[EUR]": 50 }}`,
- expectQuery: storagecommon.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
+ expectQuery: storagecommon.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
PageSize: bunpaginate.QueryDefaultPageSize,
- Options: storagecommon.ResourceQuery[ledgercontroller.GetVolumesOptions]{
+ Options: storagecommon.ResourceQuery[ledgerstore.GetVolumesOptions]{
PIT: &before,
Builder: query.Gte("balance[EUR]", float64(50)),
Expand: make([]string, 0),
diff --git a/internal/api/v2/mocks_ledger_controller_test.go b/internal/api/v2/mocks_ledger_controller_test.go
index d45b1434fc..4e2b699f70 100644
--- a/internal/api/v2/mocks_ledger_controller_test.go
+++ b/internal/api/v2/mocks_ledger_controller_test.go
@@ -17,6 +17,7 @@ import (
ledger "github.com/formancehq/ledger/internal"
ledger0 "github.com/formancehq/ledger/internal/controller/ledger"
common "github.com/formancehq/ledger/internal/storage/common"
+ ledger1 "github.com/formancehq/ledger/internal/storage/ledger"
bun "github.com/uptrace/bun"
gomock "go.uber.org/mock/gomock"
)
@@ -181,7 +182,7 @@ func (mr *LedgerControllerMockRecorder) GetAccount(ctx, query any) *gomock.Call
}
// GetAggregatedBalances mocks base method.
-func (m *LedgerController) GetAggregatedBalances(ctx context.Context, q common.ResourceQuery[ledger0.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) {
+func (m *LedgerController) GetAggregatedBalances(ctx context.Context, q common.ResourceQuery[ledger1.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAggregatedBalances", ctx, q)
ret0, _ := ret[0].(ledger.BalancesByAssets)
@@ -241,7 +242,7 @@ func (mr *LedgerControllerMockRecorder) GetTransaction(ctx, query any) *gomock.C
}
// GetVolumesWithBalances mocks base method.
-func (m *LedgerController) GetVolumesWithBalances(ctx context.Context, q common.OffsetPaginatedQuery[ledger0.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) {
+func (m *LedgerController) GetVolumesWithBalances(ctx context.Context, q common.OffsetPaginatedQuery[ledger1.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])
@@ -269,6 +270,20 @@ func (mr *LedgerControllerMockRecorder) Import(ctx, stream any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Import", reflect.TypeOf((*LedgerController)(nil).Import), ctx, stream)
}
+// Info mocks base method.
+func (m *LedgerController) Info() ledger.Ledger {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Info")
+ ret0, _ := ret[0].(ledger.Ledger)
+ return ret0
+}
+
+// Info indicates an expected call of Info.
+func (mr *LedgerControllerMockRecorder) Info() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*LedgerController)(nil).Info))
+}
+
// IsDatabaseUpToDate mocks base method.
func (m *LedgerController) IsDatabaseUpToDate(ctx context.Context) (bool, error) {
m.ctrl.T.Helper()
diff --git a/internal/api/v2/mocks_system_controller_test.go b/internal/api/v2/mocks_system_controller_test.go
index e5e596f0cf..a44e567dec 100644
--- a/internal/api/v2/mocks_system_controller_test.go
+++ b/internal/api/v2/mocks_system_controller_test.go
@@ -15,9 +15,194 @@ import (
ledger "github.com/formancehq/ledger/internal"
ledger0 "github.com/formancehq/ledger/internal/controller/ledger"
common "github.com/formancehq/ledger/internal/storage/common"
+ system "github.com/formancehq/ledger/internal/storage/system"
gomock "go.uber.org/mock/gomock"
)
+// MockReplicationBackend is a mock of ReplicationBackend interface.
+type MockReplicationBackend struct {
+ ctrl *gomock.Controller
+ recorder *MockReplicationBackendMockRecorder
+ isgomock struct{}
+}
+
+// MockReplicationBackendMockRecorder is the mock recorder for MockReplicationBackend.
+type MockReplicationBackendMockRecorder struct {
+ mock *MockReplicationBackend
+}
+
+// NewMockReplicationBackend creates a new mock instance.
+func NewMockReplicationBackend(ctrl *gomock.Controller) *MockReplicationBackend {
+ mock := &MockReplicationBackend{ctrl: ctrl}
+ mock.recorder = &MockReplicationBackendMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockReplicationBackend) EXPECT() *MockReplicationBackendMockRecorder {
+ return m.recorder
+}
+
+// CreateExporter mocks base method.
+func (m *MockReplicationBackend) CreateExporter(ctx context.Context, configuration ledger.ExporterConfiguration) (*ledger.Exporter, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "CreateExporter", ctx, configuration)
+ ret0, _ := ret[0].(*ledger.Exporter)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// CreateExporter indicates an expected call of CreateExporter.
+func (mr *MockReplicationBackendMockRecorder) CreateExporter(ctx, configuration any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateExporter", reflect.TypeOf((*MockReplicationBackend)(nil).CreateExporter), ctx, configuration)
+}
+
+// CreatePipeline mocks base method.
+func (m *MockReplicationBackend) CreatePipeline(ctx context.Context, pipelineConfiguration ledger.PipelineConfiguration) (*ledger.Pipeline, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "CreatePipeline", ctx, pipelineConfiguration)
+ ret0, _ := ret[0].(*ledger.Pipeline)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// CreatePipeline indicates an expected call of CreatePipeline.
+func (mr *MockReplicationBackendMockRecorder) CreatePipeline(ctx, pipelineConfiguration any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePipeline", reflect.TypeOf((*MockReplicationBackend)(nil).CreatePipeline), ctx, pipelineConfiguration)
+}
+
+// DeleteExporter mocks base method.
+func (m *MockReplicationBackend) DeleteExporter(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "DeleteExporter", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// DeleteExporter indicates an expected call of DeleteExporter.
+func (mr *MockReplicationBackendMockRecorder) DeleteExporter(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteExporter", reflect.TypeOf((*MockReplicationBackend)(nil).DeleteExporter), ctx, id)
+}
+
+// DeletePipeline mocks base method.
+func (m *MockReplicationBackend) DeletePipeline(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "DeletePipeline", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// DeletePipeline indicates an expected call of DeletePipeline.
+func (mr *MockReplicationBackendMockRecorder) DeletePipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePipeline", reflect.TypeOf((*MockReplicationBackend)(nil).DeletePipeline), ctx, id)
+}
+
+// GetExporter mocks base method.
+func (m *MockReplicationBackend) GetExporter(ctx context.Context, id string) (*ledger.Exporter, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetExporter", ctx, id)
+ ret0, _ := ret[0].(*ledger.Exporter)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetExporter indicates an expected call of GetExporter.
+func (mr *MockReplicationBackendMockRecorder) GetExporter(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetExporter", reflect.TypeOf((*MockReplicationBackend)(nil).GetExporter), ctx, id)
+}
+
+// GetPipeline mocks base method.
+func (m *MockReplicationBackend) GetPipeline(ctx context.Context, id string) (*ledger.Pipeline, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetPipeline", ctx, id)
+ ret0, _ := ret[0].(*ledger.Pipeline)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetPipeline indicates an expected call of GetPipeline.
+func (mr *MockReplicationBackendMockRecorder) GetPipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPipeline", reflect.TypeOf((*MockReplicationBackend)(nil).GetPipeline), ctx, id)
+}
+
+// ListExporters mocks base method.
+func (m *MockReplicationBackend) ListExporters(ctx context.Context) (*bunpaginate.Cursor[ledger.Exporter], error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ListExporters", ctx)
+ ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Exporter])
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ListExporters indicates an expected call of ListExporters.
+func (mr *MockReplicationBackendMockRecorder) ListExporters(ctx any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListExporters", reflect.TypeOf((*MockReplicationBackend)(nil).ListExporters), ctx)
+}
+
+// ListPipelines mocks base method.
+func (m *MockReplicationBackend) ListPipelines(ctx context.Context) (*bunpaginate.Cursor[ledger.Pipeline], error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ListPipelines", ctx)
+ ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Pipeline])
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ListPipelines indicates an expected call of ListPipelines.
+func (mr *MockReplicationBackendMockRecorder) ListPipelines(ctx any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPipelines", reflect.TypeOf((*MockReplicationBackend)(nil).ListPipelines), ctx)
+}
+
+// ResetPipeline mocks base method.
+func (m *MockReplicationBackend) ResetPipeline(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ResetPipeline", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// ResetPipeline indicates an expected call of ResetPipeline.
+func (mr *MockReplicationBackendMockRecorder) ResetPipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResetPipeline", reflect.TypeOf((*MockReplicationBackend)(nil).ResetPipeline), ctx, id)
+}
+
+// StartPipeline mocks base method.
+func (m *MockReplicationBackend) StartPipeline(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "StartPipeline", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// StartPipeline indicates an expected call of StartPipeline.
+func (mr *MockReplicationBackendMockRecorder) StartPipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartPipeline", reflect.TypeOf((*MockReplicationBackend)(nil).StartPipeline), ctx, id)
+}
+
+// StopPipeline mocks base method.
+func (m *MockReplicationBackend) StopPipeline(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "StopPipeline", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// StopPipeline indicates an expected call of StopPipeline.
+func (mr *MockReplicationBackendMockRecorder) StopPipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StopPipeline", reflect.TypeOf((*MockReplicationBackend)(nil).StopPipeline), ctx, id)
+}
+
// SystemController is a mock of Controller interface.
type SystemController struct {
ctrl *gomock.Controller
@@ -42,6 +227,21 @@ func (m *SystemController) EXPECT() *SystemControllerMockRecorder {
return m.recorder
}
+// CreateExporter mocks base method.
+func (m *SystemController) CreateExporter(ctx context.Context, configuration ledger.ExporterConfiguration) (*ledger.Exporter, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "CreateExporter", ctx, configuration)
+ ret0, _ := ret[0].(*ledger.Exporter)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// CreateExporter indicates an expected call of CreateExporter.
+func (mr *SystemControllerMockRecorder) CreateExporter(ctx, configuration any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateExporter", reflect.TypeOf((*SystemController)(nil).CreateExporter), ctx, configuration)
+}
+
// CreateLedger mocks base method.
func (m *SystemController) CreateLedger(ctx context.Context, name string, configuration ledger.Configuration) error {
m.ctrl.T.Helper()
@@ -56,6 +256,35 @@ func (mr *SystemControllerMockRecorder) CreateLedger(ctx, name, configuration an
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateLedger", reflect.TypeOf((*SystemController)(nil).CreateLedger), ctx, name, configuration)
}
+// CreatePipeline mocks base method.
+func (m *SystemController) CreatePipeline(ctx context.Context, pipelineConfiguration ledger.PipelineConfiguration) (*ledger.Pipeline, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "CreatePipeline", ctx, pipelineConfiguration)
+ ret0, _ := ret[0].(*ledger.Pipeline)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// CreatePipeline indicates an expected call of CreatePipeline.
+func (mr *SystemControllerMockRecorder) CreatePipeline(ctx, pipelineConfiguration any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePipeline", reflect.TypeOf((*SystemController)(nil).CreatePipeline), ctx, pipelineConfiguration)
+}
+
+// DeleteExporter mocks base method.
+func (m *SystemController) DeleteExporter(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "DeleteExporter", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// DeleteExporter indicates an expected call of DeleteExporter.
+func (mr *SystemControllerMockRecorder) DeleteExporter(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteExporter", reflect.TypeOf((*SystemController)(nil).DeleteExporter), ctx, id)
+}
+
// DeleteLedgerMetadata mocks base method.
func (m *SystemController) DeleteLedgerMetadata(ctx context.Context, param, key string) error {
m.ctrl.T.Helper()
@@ -70,6 +299,35 @@ func (mr *SystemControllerMockRecorder) DeleteLedgerMetadata(ctx, param, key any
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLedgerMetadata", reflect.TypeOf((*SystemController)(nil).DeleteLedgerMetadata), ctx, param, key)
}
+// DeletePipeline mocks base method.
+func (m *SystemController) DeletePipeline(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "DeletePipeline", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// DeletePipeline indicates an expected call of DeletePipeline.
+func (mr *SystemControllerMockRecorder) DeletePipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePipeline", reflect.TypeOf((*SystemController)(nil).DeletePipeline), ctx, id)
+}
+
+// GetExporter mocks base method.
+func (m *SystemController) GetExporter(ctx context.Context, id string) (*ledger.Exporter, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetExporter", ctx, id)
+ ret0, _ := ret[0].(*ledger.Exporter)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetExporter indicates an expected call of GetExporter.
+func (mr *SystemControllerMockRecorder) GetExporter(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetExporter", reflect.TypeOf((*SystemController)(nil).GetExporter), ctx, id)
+}
+
// GetLedger mocks base method.
func (m *SystemController) GetLedger(ctx context.Context, name string) (*ledger.Ledger, error) {
m.ctrl.T.Helper()
@@ -100,8 +358,38 @@ func (mr *SystemControllerMockRecorder) GetLedgerController(ctx, name any) *gomo
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLedgerController", reflect.TypeOf((*SystemController)(nil).GetLedgerController), ctx, name)
}
+// GetPipeline mocks base method.
+func (m *SystemController) GetPipeline(ctx context.Context, id string) (*ledger.Pipeline, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetPipeline", ctx, id)
+ ret0, _ := ret[0].(*ledger.Pipeline)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetPipeline indicates an expected call of GetPipeline.
+func (mr *SystemControllerMockRecorder) GetPipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPipeline", reflect.TypeOf((*SystemController)(nil).GetPipeline), ctx, id)
+}
+
+// ListExporters mocks base method.
+func (m *SystemController) ListExporters(ctx context.Context) (*bunpaginate.Cursor[ledger.Exporter], error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ListExporters", ctx)
+ ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Exporter])
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ListExporters indicates an expected call of ListExporters.
+func (mr *SystemControllerMockRecorder) ListExporters(ctx any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListExporters", reflect.TypeOf((*SystemController)(nil).ListExporters), ctx)
+}
+
// ListLedgers mocks base method.
-func (m *SystemController) ListLedgers(ctx context.Context, query common.ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Ledger], error) {
+func (m *SystemController) ListLedgers(ctx context.Context, query common.ColumnPaginatedQuery[system.ListLedgersQueryPayload]) (*bunpaginate.Cursor[ledger.Ledger], error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListLedgers", ctx, query)
ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Ledger])
@@ -115,6 +403,63 @@ func (mr *SystemControllerMockRecorder) ListLedgers(ctx, query any) *gomock.Call
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLedgers", reflect.TypeOf((*SystemController)(nil).ListLedgers), ctx, query)
}
+// ListPipelines mocks base method.
+func (m *SystemController) ListPipelines(ctx context.Context) (*bunpaginate.Cursor[ledger.Pipeline], error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ListPipelines", ctx)
+ ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Pipeline])
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ListPipelines indicates an expected call of ListPipelines.
+func (mr *SystemControllerMockRecorder) ListPipelines(ctx any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPipelines", reflect.TypeOf((*SystemController)(nil).ListPipelines), ctx)
+}
+
+// ResetPipeline mocks base method.
+func (m *SystemController) ResetPipeline(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ResetPipeline", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// ResetPipeline indicates an expected call of ResetPipeline.
+func (mr *SystemControllerMockRecorder) ResetPipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResetPipeline", reflect.TypeOf((*SystemController)(nil).ResetPipeline), ctx, id)
+}
+
+// StartPipeline mocks base method.
+func (m *SystemController) StartPipeline(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "StartPipeline", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// StartPipeline indicates an expected call of StartPipeline.
+func (mr *SystemControllerMockRecorder) StartPipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartPipeline", reflect.TypeOf((*SystemController)(nil).StartPipeline), ctx, id)
+}
+
+// StopPipeline mocks base method.
+func (m *SystemController) StopPipeline(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "StopPipeline", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// StopPipeline indicates an expected call of StopPipeline.
+func (mr *SystemControllerMockRecorder) StopPipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StopPipeline", reflect.TypeOf((*SystemController)(nil).StopPipeline), ctx, id)
+}
+
// UpdateLedgerMetadata mocks base method.
func (m_2 *SystemController) UpdateLedgerMetadata(ctx context.Context, name string, m map[string]string) error {
m_2.ctrl.T.Helper()
diff --git a/internal/api/v2/routes.go b/internal/api/v2/routes.go
index fa1380ccff..b1e00b8c36 100644
--- a/internal/api/v2/routes.go
+++ b/internal/api/v2/routes.go
@@ -7,7 +7,7 @@ import (
nooptracer "go.opentelemetry.io/otel/trace/noop"
"net/http"
- "github.com/formancehq/ledger/internal/controller/system"
+ systemcontroller "github.com/formancehq/ledger/internal/controller/system"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
@@ -18,7 +18,7 @@ import (
)
func NewRouter(
- systemController system.Controller,
+ systemController systemcontroller.Controller,
authenticator auth.Authenticator,
version string,
opts ...RouterOption,
@@ -35,6 +35,16 @@ func NewRouter(
router.Get("/_info", v1.GetInfo(systemController, version))
+ router.Route("/_", func(router chi.Router) {
+ if routerOptions.exporters {
+ router.Route("/exporters", func(router chi.Router) {
+ router.Get("/", listExporters(systemController))
+ router.Get("/{exporterID}", getExporter(systemController))
+ router.Delete("/{exporterID}", deleteExporter(systemController))
+ router.Post("/", createExporter(systemController))
+ })
+ }
+ })
router.Get("/", listLedgers(systemController, routerOptions.paginationConfig))
router.Route("/{ledger}", func(router chi.Router) {
router.Use(func(handler http.Handler) http.Handler {
@@ -58,30 +68,46 @@ func NewRouter(
routerOptions.bulkHandlerFactories,
))
- // LedgerController
router.Get("/_info", getLedgerInfo)
router.Get("/stats", readStats)
- router.Get("/logs", listLogs(routerOptions.paginationConfig))
- router.Post("/logs/import", importLogs)
- router.Post("/logs/export", exportLogs)
-
- // AccountController
- 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(routerOptions.paginationConfig))
- router.Head("/transactions", countTransactions)
+ if routerOptions.exporters {
+ router.Route("/pipelines", func(router chi.Router) {
+ router.Get("/", listPipelines(systemController))
+ router.Post("/", createPipeline(systemController))
+ router.Route("/{pipelineID}", func(router chi.Router) {
+ router.Get("/", readPipeline(systemController))
+ router.Delete("/", deletePipeline(systemController))
+ router.Post("/start", startPipeline(systemController))
+ router.Post("/stop", stopPipeline(systemController))
+ router.Post("/reset", resetPipeline(systemController))
+ })
+ })
+ }
+
+ router.Route("/logs", func(router chi.Router) {
+ router.Get("/", listLogs(routerOptions.paginationConfig))
+ router.Post("/import", importLogs)
+ router.Post("/export", exportLogs)
+ })
- router.Post("/transactions", createTransaction)
+ router.Route("/accounts", func(router chi.Router) {
+ router.Get("/", listAccounts(routerOptions.paginationConfig))
+ router.Head("/", countAccounts)
+ router.Get("/{address}", readAccount)
+ router.Post("/{address}/metadata", addAccountMetadata)
+ router.Delete("/{address}/metadata/{key}", deleteAccountMetadata)
+ })
- router.Get("/transactions/{id}", readTransaction)
- router.Post("/transactions/{id}/revert", revertTransaction)
- router.Post("/transactions/{id}/metadata", addTransactionMetadata)
- router.Delete("/transactions/{id}/metadata/{key}", deleteTransactionMetadata)
+ router.Route("/transactions", func(router chi.Router) {
+ router.Get("/", listTransactions(routerOptions.paginationConfig))
+ router.Head("/", countTransactions)
+ router.Post("/", createTransaction)
+ router.Get("/{id}", readTransaction)
+ router.Post("/{id}/revert", revertTransaction)
+ router.Post("/{id}/metadata", addTransactionMetadata)
+ router.Delete("/{id}/metadata/{key}", deleteTransactionMetadata)
+ })
router.Get("/aggregate/balances", readBalancesAggregated)
@@ -98,6 +124,7 @@ type routerOptions struct {
bulkerFactory bulking.BulkerFactory
bulkHandlerFactories map[string]bulking.HandlerFactory
paginationConfig common.PaginationConfig
+ exporters bool
}
type RouterOption func(ro *routerOptions)
@@ -126,6 +153,12 @@ func WithPaginationConfig(paginationConfig common.PaginationConfig) RouterOption
}
}
+func WithExporters(v bool) RouterOption {
+ return func(ro *routerOptions) {
+ ro.exporters = v
+ }
+}
+
func WithDefaultBulkHandlerFactories(bulkMaxSize int) RouterOption {
return WithBulkHandlerFactories(map[string]bulking.HandlerFactory{
"application/json": bulking.NewJSONBulkHandlerFactory(bulkMaxSize),
diff --git a/internal/bus/listener.go b/internal/bus/listener.go
index 1ed71b16d9..0c77a25b8c 100644
--- a/internal/bus/listener.go
+++ b/internal/bus/listener.go
@@ -27,7 +27,7 @@ func NewLedgerListener(publisher message.Publisher) *LedgerListener {
func (lis *LedgerListener) CommittedTransactions(ctx context.Context, l string, txs ledger.Transaction, accountMetadata ledger.AccountMetadata) {
lis.publish(ctx, events.EventTypeCommittedTransactions,
- newEventCommittedTransactions(CommittedTransactions{
+ events.NewEventCommittedTransactions(events.CommittedTransactions{
Ledger: l,
Transactions: []ledger.Transaction{txs},
AccountMetadata: accountMetadata,
@@ -36,7 +36,7 @@ func (lis *LedgerListener) CommittedTransactions(ctx context.Context, l string,
func (lis *LedgerListener) SavedMetadata(ctx context.Context, l string, targetType, targetID string, metadata metadata.Metadata) {
lis.publish(ctx, events.EventTypeSavedMetadata,
- newEventSavedMetadata(SavedMetadata{
+ events.NewEventSavedMetadata(events.SavedMetadata{
Ledger: l,
TargetType: targetType,
TargetID: targetID,
@@ -46,7 +46,7 @@ func (lis *LedgerListener) SavedMetadata(ctx context.Context, l string, targetTy
func (lis *LedgerListener) RevertedTransaction(ctx context.Context, l string, reverted, revert ledger.Transaction) {
lis.publish(ctx, events.EventTypeRevertedTransaction,
- newEventRevertedTransaction(RevertedTransaction{
+ events.NewEventRevertedTransaction(events.RevertedTransaction{
Ledger: l,
RevertedTransaction: reverted,
RevertTransaction: revert,
@@ -55,7 +55,7 @@ func (lis *LedgerListener) RevertedTransaction(ctx context.Context, l string, re
func (lis *LedgerListener) DeletedMetadata(ctx context.Context, l string, targetType string, targetID any, key string) {
lis.publish(ctx, events.EventTypeDeletedMetadata,
- newEventDeletedMetadata(DeletedMetadata{
+ events.NewEventDeletedMetadata(events.DeletedMetadata{
Ledger: l,
TargetType: targetType,
TargetID: targetID,
diff --git a/internal/controller/ledger/controller.go b/internal/controller/ledger/controller.go
index e11edfbb0f..146240595e 100644
--- a/internal/controller/ledger/controller.go
+++ b/internal/controller/ledger/controller.go
@@ -7,6 +7,7 @@ import (
"github.com/formancehq/go-libs/v3/metadata"
"github.com/formancehq/ledger/internal/machine/vm"
"github.com/formancehq/ledger/internal/storage/common"
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
"github.com/uptrace/bun"
"github.com/formancehq/go-libs/v3/bun/bunpaginate"
@@ -17,6 +18,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 {
+ Info() ledger.Ledger
BeginTX(ctx context.Context, options *sql.TxOptions) (Controller, *bun.Tx, error)
Commit(ctx context.Context) error
Rollback(ctx context.Context) error
@@ -35,8 +37,8 @@ type Controller interface {
CountTransactions(ctx context.Context, query common.ResourceQuery[any]) (int, error)
ListTransactions(ctx context.Context, query common.ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Transaction], error)
GetTransaction(ctx context.Context, query common.ResourceQuery[any]) (*ledger.Transaction, error)
- GetVolumesWithBalances(ctx context.Context, q common.OffsetPaginatedQuery[GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error)
- GetAggregatedBalances(ctx context.Context, q common.ResourceQuery[GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error)
+ GetVolumesWithBalances(ctx context.Context, q common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error)
+ GetAggregatedBalances(ctx context.Context, q common.ResourceQuery[ledgerstore.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 19f4144988..3aea3dfcc5 100644
--- a/internal/controller/ledger/controller_default.go
+++ b/internal/controller/ledger/controller_default.go
@@ -4,11 +4,11 @@ import (
"context"
"database/sql"
"fmt"
+ "github.com/formancehq/ledger/internal/storage/common"
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
"math/big"
"reflect"
- "github.com/formancehq/ledger/internal/storage/common"
-
"github.com/formancehq/go-libs/v3/pointer"
"github.com/formancehq/go-libs/v3/time"
"github.com/formancehq/ledger/pkg/features"
@@ -57,6 +57,10 @@ type DefaultController struct {
deleteAccountMetadataLp *logProcessor[DeleteAccountMetadata, ledger.DeletedMetadata]
}
+func (ctrl *DefaultController) Info() ledger.Ledger {
+ return ctrl.ledger
+}
+
func (ctrl *DefaultController) BeginTX(ctx context.Context, options *sql.TxOptions) (Controller, *bun.Tx, error) {
cp := *ctrl
var (
@@ -166,7 +170,7 @@ func (ctrl *DefaultController) GetAccount(ctx context.Context, q common.Resource
return ctrl.store.Accounts().GetOne(ctx, q)
}
-func (ctrl *DefaultController) GetAggregatedBalances(ctx context.Context, q common.ResourceQuery[GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) {
+func (ctrl *DefaultController) GetAggregatedBalances(ctx context.Context, q common.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) {
ret, err := ctrl.store.AggregatedBalances().GetOne(ctx, q)
if err != nil {
return nil, err
@@ -178,7 +182,7 @@ func (ctrl *DefaultController) ListLogs(ctx context.Context, q common.ColumnPagi
return ctrl.store.Logs().Paginate(ctx, q)
}
-func (ctrl *DefaultController) GetVolumesWithBalances(ctx context.Context, q common.OffsetPaginatedQuery[GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) {
+func (ctrl *DefaultController) GetVolumesWithBalances(ctx context.Context, q common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) {
return ctrl.store.Volumes().Paginate(ctx, q)
}
@@ -216,7 +220,7 @@ func (ctrl *DefaultController) Import(ctx context.Context, stream chan ledger.Lo
if err := ctrl.importLog(ctx, store, log); err != nil {
switch {
case errors.Is(err, postgres.ErrSerialization) ||
- errors.Is(err, ErrConcurrentTransaction{}):
+ errors.Is(err, ledgerstore.ErrConcurrentTransaction{}):
return NewErrImport(errors.New("concurrent transaction occur" +
"red, cannot import the ledger"))
}
diff --git a/internal/controller/ledger/controller_default_test.go b/internal/controller/ledger/controller_default_test.go
index c86f603332..decf185b36 100644
--- a/internal/controller/ledger/controller_default_test.go
+++ b/internal/controller/ledger/controller_default_test.go
@@ -7,6 +7,7 @@ import (
"github.com/formancehq/go-libs/v3/query"
"github.com/formancehq/ledger/internal/storage/common"
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
"github.com/uptrace/bun"
"github.com/formancehq/go-libs/v3/pointer"
@@ -379,14 +380,14 @@ func TestGetAggregatedBalances(t *testing.T) {
machineParser := NewMockNumscriptParser(ctrl)
interpreterParser := NewMockNumscriptParser(ctrl)
ctx := logging.TestingContext()
- aggregatedBalances := NewMockResource[ledger.AggregatedVolumes, GetAggregatedVolumesOptions](ctrl)
+ aggregatedBalances := NewMockResource[ledger.AggregatedVolumes, ledgerstore.GetAggregatedVolumesOptions](ctrl)
store.EXPECT().AggregatedBalances().Return(aggregatedBalances)
- aggregatedBalances.EXPECT().GetOne(gomock.Any(), common.ResourceQuery[GetAggregatedVolumesOptions]{}).
+ aggregatedBalances.EXPECT().GetOne(gomock.Any(), common.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{}).
Return(&ledger.AggregatedVolumes{}, nil)
l := NewDefaultController(ledger.Ledger{}, store, parser, machineParser, interpreterParser)
- ret, err := l.GetAggregatedBalances(ctx, common.ResourceQuery[GetAggregatedVolumesOptions]{})
+ ret, err := l.GetAggregatedBalances(ctx, common.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{})
require.NoError(t, err)
require.Equal(t, ledger.BalancesByAssets{}, ret)
}
@@ -429,17 +430,17 @@ func TestGetVolumesWithBalances(t *testing.T) {
machineParser := NewMockNumscriptParser(ctrl)
interpreterParser := NewMockNumscriptParser(ctrl)
ctx := logging.TestingContext()
- volumes := NewMockPaginatedResource[ledger.VolumesWithBalanceByAssetByAccount, GetVolumesOptions, common.OffsetPaginatedQuery[GetVolumesOptions]](ctrl)
+ volumes := NewMockPaginatedResource[ledger.VolumesWithBalanceByAssetByAccount, ledgerstore.GetVolumesOptions, common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]](ctrl)
balancesByAssets := &bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount]{}
store.EXPECT().Volumes().Return(volumes)
- volumes.EXPECT().Paginate(gomock.Any(), common.OffsetPaginatedQuery[GetVolumesOptions]{
+ volumes.EXPECT().Paginate(gomock.Any(), common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
PageSize: bunpaginate.QueryDefaultPageSize,
Order: pointer.For(bunpaginate.Order(bunpaginate.OrderAsc)),
}).Return(balancesByAssets, nil)
l := NewDefaultController(ledger.Ledger{}, store, parser, machineParser, interpreterParser)
- ret, err := l.GetVolumesWithBalances(ctx, common.OffsetPaginatedQuery[GetVolumesOptions]{
+ ret, err := l.GetVolumesWithBalances(ctx, common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
PageSize: bunpaginate.QueryDefaultPageSize,
Order: pointer.For(bunpaginate.Order(bunpaginate.OrderAsc)),
})
diff --git a/internal/controller/ledger/controller_generated_test.go b/internal/controller/ledger/controller_generated_test.go
index 823b27998b..ac33066cdf 100644
--- a/internal/controller/ledger/controller_generated_test.go
+++ b/internal/controller/ledger/controller_generated_test.go
@@ -16,6 +16,7 @@ import (
migrations "github.com/formancehq/go-libs/v3/migrations"
ledger "github.com/formancehq/ledger/internal"
common "github.com/formancehq/ledger/internal/storage/common"
+ ledger0 "github.com/formancehq/ledger/internal/storage/ledger"
bun "github.com/uptrace/bun"
gomock "go.uber.org/mock/gomock"
)
@@ -180,7 +181,7 @@ func (mr *MockControllerMockRecorder) GetAccount(ctx, query any) *gomock.Call {
}
// GetAggregatedBalances mocks base method.
-func (m *MockController) GetAggregatedBalances(ctx context.Context, q common.ResourceQuery[GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) {
+func (m *MockController) GetAggregatedBalances(ctx context.Context, q common.ResourceQuery[ledger0.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAggregatedBalances", ctx, q)
ret0, _ := ret[0].(ledger.BalancesByAssets)
@@ -240,7 +241,7 @@ func (mr *MockControllerMockRecorder) GetTransaction(ctx, query any) *gomock.Cal
}
// GetVolumesWithBalances mocks base method.
-func (m *MockController) GetVolumesWithBalances(ctx context.Context, q common.OffsetPaginatedQuery[GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) {
+func (m *MockController) GetVolumesWithBalances(ctx context.Context, q common.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])
@@ -268,6 +269,20 @@ func (mr *MockControllerMockRecorder) Import(ctx, stream any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Import", reflect.TypeOf((*MockController)(nil).Import), ctx, stream)
}
+// Info mocks base method.
+func (m *MockController) Info() ledger.Ledger {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Info")
+ ret0, _ := ret[0].(ledger.Ledger)
+ return ret0
+}
+
+// Info indicates an expected call of Info.
+func (mr *MockControllerMockRecorder) Info() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockController)(nil).Info))
+}
+
// IsDatabaseUpToDate mocks base method.
func (m *MockController) IsDatabaseUpToDate(ctx context.Context) (bool, error) {
m.ctrl.T.Helper()
diff --git a/internal/controller/ledger/controller_with_traces.go b/internal/controller/ledger/controller_with_traces.go
index 42d2b6549a..3dcc4034d1 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/v3/migrations"
"github.com/formancehq/ledger/internal/storage/common"
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
"github.com/formancehq/ledger/internal/tracing"
"github.com/uptrace/bun"
"go.opentelemetry.io/otel/metric"
@@ -43,6 +44,10 @@ type ControllerWithTraces struct {
lockLedgerHistogram metric.Int64Histogram
}
+func (c *ControllerWithTraces) Info() ledger.Ledger {
+ return c.underlying.Info()
+}
+
func NewControllerWithTraces(underlying Controller, tracer trace.Tracer, meter metric.Meter) *ControllerWithTraces {
ret := &ControllerWithTraces{
underlying: underlying,
@@ -283,7 +288,7 @@ func (c *ControllerWithTraces) GetAccount(ctx context.Context, q common.Resource
)
}
-func (c *ControllerWithTraces) GetAggregatedBalances(ctx context.Context, q common.ResourceQuery[GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) {
+func (c *ControllerWithTraces) GetAggregatedBalances(ctx context.Context, q common.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]) (ledger.BalancesByAssets, error) {
return tracing.TraceWithMetric(
ctx,
"GetAggregatedBalances",
@@ -343,7 +348,7 @@ func (c *ControllerWithTraces) IsDatabaseUpToDate(ctx context.Context) (bool, er
)
}
-func (c *ControllerWithTraces) GetVolumesWithBalances(ctx context.Context, q common.OffsetPaginatedQuery[GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) {
+func (c *ControllerWithTraces) GetVolumesWithBalances(ctx context.Context, q common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]) (*bunpaginate.Cursor[ledger.VolumesWithBalanceByAssetByAccount], error) {
return tracing.TraceWithMetric(
ctx,
"GetVolumesWithBalances",
diff --git a/internal/controller/ledger/errors.go b/internal/controller/ledger/errors.go
index 988528b11e..5a58d0e9b0 100644
--- a/internal/controller/ledger/errors.go
+++ b/internal/controller/ledger/errors.go
@@ -15,6 +15,23 @@ var ErrNotFound = postgres.ErrNotFound
type ErrTooManyClient = postgres.ErrTooManyClient
+// ErrInUsePipeline denotes a pipeline which is actually used
+// The client has to retry later if still relevant
+type ErrInUsePipeline string
+
+func (e ErrInUsePipeline) Error() string {
+ return fmt.Sprintf("pipeline '%s' already in use", string(e))
+}
+
+func (e ErrInUsePipeline) Is(err error) bool {
+ _, ok := err.(ErrInUsePipeline)
+ return ok
+}
+
+func NewErrInUsePipeline(id string) ErrInUsePipeline {
+ return ErrInUsePipeline(id)
+}
+
type ErrImport struct {
err error
}
@@ -90,25 +107,6 @@ func newErrAlreadyReverted(id uint64) ErrAlreadyReverted {
}
}
-type ErrMissingFeature struct {
- feature string
-}
-
-func (e ErrMissingFeature) Error() string {
- return fmt.Sprintf("missing feature %q", e.feature)
-}
-
-func (e ErrMissingFeature) Is(err error) bool {
- _, ok := err.(ErrMissingFeature)
- return ok
-}
-
-func NewErrMissingFeature(feature string) ErrMissingFeature {
- return ErrMissingFeature{
- feature: feature,
- }
-}
-
type ErrIdempotencyKeyConflict struct {
ik string
}
@@ -246,24 +244,4 @@ func newErrInvalidIdempotencyInputs(idempotencyKey, expectedIdempotencyHash, got
expectedIdempotencyHash: expectedIdempotencyHash,
computedIdempotencyHash: gotIdempotencyHash,
}
-}
-
-// ErrConcurrentTransaction can be raised in case of conflicting between an import and a single transaction
-type ErrConcurrentTransaction struct {
- id uint64
-}
-
-func (e ErrConcurrentTransaction) Error() string {
- return fmt.Sprintf("duplicate id insertion %d", e.id)
-}
-
-func (e ErrConcurrentTransaction) Is(err error) bool {
- _, ok := err.(ErrConcurrentTransaction)
- return ok
-}
-
-func NewErrConcurrentTransaction(id uint64) ErrConcurrentTransaction {
- return ErrConcurrentTransaction{
- id: id,
- }
-}
+}
\ No newline at end of file
diff --git a/internal/controller/ledger/log_process.go b/internal/controller/ledger/log_process.go
index baeabfa3a6..f1e13babb7 100644
--- a/internal/controller/ledger/log_process.go
+++ b/internal/controller/ledger/log_process.go
@@ -8,6 +8,7 @@ import (
"github.com/formancehq/go-libs/v3/platform/postgres"
"github.com/formancehq/go-libs/v3/pointer"
ledger "github.com/formancehq/ledger/internal"
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/trace"
@@ -110,7 +111,7 @@ 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{}):
+ case errors.Is(err, ledgerstore.ErrIdempotencyKeyConflict{}):
log, output, err := lp.fetchLogWithIK(ctx, store, parameters)
if err != nil {
return nil, nil, err
diff --git a/internal/controller/ledger/log_process_test.go b/internal/controller/ledger/log_process_test.go
index 8ed5108734..abbea333fa 100644
--- a/internal/controller/ledger/log_process_test.go
+++ b/internal/controller/ledger/log_process_test.go
@@ -6,6 +6,7 @@ import (
"github.com/formancehq/go-libs/v3/platform/postgres"
"github.com/formancehq/go-libs/v3/pointer"
ledger "github.com/formancehq/ledger/internal"
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
"github.com/stretchr/testify/require"
"github.com/uptrace/bun"
"go.opentelemetry.io/otel/metric/noop"
@@ -42,7 +43,7 @@ func TestForgeLogWithIKConflict(t *testing.T) {
_, _, err := lp.forgeLog(ctx, store, Parameters[RunScript]{
IdempotencyKey: "foo",
}, func(ctx context.Context, store Store, parameters Parameters[RunScript]) (*ledger.CreatedTransaction, error) {
- return nil, NewErrIdempotencyKeyConflict("foo")
+ return nil, ledgerstore.NewErrIdempotencyKeyConflict("foo")
})
require.NoError(t, err)
}
diff --git a/internal/controller/ledger/store.go b/internal/controller/ledger/store.go
index c7a061c05c..b6b641d33b 100644
--- a/internal/controller/ledger/store.go
+++ b/internal/controller/ledger/store.go
@@ -3,9 +3,9 @@ package ledger
import (
"context"
"database/sql"
- "github.com/formancehq/go-libs/v3/bun/bunpaginate"
- "github.com/formancehq/go-libs/v3/pointer"
"github.com/formancehq/ledger/internal/storage/common"
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
+ "github.com/uptrace/bun"
"math/big"
"github.com/formancehq/go-libs/v3/migrations"
@@ -16,7 +16,6 @@ import (
"github.com/formancehq/go-libs/v3/time"
ledger "github.com/formancehq/ledger/internal"
"github.com/formancehq/ledger/internal/machine/vm"
- "github.com/uptrace/bun"
)
type Balance struct {
@@ -24,9 +23,6 @@ type Balance struct {
Balance *big.Int
}
-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 . Store
type Store interface {
BeginTX(ctx context.Context, options *sql.TxOptions) (Store, *bun.Tx, error)
@@ -34,7 +30,7 @@ type Store interface {
Rollback() error
// GetBalances must returns balance and lock account until the end of the TX
- GetBalances(ctx context.Context, query BalanceQuery) (Balances, error)
+ GetBalances(ctx context.Context, query ledgerstore.BalanceQuery) (ledger.Balances, error)
CommitTransaction(ctx context.Context, transaction *ledger.Transaction, accountMetadata map[string]metadata.Metadata) error
// RevertTransaction revert the transaction with identifier id
// It returns :
@@ -52,7 +48,6 @@ type Store interface {
LockLedger(ctx context.Context) (Store, bun.IDB, func() error, error)
- GetDB() bun.IDB
ReadLogWithIdempotencyKey(ctx context.Context, ik string) (*ledger.Log, error)
IsUpToDate(ctx context.Context) (bool, error)
@@ -61,14 +56,18 @@ type Store interface {
Accounts() common.PaginatedResource[ledger.Account, any, common.OffsetPaginatedQuery[any]]
Logs() common.PaginatedResource[ledger.Log, any, common.ColumnPaginatedQuery[any]]
Transactions() common.PaginatedResource[ledger.Transaction, any, common.ColumnPaginatedQuery[any]]
- AggregatedBalances() common.Resource[ledger.AggregatedVolumes, GetAggregatedVolumesOptions]
- Volumes() common.PaginatedResource[ledger.VolumesWithBalanceByAssetByAccount, GetVolumesOptions, common.OffsetPaginatedQuery[GetVolumesOptions]]
+ AggregatedBalances() common.Resource[ledger.AggregatedVolumes, ledgerstore.GetAggregatedVolumesOptions]
+ Volumes() common.PaginatedResource[ledger.VolumesWithBalanceByAssetByAccount, ledgerstore.GetVolumesOptions, common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]]
}
type vmStoreAdapter struct {
Store
}
+func (v *vmStoreAdapter) GetBalances(ctx context.Context, query vm.BalanceQuery) (vm.Balances, error) {
+ return v.Store.GetBalances(ctx, query)
+}
+
func (v *vmStoreAdapter) GetAccount(ctx context.Context, address string) (*ledger.Account, error) {
account, err := v.Store.Accounts().GetOne(ctx, common.ResourceQuery[any]{
Builder: query.Match("address", address),
@@ -87,17 +86,6 @@ func newVmStoreAdapter(tx Store) *vmStoreAdapter {
}
}
-func NewListLedgersQuery(pageSize uint64) common.ColumnPaginatedQuery[any] {
- return common.ColumnPaginatedQuery[any]{
- PageSize: pageSize,
- Column: "id",
- Order: (*bunpaginate.Order)(pointer.For(bunpaginate.OrderAsc)),
- Options: common.ResourceQuery[any]{
- Expand: make([]string, 0),
- },
- }
-}
-
// numscript rewrite implementation
var _ numscript.Store = (*numscriptRewriteAdapter)(nil)
@@ -113,12 +101,12 @@ type numscriptRewriteAdapter struct {
}
func (s *numscriptRewriteAdapter) GetBalances(ctx context.Context, q numscript.BalanceQuery) (numscript.Balances, error) {
- vmBalances, err := s.Store.GetBalances(ctx, BalanceQuery(q))
+ vmBalances, err := s.Store.GetBalances(ctx, q)
if err != nil {
return nil, err
}
- return numscript.Balances(vmBalances), nil
+ return vmBalances, nil
}
func (s *numscriptRewriteAdapter) GetAccountsMetadata(ctx context.Context, q numscript.MetadataQuery) (numscript.AccountsMetadata, error) {
@@ -137,12 +125,3 @@ 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 46e05ffa0e..a70bdf3c12 100644
--- a/internal/controller/ledger/store_generated_test.go
+++ b/internal/controller/ledger/store_generated_test.go
@@ -17,6 +17,7 @@ import (
time "github.com/formancehq/go-libs/v3/time"
ledger "github.com/formancehq/ledger/internal"
common "github.com/formancehq/ledger/internal/storage/common"
+ ledger0 "github.com/formancehq/ledger/internal/storage/ledger"
bun "github.com/uptrace/bun"
gomock "go.uber.org/mock/gomock"
)
@@ -60,10 +61,10 @@ func (mr *MockStoreMockRecorder) Accounts() *gomock.Call {
}
// AggregatedBalances mocks base method.
-func (m *MockStore) AggregatedBalances() common.Resource[ledger.AggregatedVolumes, GetAggregatedVolumesOptions] {
+func (m *MockStore) AggregatedBalances() common.Resource[ledger.AggregatedVolumes, ledger0.GetAggregatedVolumesOptions] {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AggregatedBalances")
- ret0, _ := ret[0].(common.Resource[ledger.AggregatedVolumes, GetAggregatedVolumesOptions])
+ ret0, _ := ret[0].(common.Resource[ledger.AggregatedVolumes, ledger0.GetAggregatedVolumesOptions])
return ret0
}
@@ -148,10 +149,10 @@ func (mr *MockStoreMockRecorder) DeleteTransactionMetadata(ctx, transactionID, k
}
// GetBalances mocks base method.
-func (m *MockStore) GetBalances(ctx context.Context, query BalanceQuery) (Balances, error) {
+func (m *MockStore) GetBalances(ctx context.Context, query ledger0.BalanceQuery) (ledger.Balances, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetBalances", ctx, query)
- ret0, _ := ret[0].(Balances)
+ ret0, _ := ret[0].(ledger.Balances)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@@ -162,20 +163,6 @@ func (mr *MockStoreMockRecorder) GetBalances(ctx, query any) *gomock.Call {
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()
- ret := m.ctrl.Call(m, "GetDB")
- ret0, _ := ret[0].(bun.IDB)
- return ret0
-}
-
-// GetDB indicates an expected call of GetDB.
-func (mr *MockStoreMockRecorder) GetDB() *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDB", reflect.TypeOf((*MockStore)(nil).GetDB))
-}
-
// GetMigrationsInfo mocks base method.
func (m *MockStore) GetMigrationsInfo(ctx context.Context) ([]migrations.Info, error) {
m.ctrl.T.Helper()
@@ -360,10 +347,10 @@ func (mr *MockStoreMockRecorder) UpsertAccounts(ctx any, accounts ...any) *gomoc
}
// Volumes mocks base method.
-func (m *MockStore) Volumes() common.PaginatedResource[ledger.VolumesWithBalanceByAssetByAccount, GetVolumesOptions, common.OffsetPaginatedQuery[GetVolumesOptions]] {
+func (m *MockStore) Volumes() common.PaginatedResource[ledger.VolumesWithBalanceByAssetByAccount, ledger0.GetVolumesOptions, common.OffsetPaginatedQuery[ledger0.GetVolumesOptions]] {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Volumes")
- ret0, _ := ret[0].(common.PaginatedResource[ledger.VolumesWithBalanceByAssetByAccount, GetVolumesOptions, common.OffsetPaginatedQuery[GetVolumesOptions]])
+ ret0, _ := ret[0].(common.PaginatedResource[ledger.VolumesWithBalanceByAssetByAccount, ledger0.GetVolumesOptions, common.OffsetPaginatedQuery[ledger0.GetVolumesOptions]])
return ret0
}
diff --git a/internal/controller/system/adapters.go b/internal/controller/system/adapters.go
new file mode 100644
index 0000000000..a070344507
--- /dev/null
+++ b/internal/controller/system/adapters.go
@@ -0,0 +1,94 @@
+package system
+
+import (
+ "context"
+ "database/sql"
+ ledger "github.com/formancehq/ledger/internal"
+ ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
+ "github.com/formancehq/ledger/internal/storage/common"
+ "github.com/formancehq/ledger/internal/storage/driver"
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
+ "github.com/uptrace/bun"
+)
+
+type DefaultStorageDriverAdapter struct {
+ *driver.Driver
+ store Store
+}
+
+func (d *DefaultStorageDriverAdapter) OpenLedger(ctx context.Context, name string) (ledgercontroller.Store, *ledger.Ledger, error) {
+ store, l, err := d.Driver.OpenLedger(ctx, name)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return NewDefaultStoreAdapter(store), l, nil
+}
+
+func (d *DefaultStorageDriverAdapter) CreateLedger(ctx context.Context, l *ledger.Ledger) error {
+ _, err := d.Driver.CreateLedger(ctx, l)
+ return err
+}
+
+func (d *DefaultStorageDriverAdapter) GetSystemStore() Store {
+ return d.store
+}
+
+func NewControllerStorageDriverAdapter(d *driver.Driver, systemStore Store) *DefaultStorageDriverAdapter {
+ return &DefaultStorageDriverAdapter{
+ Driver: d,
+ store: systemStore,
+ }
+}
+
+var _ Driver = (*DefaultStorageDriverAdapter)(nil)
+
+type DefaultStoreAdapter struct {
+ *ledgerstore.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, *bun.Tx, error) {
+ store, tx, err := d.Store.BeginTX(ctx, opts)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return &DefaultStoreAdapter{
+ Store: store,
+ }, tx, nil
+}
+
+func (d *DefaultStoreAdapter) Commit() error {
+ return d.Store.Commit()
+}
+
+func (d *DefaultStoreAdapter) Rollback() error {
+ return d.Store.Rollback()
+}
+
+func (d *DefaultStoreAdapter) AggregatedBalances() common.Resource[ledger.AggregatedVolumes, ledgerstore.GetAggregatedVolumesOptions] {
+ return d.AggregatedVolumes()
+}
+
+func (d *DefaultStoreAdapter) LockLedger(ctx context.Context) (ledgercontroller.Store, bun.IDB, func() error, error) {
+ lockLedger, conn, release, err := d.Store.LockLedger(ctx)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+
+ return &DefaultStoreAdapter{
+ Store: lockLedger,
+ }, conn, release, nil
+}
+
+func NewDefaultStoreAdapter(store *ledgerstore.Store) *DefaultStoreAdapter {
+ return &DefaultStoreAdapter{
+ Store: store,
+ }
+}
+
+var _ ledgercontroller.Store = (*DefaultStoreAdapter)(nil)
diff --git a/internal/controller/system/controller.go b/internal/controller/system/controller.go
index 169e587b66..a2e8341df1 100644
--- a/internal/controller/system/controller.go
+++ b/internal/controller/system/controller.go
@@ -2,10 +2,12 @@ package system
import (
"context"
+ "fmt"
"reflect"
"time"
"github.com/formancehq/ledger/internal/storage/common"
+ systemstore "github.com/formancehq/ledger/internal/storage/system"
"github.com/formancehq/ledger/pkg/features"
"go.opentelemetry.io/otel/attribute"
@@ -21,10 +23,26 @@ import (
ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
)
+type ReplicationBackend interface {
+ ListExporters(ctx context.Context) (*bunpaginate.Cursor[ledger.Exporter], error)
+ CreateExporter(ctx context.Context, configuration ledger.ExporterConfiguration) (*ledger.Exporter, error)
+ DeleteExporter(ctx context.Context, id string) error
+ GetExporter(ctx context.Context, id string) (*ledger.Exporter, error)
+
+ ListPipelines(ctx context.Context) (*bunpaginate.Cursor[ledger.Pipeline], error)
+ GetPipeline(ctx context.Context, id string) (*ledger.Pipeline, error)
+ CreatePipeline(ctx context.Context, pipelineConfiguration ledger.PipelineConfiguration) (*ledger.Pipeline, error)
+ DeletePipeline(ctx context.Context, id string) error
+ StartPipeline(ctx context.Context, id string) error
+ ResetPipeline(ctx context.Context, id string) error
+ StopPipeline(ctx context.Context, id string) error
+}
+
type Controller interface {
+ ReplicationBackend
GetLedgerController(ctx context.Context, name string) (ledgercontroller.Controller, error)
GetLedger(ctx context.Context, name string) (*ledger.Ledger, error)
- ListLedgers(ctx context.Context, query common.ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Ledger], error)
+ ListLedgers(ctx context.Context, query common.ColumnPaginatedQuery[systemstore.ListLedgersQueryPayload]) (*bunpaginate.Cursor[ledger.Ledger], error)
// CreateLedger can return following errors:
// * ErrLedgerAlreadyExists
// * ledger.ErrInvalidLedgerName
@@ -35,7 +53,7 @@ type Controller interface {
}
type DefaultController struct {
- store Store
+ driver Driver
listener ledgercontroller.Listener
// The numscript runtime used by default
defaultParser ledgercontroller.NumscriptParser
@@ -45,15 +63,70 @@ type DefaultController struct {
interpreterParser ledgercontroller.NumscriptParser
registry *ledgercontroller.StateRegistry
databaseRetryConfiguration DatabaseRetryConfiguration
+ replicationBackend ReplicationBackend
tracerProvider trace.TracerProvider
meterProvider metric.MeterProvider
enableFeatures bool
}
+func (ctrl *DefaultController) ListExporters(ctx context.Context) (*bunpaginate.Cursor[ledger.Exporter], error) {
+ return ctrl.replicationBackend.ListExporters(ctx)
+}
+
+// CreateExporter can return following errors:
+// * ErrInvalidDriverConfiguration
+func (ctrl *DefaultController) CreateExporter(ctx context.Context, configuration ledger.ExporterConfiguration) (*ledger.Exporter, error) {
+ ret, err := ctrl.replicationBackend.CreateExporter(ctx, configuration)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create exporter: %w", err)
+ }
+ return ret, nil
+}
+
+// DeleteExporter can return following errors:
+// ErrExporterNotFound
+func (ctrl *DefaultController) DeleteExporter(ctx context.Context, id string) error {
+ return ctrl.replicationBackend.DeleteExporter(ctx, id)
+}
+
+// GetExporter can return following errors:
+// ErrExporterNotFound
+func (ctrl *DefaultController) GetExporter(ctx context.Context, id string) (*ledger.Exporter, error) {
+ return ctrl.replicationBackend.GetExporter(ctx, id)
+}
+
+func (ctrl *DefaultController) ListPipelines(ctx context.Context) (*bunpaginate.Cursor[ledger.Pipeline], error) {
+ return ctrl.replicationBackend.ListPipelines(ctx)
+}
+
+func (ctrl *DefaultController) GetPipeline(ctx context.Context, id string) (*ledger.Pipeline, error) {
+ return ctrl.replicationBackend.GetPipeline(ctx, id)
+}
+
+func (ctrl *DefaultController) CreatePipeline(ctx context.Context, pipelineConfiguration ledger.PipelineConfiguration) (*ledger.Pipeline, error) {
+ return ctrl.replicationBackend.CreatePipeline(ctx, pipelineConfiguration)
+}
+
+func (ctrl *DefaultController) DeletePipeline(ctx context.Context, id string) error {
+ return ctrl.replicationBackend.DeletePipeline(ctx, id)
+}
+
+func (ctrl *DefaultController) StartPipeline(ctx context.Context, id string) error {
+ return ctrl.replicationBackend.StartPipeline(ctx, id)
+}
+
+func (ctrl *DefaultController) ResetPipeline(ctx context.Context, id string) error {
+ return ctrl.replicationBackend.ResetPipeline(ctx, id)
+}
+
+func (ctrl *DefaultController) StopPipeline(ctx context.Context, id string) error {
+ return ctrl.replicationBackend.StopPipeline(ctx, id)
+}
+
func (ctrl *DefaultController) GetLedgerController(ctx context.Context, name string) (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)
+ store, l, err := ctrl.driver.OpenLedger(ctx, name)
if err != nil {
return nil, err
}
@@ -121,40 +194,46 @@ func (ctrl *DefaultController) CreateLedger(ctx context.Context, name string, co
return newErrInvalidLedgerConfiguration(err)
}
- return ctrl.store.CreateLedger(ctx, l)
+ return ctrl.driver.CreateLedger(ctx, l)
})))
}
func (ctrl *DefaultController) GetLedger(ctx context.Context, name string) (*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)
+ return ctrl.driver.GetSystemStore().GetLedger(ctx, name)
})
}
-func (ctrl *DefaultController) ListLedgers(ctx context.Context, query common.ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Ledger], error) {
+func (ctrl *DefaultController) ListLedgers(ctx context.Context, query common.ColumnPaginatedQuery[systemstore.ListLedgersQueryPayload]) (*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)
+ return ctrl.driver.GetSystemStore().Ledgers().Paginate(ctx, query)
})
}
func (ctrl *DefaultController) UpdateLedgerMetadata(ctx context.Context, name string, m map[string]string) 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)
+ return ctrl.driver.GetSystemStore().UpdateLedgerMetadata(ctx, name, m)
})))
}
func (ctrl *DefaultController) DeleteLedgerMetadata(ctx context.Context, param string, key string) 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)
+ return ctrl.driver.GetSystemStore().DeleteLedgerMetadata(ctx, param, key)
})))
}
-func NewDefaultController(store Store, listener ledgercontroller.Listener, opts ...Option) *DefaultController {
+func NewDefaultController(
+ store Driver,
+ listener ledgercontroller.Listener,
+ replicationBackend ReplicationBackend,
+ opts ...Option,
+) *DefaultController {
ret := &DefaultController{
- store: store,
- listener: listener,
- registry: ledgercontroller.NewStateRegistry(),
- defaultParser: ledgercontroller.NewDefaultNumscriptParser(),
+ driver: store,
+ listener: listener,
+ registry: ledgercontroller.NewStateRegistry(),
+ defaultParser: ledgercontroller.NewDefaultNumscriptParser(),
+ replicationBackend: replicationBackend,
}
for _, opt := range append(defaultOptions, opts...) {
opt(ret)
diff --git a/internal/controller/system/errors.go b/internal/controller/system/errors.go
index fb503d902e..858648aa66 100644
--- a/internal/controller/system/errors.go
+++ b/internal/controller/system/errors.go
@@ -3,11 +3,13 @@ package system
import (
"errors"
"fmt"
+ "github.com/formancehq/ledger/internal/storage/driver"
+ systemstore "github.com/formancehq/ledger/internal/storage/system"
)
var (
- ErrLedgerAlreadyExists = errors.New("ledger already exists")
- ErrBucketOutdated = errors.New("bucket is outdated, you need to upgrade it before adding a new ledger")
+ ErrLedgerAlreadyExists = systemstore.ErrLedgerAlreadyExists
+ ErrBucketOutdated = driver.ErrBucketOutdated
ErrExperimentalFeaturesDisabled = errors.New("experimental features are disabled")
)
@@ -29,3 +31,59 @@ func newErrInvalidLedgerConfiguration(err error) ErrInvalidLedgerConfiguration {
err: err,
}
}
+
+// ErrExporterNotFound denotes an attempt to use a not found exporter
+type ErrExporterNotFound string
+
+func (e ErrExporterNotFound) Error() string {
+ return fmt.Sprintf("exporter '%s' not found", string(e))
+}
+
+func (e ErrExporterNotFound) Is(err error) bool {
+ _, ok := err.(ErrExporterNotFound)
+ return ok
+}
+
+func NewErrExporterNotFound(exporterID string) ErrExporterNotFound {
+ return ErrExporterNotFound(exporterID)
+}
+
+type ErrInvalidDriverConfiguration struct {
+ name string
+ err error
+}
+
+func (e ErrInvalidDriverConfiguration) Error() string {
+ return fmt.Sprintf("driver '%s' invalid: %s", e.name, e.err)
+}
+
+func (e ErrInvalidDriverConfiguration) Is(err error) bool {
+ _, ok := err.(ErrInvalidDriverConfiguration)
+ return ok
+}
+
+func (e ErrInvalidDriverConfiguration) Unwrap() error {
+ return e.err
+}
+
+func NewErrInvalidDriverConfiguration(name string, err error) ErrInvalidDriverConfiguration {
+ return ErrInvalidDriverConfiguration{
+ name: name,
+ err: err,
+ }
+}
+
+type ErrExporterUsed string
+
+func (e ErrExporterUsed) Error() string {
+ return fmt.Sprintf("exporter '%s' actually used by an existing pipeline", string(e))
+}
+
+func (e ErrExporterUsed) Is(err error) bool {
+ _, ok := err.(ErrExporterUsed)
+ return ok
+}
+
+func NewErrExporterUsed(id string) ErrExporterUsed {
+ return ErrExporterUsed(id)
+}
diff --git a/internal/controller/system/module.go b/internal/controller/system/module.go
index f61c8678a2..95e937cecc 100644
--- a/internal/controller/system/module.go
+++ b/internal/controller/system/module.go
@@ -1,6 +1,7 @@
package system
import (
+ systemstore "github.com/formancehq/ledger/internal/storage/system"
"time"
ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
@@ -28,11 +29,16 @@ func NewFXModule(configuration ModuleConfiguration) fx.Option {
fx.Provide(func(controller *DefaultController) Controller {
return controller
}),
+ fx.Provide(func(store *systemstore.DefaultStore) Store {
+ return store
+ }),
+ fx.Provide(fx.Annotate(NewControllerStorageDriverAdapter, fx.As(new(Driver)))),
fx.Provide(func(
- store Store,
+ driver Driver,
listener ledgercontroller.Listener,
meterProvider metric.MeterProvider,
tracerProvider trace.TracerProvider,
+ replicationBackend ReplicationBackend,
) *DefaultController {
var (
machineParser ledgercontroller.NumscriptParser = ledgercontroller.NewDefaultNumscriptParser()
@@ -54,8 +60,9 @@ func NewFXModule(configuration ModuleConfiguration) fx.Option {
}
return NewDefaultController(
- store,
+ driver,
listener,
+ replicationBackend,
WithParser(parser, machineParser, interpreterParser),
WithDatabaseRetryConfiguration(configuration.DatabaseRetryConfiguration),
WithMeterProvider(meterProvider),
diff --git a/internal/controller/system/store.go b/internal/controller/system/store.go
index f3cd1181fd..cbb2dd22a2 100644
--- a/internal/controller/system/store.go
+++ b/internal/controller/system/store.go
@@ -3,19 +3,23 @@ package system
import (
"context"
"github.com/formancehq/ledger/internal/storage/common"
+ systemstore "github.com/formancehq/ledger/internal/storage/system"
ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
- "github.com/formancehq/go-libs/v3/bun/bunpaginate"
"github.com/formancehq/go-libs/v3/metadata"
ledger "github.com/formancehq/ledger/internal"
)
type Store interface {
GetLedger(ctx context.Context, name string) (*ledger.Ledger, error)
- ListLedgers(ctx context.Context, query common.ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Ledger], error)
+ Ledgers() common.PaginatedResource[ledger.Ledger, systemstore.ListLedgersQueryPayload, common.ColumnPaginatedQuery[systemstore.ListLedgersQueryPayload]]
UpdateLedgerMetadata(ctx context.Context, name string, m metadata.Metadata) error
DeleteLedgerMetadata(ctx context.Context, param string, key string) error
+}
+
+type Driver interface {
OpenLedger(context.Context, string) (ledgercontroller.Store, *ledger.Ledger, error)
CreateLedger(context.Context, *ledger.Ledger) error
+ GetSystemStore() Store
}
diff --git a/internal/errors.go b/internal/errors.go
index aa6d9e7779..4159e9b0cd 100644
--- a/internal/errors.go
+++ b/internal/errors.go
@@ -1,6 +1,8 @@
package ledger
-import "fmt"
+import (
+ "fmt"
+)
type ErrInvalidLedgerName struct {
err error
@@ -37,3 +39,65 @@ func (e ErrInvalidBucketName) Is(err error) bool {
func newErrInvalidBucketName(bucket string, err error) ErrInvalidBucketName {
return ErrInvalidBucketName{err: err, bucket: bucket}
}
+
+type ErrExporterUsed string
+
+func (e ErrExporterUsed) Error() string {
+ return fmt.Sprintf("exporter '%s' actually used by an existing pipeline", string(e))
+}
+
+func (e ErrExporterUsed) Is(err error) bool {
+ _, ok := err.(ErrExporterUsed)
+ return ok
+}
+
+func NewErrExporterUsed(id string) ErrExporterUsed {
+ return ErrExporterUsed(id)
+}
+
+// ErrPipelineAlreadyExists denotes a pipeline already created
+// The store is in charge of returning this error on a failing call on Store.CreatePipeline
+type ErrPipelineAlreadyExists PipelineConfiguration
+
+func (e ErrPipelineAlreadyExists) Error() string {
+ return fmt.Sprintf("pipeline '%s/%s' already exists", e.Ledger, e.ExporterID)
+}
+
+func (e ErrPipelineAlreadyExists) Is(err error) bool {
+ _, ok := err.(ErrPipelineAlreadyExists)
+ return ok
+}
+
+func NewErrPipelineAlreadyExists(pipelineConfiguration PipelineConfiguration) ErrPipelineAlreadyExists {
+ return ErrPipelineAlreadyExists(pipelineConfiguration)
+}
+
+type ErrPipelineNotFound string
+
+func (e ErrPipelineNotFound) Error() string {
+ return fmt.Sprintf("pipeline '%s' not found", string(e))
+}
+
+func (e ErrPipelineNotFound) Is(err error) bool {
+ _, ok := err.(ErrPipelineNotFound)
+ return ok
+}
+
+func NewErrPipelineNotFound(id string) ErrPipelineNotFound {
+ return ErrPipelineNotFound(id)
+}
+
+type ErrAlreadyStarted string
+
+func (e ErrAlreadyStarted) Error() string {
+ return fmt.Sprintf("pipeline '%s' already started", string(e))
+}
+
+func (e ErrAlreadyStarted) Is(err error) bool {
+ _, ok := err.(ErrAlreadyStarted)
+ return ok
+}
+
+func NewErrAlreadyStarted(id string) ErrAlreadyStarted {
+ return ErrAlreadyStarted(id)
+}
diff --git a/internal/exporter.go b/internal/exporter.go
new file mode 100644
index 0000000000..0c44a1b2c7
--- /dev/null
+++ b/internal/exporter.go
@@ -0,0 +1,37 @@
+package ledger
+
+import (
+ "encoding/json"
+ "github.com/uptrace/bun"
+
+ "github.com/formancehq/go-libs/v3/time"
+ "github.com/google/uuid"
+)
+
+type ExporterConfiguration struct {
+ Driver string `json:"driver" bun:"driver"`
+ Config json.RawMessage `json:"config" bun:"config"`
+}
+
+func NewExporterConfiguration(driver string, config json.RawMessage) ExporterConfiguration {
+ return ExporterConfiguration{
+ Driver: driver,
+ Config: config,
+ }
+}
+
+type Exporter struct {
+ bun.BaseModel `bun:"table:_system.exporters"`
+
+ ID string `json:"id" bun:"id,pk"`
+ CreatedAt time.Time `json:"createdAt" bun:"created_at"`
+ ExporterConfiguration
+}
+
+func NewExporter(configuration ExporterConfiguration) Exporter {
+ return Exporter{
+ ExporterConfiguration: configuration,
+ ID: uuid.NewString(),
+ CreatedAt: time.Now(),
+ }
+}
diff --git a/internal/pipeline.go b/internal/pipeline.go
new file mode 100644
index 0000000000..1b5ebc339a
--- /dev/null
+++ b/internal/pipeline.go
@@ -0,0 +1,46 @@
+package ledger
+
+import (
+ "fmt"
+ "github.com/uptrace/bun"
+
+ "github.com/formancehq/go-libs/v3/time"
+ "github.com/google/uuid"
+)
+
+type PipelineConfiguration struct {
+ Ledger string `json:"ledger" bun:"ledger"`
+ ExporterID string `json:"exporterID" bun:"exporter_id"`
+}
+
+func (p PipelineConfiguration) String() string {
+ return fmt.Sprintf("%s/%s", p.Ledger, p.ExporterID)
+}
+
+func NewPipelineConfiguration(ledger, exporterID string) PipelineConfiguration {
+ return PipelineConfiguration{
+ Ledger: ledger,
+ ExporterID: exporterID,
+ }
+}
+
+type Pipeline struct {
+ bun.BaseModel `bun:"table:_system.pipelines"`
+
+ PipelineConfiguration
+ CreatedAt time.Time `json:"createdAt" bun:"created_at"`
+ ID string `json:"id" bun:"id,pk"`
+ Enabled bool `json:"enabled" bun:"enabled"`
+ LastLogID *uint64 `json:"lastLogID,omitempty" bun:"last_log_id"`
+ Error string `json:"error,omitempty" bun:"error"`
+}
+
+func NewPipeline(pipelineConfiguration PipelineConfiguration) Pipeline {
+ return Pipeline{
+ ID: uuid.NewString(),
+ PipelineConfiguration: pipelineConfiguration,
+ Enabled: true,
+ CreatedAt: time.Now(),
+ LastLogID: nil,
+ }
+}
diff --git a/internal/replication/config/config.go b/internal/replication/config/config.go
new file mode 100644
index 0000000000..8bce4c57cc
--- /dev/null
+++ b/internal/replication/config/config.go
@@ -0,0 +1,9 @@
+package config
+
+type Validator interface {
+ Validate() error
+}
+
+type Defaulter interface {
+ SetDefaults()
+}
diff --git a/internal/replication/controller_grpc_client.go b/internal/replication/controller_grpc_client.go
new file mode 100644
index 0000000000..621d8616bc
--- /dev/null
+++ b/internal/replication/controller_grpc_client.go
@@ -0,0 +1,147 @@
+package replication
+
+import (
+ "context"
+ "github.com/formancehq/go-libs/v3/bun/bunpaginate"
+ . "github.com/formancehq/go-libs/v3/collectionutils"
+ "github.com/formancehq/go-libs/v3/pointer"
+ ledger "github.com/formancehq/ledger/internal"
+ "github.com/formancehq/ledger/internal/controller/system"
+ "github.com/formancehq/ledger/internal/replication/grpc"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/grpc/status"
+)
+
+type ThroughGRPCBackend struct {
+ client grpc.ReplicationClient
+}
+
+func (t ThroughGRPCBackend) ListExporters(ctx context.Context) (*bunpaginate.Cursor[ledger.Exporter], error) {
+ ret, err := t.client.ListExporters(ctx, &grpc.ListExportersRequest{})
+ if err != nil {
+ return nil, err
+ }
+
+ return mapCursorFromGRPC(ret.Cursor, Map(ret.Data, mapExporterFromGRPC)), nil
+}
+
+func (t ThroughGRPCBackend) CreateExporter(ctx context.Context, configuration ledger.ExporterConfiguration) (*ledger.Exporter, error) {
+ exporter, err := t.client.CreateExporter(ctx, &grpc.CreateExporterRequest{
+ Config: mapExporterConfiguration(configuration),
+ })
+ if err != nil {
+ if status.Code(err) != codes.InvalidArgument {
+ return nil, system.NewErrInvalidDriverConfiguration(configuration.Driver, err)
+ }
+
+ return nil, err
+ }
+
+ return pointer.For(mapExporterFromGRPC(exporter.Exporter)), nil
+}
+
+func (t ThroughGRPCBackend) DeleteExporter(ctx context.Context, id string) error {
+ _, err := t.client.DeleteExporter(ctx, &grpc.DeleteExporterRequest{
+ Id: id,
+ })
+ if err != nil && status.Code(err) == codes.NotFound {
+ return system.NewErrExporterNotFound(id)
+ }
+ return err
+}
+
+func (t ThroughGRPCBackend) GetExporter(ctx context.Context, id string) (*ledger.Exporter, error) {
+ exporter, err := t.client.GetExporter(ctx, &grpc.GetExporterRequest{
+ Id: id,
+ })
+ if err != nil {
+ if status.Code(err) == codes.NotFound {
+ return nil, system.NewErrExporterNotFound(id)
+ }
+ return nil, err
+ }
+
+ return pointer.For(mapExporterFromGRPC(exporter.Exporter)), nil
+}
+
+func (t ThroughGRPCBackend) ListPipelines(ctx context.Context) (*bunpaginate.Cursor[ledger.Pipeline], error) {
+ pipelines, err := t.client.ListPipelines(ctx, &grpc.ListPipelinesRequest{})
+ if err != nil {
+ return nil, err
+ }
+
+ return mapCursorFromGRPC(pipelines.Cursor, Map(pipelines.Data, mapPipelineFromGRPC)), nil
+}
+
+func (t ThroughGRPCBackend) GetPipeline(ctx context.Context, id string) (*ledger.Pipeline, error) {
+ pipeline, err := t.client.GetPipeline(ctx, &grpc.GetPipelineRequest{
+ Id: id,
+ })
+ if err != nil {
+ if status.Code(err) == codes.NotFound {
+ return nil, ledger.NewErrPipelineNotFound(id)
+ }
+ return nil, err
+ }
+
+ return pointer.For(mapPipelineFromGRPC(pipeline.Pipeline)), nil
+}
+
+func (t ThroughGRPCBackend) CreatePipeline(ctx context.Context, pipelineConfiguration ledger.PipelineConfiguration) (*ledger.Pipeline, error) {
+ pipeline, err := t.client.CreatePipeline(ctx, &grpc.CreatePipelineRequest{
+ Config: mapPipelineConfiguration(pipelineConfiguration),
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return pointer.For(mapPipelineFromGRPC(pipeline.Pipeline)), nil
+}
+
+func (t ThroughGRPCBackend) DeletePipeline(ctx context.Context, id string) error {
+ _, err := t.client.DeletePipeline(ctx, &grpc.DeletePipelineRequest{
+ Id: id,
+ })
+ if err != nil && status.Code(err) == codes.NotFound {
+ return ledger.NewErrPipelineNotFound(id)
+ }
+ return err
+}
+
+func (t ThroughGRPCBackend) StartPipeline(ctx context.Context, id string) error {
+ _, err := t.client.StartPipeline(ctx, &grpc.StartPipelineRequest{
+ Id: id,
+ })
+ if err != nil && status.Code(err) == codes.FailedPrecondition {
+ return ledger.NewErrAlreadyStarted(id)
+ }
+ return err
+}
+
+func (t ThroughGRPCBackend) ResetPipeline(ctx context.Context, id string) error {
+ _, err := t.client.ResetPipeline(ctx, &grpc.ResetPipelineRequest{
+ Id: id,
+ })
+ if err != nil && status.Code(err) == codes.NotFound {
+ return ledger.NewErrPipelineNotFound(id)
+ }
+ return err
+}
+
+func (t ThroughGRPCBackend) StopPipeline(ctx context.Context, id string) error {
+ _, err := t.client.StopPipeline(ctx, &grpc.StopPipelineRequest{
+ Id: id,
+ })
+ if err != nil && status.Code(err) == codes.NotFound {
+ return ledger.NewErrPipelineNotFound(id)
+ }
+ return err
+}
+
+var _ system.ReplicationBackend = (*ThroughGRPCBackend)(nil)
+
+func NewThroughGRPCBackend(client grpc.ReplicationClient) *ThroughGRPCBackend {
+ return &ThroughGRPCBackend{
+ client: client,
+ }
+}
diff --git a/internal/replication/controller_grpc_server.go b/internal/replication/controller_grpc_server.go
new file mode 100644
index 0000000000..70de5de9e3
--- /dev/null
+++ b/internal/replication/controller_grpc_server.go
@@ -0,0 +1,181 @@
+package replication
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "github.com/formancehq/go-libs/v3/collectionutils"
+ ledger "github.com/formancehq/ledger/internal"
+ "github.com/formancehq/ledger/internal/controller/system"
+ "github.com/formancehq/ledger/internal/replication/grpc"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/grpc/status"
+)
+
+type GRPCServiceImpl struct {
+ grpc.UnimplementedReplicationServer
+ manager *Manager
+}
+
+func (srv GRPCServiceImpl) CreateExporter(ctx context.Context, request *grpc.CreateExporterRequest) (*grpc.CreateExporterResponse, error) {
+ exporter, err := srv.manager.CreateExporter(ctx, ledger.ExporterConfiguration{
+ Driver: request.Config.Driver,
+ Config: json.RawMessage(request.Config.Config),
+ })
+ if err != nil {
+ switch {
+ case errors.Is(err, system.ErrInvalidDriverConfiguration{}):
+ err := &system.ErrInvalidDriverConfiguration{}
+ if !errors.As(err, &err) {
+ panic("should never happen")
+ }
+
+ return nil, status.Errorf(codes.InvalidArgument, "%s", err.Error())
+ default:
+ return nil, err
+ }
+ }
+
+ return &grpc.CreateExporterResponse{
+ Exporter: mapExporter(*exporter),
+ }, nil
+}
+
+func (srv GRPCServiceImpl) ListExporters(ctx context.Context, _ *grpc.ListExportersRequest) (*grpc.ListExportersResponse, error) {
+ ret, err := srv.manager.ListExporters(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ return &grpc.ListExportersResponse{
+ Data: collectionutils.Map(ret.Data, mapExporter),
+ Cursor: mapCursor(ret),
+ }, nil
+}
+
+func (srv GRPCServiceImpl) GetExporter(ctx context.Context, request *grpc.GetExporterRequest) (*grpc.GetExporterResponse, error) {
+ ret, err := srv.manager.GetExporter(ctx, request.Id)
+ if err != nil {
+ switch {
+ case errors.Is(err, system.ErrExporterNotFound("")):
+ return nil, status.Errorf(codes.NotFound, "%s", err.Error())
+ default:
+ return nil, err
+ }
+ }
+
+ return &grpc.GetExporterResponse{
+ Exporter: mapExporter(*ret),
+ }, nil
+}
+
+func (srv GRPCServiceImpl) DeleteExporter(ctx context.Context, request *grpc.DeleteExporterRequest) (*grpc.DeleteExporterResponse, error) {
+ if err := srv.manager.DeleteExporter(ctx, request.Id); err != nil {
+ switch {
+ case errors.Is(err, system.ErrExporterNotFound("")):
+ return nil, status.Errorf(codes.NotFound, "%s", err.Error())
+ default:
+ return nil, err
+ }
+ }
+ return &grpc.DeleteExporterResponse{}, nil
+}
+
+func (srv GRPCServiceImpl) ListPipelines(ctx context.Context, _ *grpc.ListPipelinesRequest) (*grpc.ListPipelinesResponse, error) {
+ cursor, err := srv.manager.ListPipelines(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ return &grpc.ListPipelinesResponse{
+ Data: collectionutils.Map(cursor.Data, mapPipeline),
+ Cursor: mapCursor(cursor),
+ }, nil
+}
+
+func (srv GRPCServiceImpl) GetPipeline(ctx context.Context, request *grpc.GetPipelineRequest) (*grpc.GetPipelineResponse, error) {
+ pipeline, err := srv.manager.GetPipeline(ctx, request.Id)
+ if err != nil {
+ switch {
+ case errors.Is(err, ledger.ErrPipelineNotFound("")):
+ return nil, status.Errorf(codes.NotFound, "%s", err.Error())
+ default:
+ return nil, err
+ }
+ }
+
+ return &grpc.GetPipelineResponse{
+ Pipeline: mapPipeline(*pipeline),
+ }, nil
+}
+
+func (srv GRPCServiceImpl) CreatePipeline(ctx context.Context, request *grpc.CreatePipelineRequest) (*grpc.CreatePipelineResponse, error) {
+ pipeline, err := srv.manager.CreatePipeline(ctx, mapPipelineConfigurationFromGRPC(request.Config))
+ if err != nil {
+ return nil, err
+ }
+
+ return &grpc.CreatePipelineResponse{
+ Pipeline: mapPipeline(*pipeline),
+ }, nil
+}
+
+func (srv GRPCServiceImpl) DeletePipeline(ctx context.Context, request *grpc.DeletePipelineRequest) (*grpc.DeletePipelineResponse, error) {
+ if err := srv.manager.DeletePipeline(ctx, request.Id); err != nil {
+ switch {
+ case errors.Is(err, ledger.ErrPipelineNotFound("")):
+ return nil, status.Errorf(codes.NotFound, "%s", err.Error())
+ default:
+ return nil, err
+ }
+ }
+ return &grpc.DeletePipelineResponse{}, nil
+}
+
+func (srv GRPCServiceImpl) StartPipeline(ctx context.Context, request *grpc.StartPipelineRequest) (*grpc.StartPipelineResponse, error) {
+ if err := srv.manager.StartPipeline(ctx, request.Id); err != nil {
+ switch {
+ case errors.Is(err, ledger.ErrAlreadyStarted("")):
+ return nil, status.Errorf(codes.FailedPrecondition, "%s", err.Error())
+ default:
+ return nil, err
+ }
+ }
+
+ return &grpc.StartPipelineResponse{}, nil
+}
+
+func (srv GRPCServiceImpl) StopPipeline(ctx context.Context, request *grpc.StopPipelineRequest) (*grpc.StopPipelineResponse, error) {
+ err := srv.manager.StopPipeline(ctx, request.Id)
+ if err != nil {
+ switch {
+ case errors.Is(err, ledger.ErrPipelineNotFound("")):
+ return nil, status.Errorf(codes.NotFound, "%s", err.Error())
+ default:
+ return nil, err
+ }
+ }
+
+ return &grpc.StopPipelineResponse{}, nil
+}
+
+func (srv GRPCServiceImpl) ResetPipeline(ctx context.Context, request *grpc.ResetPipelineRequest) (*grpc.ResetPipelineResponse, error) {
+ if err := srv.manager.ResetPipeline(ctx, request.Id); err != nil {
+ switch {
+ case errors.Is(err, ledger.ErrPipelineNotFound("")):
+ return nil, status.Errorf(codes.NotFound, "%s", err.Error())
+ default:
+ return nil, err
+ }
+ }
+
+ return &grpc.ResetPipelineResponse{}, nil
+}
+
+var _ grpc.ReplicationServer = (*GRPCServiceImpl)(nil)
+
+func NewReplicationServiceImpl(runner *Manager) *GRPCServiceImpl {
+ return &GRPCServiceImpl{
+ manager: runner,
+ }
+}
diff --git a/internal/replication/driver_facade.go b/internal/replication/driver_facade.go
new file mode 100644
index 0000000000..f7d50511a3
--- /dev/null
+++ b/internal/replication/driver_facade.go
@@ -0,0 +1,97 @@
+package replication
+
+import (
+ "context"
+ "time"
+
+ "github.com/formancehq/go-libs/v3/logging"
+ "github.com/formancehq/ledger/internal/replication/drivers"
+ "github.com/pkg/errors"
+)
+
+type DriverFacade struct {
+ drivers.Driver
+ readyChan chan struct{}
+ logger logging.Logger
+ retryInterval time.Duration
+
+ startContext context.Context
+ cancelStart func()
+ startingChan chan struct{}
+}
+
+func (c *DriverFacade) Ready() chan struct{} {
+ return c.readyChan
+}
+
+func (c *DriverFacade) Run(ctx context.Context) {
+
+ c.startContext, c.cancelStart = context.WithCancel(ctx)
+
+ go func() {
+ defer close(c.startingChan)
+ for {
+ if err := c.Start(c.startContext); err != nil {
+ c.logger.Errorf("unable to start exporter: %s", err)
+ if errors.Is(err, context.Canceled) {
+ return
+ }
+ select {
+ case <-ctx.Done():
+ return
+ case <-time.After(c.retryInterval):
+ }
+ continue
+ }
+
+ close(c.readyChan)
+ return
+ }
+ }()
+}
+
+func (c *DriverFacade) Stop(ctx context.Context) error {
+ select {
+ case <-c.startingChan:
+ // not in starting phase
+ default:
+ // Cancel start
+ c.cancelStart()
+
+ // Wait for the termination of the routine starting the driver
+ select {
+ case <-c.startingChan:
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+ }
+
+ // Check if driver has been started
+ select {
+ case <-c.readyChan:
+ return c.Driver.Stop(ctx)
+ default:
+ return nil
+ }
+}
+
+func (c *DriverFacade) Accept(ctx context.Context, logs ...drivers.LogWithLedger) ([]error, error) {
+ select {
+ case <-c.readyChan:
+ return c.Driver.Accept(ctx, logs...)
+ default:
+ return nil, errors.New("not ready exporter")
+ }
+}
+
+var _ drivers.Driver = (*DriverFacade)(nil)
+
+func newDriverFacade(driver drivers.Driver, logger logging.Logger, retryInterval time.Duration) *DriverFacade {
+ return &DriverFacade{
+ Driver: driver,
+ readyChan: make(chan struct{}),
+ startingChan: make(chan struct{}),
+ logger: logger,
+ retryInterval: retryInterval,
+ }
+}
diff --git a/internal/replication/drivers/all/drivers.go b/internal/replication/drivers/all/drivers.go
new file mode 100644
index 0000000000..55668517d8
--- /dev/null
+++ b/internal/replication/drivers/all/drivers.go
@@ -0,0 +1,18 @@
+package all
+
+import (
+ "github.com/formancehq/ledger/internal/replication/drivers"
+ "github.com/formancehq/ledger/internal/replication/drivers/clickhouse"
+ "github.com/formancehq/ledger/internal/replication/drivers/elasticsearch"
+ "github.com/formancehq/ledger/internal/replication/drivers/http"
+ "github.com/formancehq/ledger/internal/replication/drivers/noop"
+ "github.com/formancehq/ledger/internal/replication/drivers/stdout"
+)
+
+func Register(driversRegistry *drivers.Registry) {
+ driversRegistry.RegisterDriver("elasticsearch", elasticsearch.NewDriver)
+ driversRegistry.RegisterDriver("clickhouse", clickhouse.NewDriver)
+ driversRegistry.RegisterDriver("stdout", stdout.NewDriver)
+ driversRegistry.RegisterDriver("http", http.NewDriver)
+ driversRegistry.RegisterDriver("noop", noop.NewDriver)
+}
diff --git a/internal/replication/drivers/batcher.go b/internal/replication/drivers/batcher.go
new file mode 100644
index 0000000000..8bfe54bef3
--- /dev/null
+++ b/internal/replication/drivers/batcher.go
@@ -0,0 +1,186 @@
+package drivers
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "sync"
+ "time"
+
+ "github.com/formancehq/go-libs/v3/collectionutils"
+ "github.com/formancehq/go-libs/v3/logging"
+ "github.com/pkg/errors"
+ "go.vallahaye.net/batcher"
+)
+
+type Batcher struct {
+ Driver
+ mu sync.Mutex
+ batcher *batcher.Batcher[LogWithLedger, error]
+ cancel context.CancelFunc
+ stopped chan struct{}
+ batching Batching
+ logger logging.Logger
+}
+
+func (b *Batcher) Accept(ctx context.Context, logs ...LogWithLedger) ([]error, error) {
+ itemsErrors := make([]error, len(logs))
+ operations := make(batcher.Operations[LogWithLedger, error], len(logs))
+ for ind, log := range logs {
+ ret, err := b.batcher.Send(ctx, log)
+ if err != nil {
+ itemsErrors[ind] = fmt.Errorf("failed to send log to the batcher: %w", err)
+ continue
+ }
+ operations[ind] = ret
+ }
+
+ for ind, operation := range operations {
+ if _, err := operation.Wait(ctx); err != nil {
+ itemsErrors[ind] = fmt.Errorf("failure while waiting for operation completion: %w", err)
+ continue
+ }
+ }
+
+ for _, err := range itemsErrors {
+ if err != nil {
+ return itemsErrors, fmt.Errorf("some logs failed to be sent to the batcher: %w", err)
+ }
+ }
+
+ return itemsErrors, nil
+}
+
+func (b *Batcher) commit(ctx context.Context, logs batcher.Operations[LogWithLedger, error]) {
+ b.logger.WithFields(map[string]any{
+ "len": len(logs),
+ }).Debugf("commit batch")
+ itemsErrors, err := b.Driver.Accept(ctx, collectionutils.Map(logs, func(from *batcher.Operation[LogWithLedger, error]) LogWithLedger {
+ return from.Value
+ })...)
+ if err != nil {
+ for _, log := range logs {
+ log.SetError(err)
+ }
+ return
+ }
+ for index, log := range logs {
+ if itemsErrors[index] != nil {
+ log.SetError(itemsErrors[index])
+ } else {
+ log.SetResult(nil)
+ }
+ }
+}
+
+func (b *Batcher) Start(ctx context.Context) error {
+ b.mu.Lock()
+ defer b.mu.Unlock()
+
+ b.logger.Infof("starting batching with parameters: maxItems=%d, flushInterval=%s", b.batching.MaxItems, b.batching.FlushInterval)
+
+ if err := b.Driver.Start(ctx); err != nil {
+ return errors.Wrap(err, "failed to start exporter")
+ }
+
+ ctx, b.cancel = context.WithCancel(ctx)
+ b.stopped = make(chan struct{})
+ go func() {
+ defer close(b.stopped)
+ b.batcher.Batch(ctx)
+ }()
+ return nil
+}
+
+func (b *Batcher) Stop(ctx context.Context) error {
+ b.mu.Lock()
+ defer b.mu.Unlock()
+
+ if b.cancel == nil {
+ return nil
+ }
+
+ b.logger.Infof("stopping batching")
+ b.cancel()
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-b.stopped:
+ return b.Driver.Stop(ctx)
+ }
+}
+
+func newBatcher(driver Driver, batching Batching, logger logging.Logger) *Batcher {
+ ret := &Batcher{
+ Driver: driver,
+ batching: batching,
+ logger: logger.WithFields(map[string]any{
+ "component": "batcher",
+ }),
+ }
+ ret.batcher = batcher.New(
+ ret.commit,
+ batcher.WithTimeout[LogWithLedger, error](batching.FlushInterval),
+ batcher.WithMaxSize[LogWithLedger, error](batching.MaxItems),
+ )
+ return ret
+}
+
+type Batching struct {
+ MaxItems int `json:"maxItems"`
+ FlushInterval time.Duration `json:"flushInterval"`
+}
+
+func (b Batching) MarshalJSON() ([]byte, error) {
+ type Aux Batching
+ return json.Marshal(struct {
+ Aux
+ FlushInterval string `json:"flushInterval,omitempty"`
+ }{
+ Aux: Aux(b),
+ FlushInterval: b.FlushInterval.String(),
+ })
+}
+
+func (b *Batching) UnmarshalJSON(data []byte) error {
+ type Aux Batching
+ x := struct {
+ Aux
+ FlushInterval string `json:"flushInterval,omitempty"`
+ }{}
+ if err := json.Unmarshal(data, &x); err != nil {
+ return err
+ }
+
+ *b = Batching{
+ MaxItems: x.MaxItems,
+ }
+
+ if x.FlushInterval != "" {
+ var err error
+ b.FlushInterval, err = time.ParseDuration(x.FlushInterval)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (b *Batching) Validate() error {
+ if b.MaxItems < 0 {
+ return errors.New("flushBytes must be greater than 0")
+ }
+
+ if b.MaxItems == 0 && b.FlushInterval == 0 {
+ return errors.New("while configuring the batcher with unlimited size, you must configure the flush interval")
+ }
+
+ return nil
+}
+
+func (b *Batching) SetDefaults() {
+ if b.MaxItems == 0 && b.FlushInterval == 0 {
+ b.FlushInterval = time.Second
+ }
+}
diff --git a/internal/replication/drivers/batcher_test.go b/internal/replication/drivers/batcher_test.go
new file mode 100644
index 0000000000..71e662e9f2
--- /dev/null
+++ b/internal/replication/drivers/batcher_test.go
@@ -0,0 +1,86 @@
+package drivers
+
+import (
+ "context"
+ ledger "github.com/formancehq/ledger/internal"
+ "testing"
+ "time"
+
+ "github.com/formancehq/go-libs/v3/logging"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/mock/gomock"
+)
+
+func TestBatchingConfiguration(t *testing.T) {
+ t.Parallel()
+
+ type testCase struct {
+ name string
+ configuration Batching
+ expectError string
+ }
+
+ for _, testCase := range []testCase{
+ {
+ name: "nominal",
+ configuration: Batching{
+ FlushInterval: time.Second,
+ },
+ },
+ {
+ name: "no configuration",
+ configuration: Batching{},
+ expectError: "while configuring the batcher with unlimited size, you must configure the flush interval",
+ },
+ {
+ name: "negative max item",
+ configuration: Batching{
+ MaxItems: -1,
+ },
+ expectError: "flushBytes must be greater than 0",
+ },
+ } {
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ err := testCase.configuration.Validate()
+ if testCase.expectError != "" {
+ require.EqualError(t, err, testCase.expectError)
+ } else {
+ require.NoError(t, err)
+ }
+ })
+ }
+
+}
+
+func TestBatcher(t *testing.T) {
+ t.Parallel()
+
+ ctrl := gomock.NewController(t)
+ driver := NewMockDriver(ctrl)
+ logger := logging.Testing()
+ ctx := context.TODO()
+
+ log := NewLogWithLedger("module1", ledger.Log{})
+
+ driver.EXPECT().Start(gomock.Any()).Return(nil)
+ driver.EXPECT().Stop(gomock.Any()).Return(nil)
+ driver.EXPECT().
+ Accept(gomock.Any(), log).
+ Return([]error{nil}, nil)
+
+ batcher := newBatcher(driver, Batching{
+ MaxItems: 5,
+ FlushInterval: 50 * time.Millisecond,
+ }, logger)
+ require.NoError(t, batcher.Start(ctx))
+ t.Cleanup(func() {
+ require.NoError(t, batcher.Stop(ctx))
+ })
+
+ itemsErrors, err := batcher.Accept(ctx, log)
+ require.NoError(t, err)
+ require.Len(t, itemsErrors, 1)
+ require.Nil(t, itemsErrors[0])
+}
diff --git a/internal/replication/drivers/clickhouse/driver.go b/internal/replication/drivers/clickhouse/driver.go
new file mode 100644
index 0000000000..d0f03f5b9a
--- /dev/null
+++ b/internal/replication/drivers/clickhouse/driver.go
@@ -0,0 +1,176 @@
+package clickhouse
+
+import (
+ "context"
+ "encoding/json"
+ "github.com/ClickHouse/clickhouse-go/v2"
+ "github.com/ClickHouse/clickhouse-go/v2/lib/driver"
+ "github.com/formancehq/go-libs/v3/logging"
+ "github.com/formancehq/ledger/internal/replication/config"
+ "github.com/formancehq/ledger/internal/replication/drivers"
+ "github.com/pkg/errors"
+)
+
+type Driver struct {
+ db driver.Conn
+ config Config
+ logger logging.Logger
+}
+
+func (c *Driver) Stop(_ context.Context) error {
+ return c.db.Close()
+}
+
+func (c *Driver) Start(ctx context.Context) error {
+
+ var err error
+ c.db, err = OpenDB(c.logger, c.config.DSN, false)
+ if err != nil {
+ return errors.Wrap(err, "opening database")
+ }
+
+ // Create the logs table
+ // One table is used for the entire stack
+ err = c.db.Exec(ctx, createLogsTable)
+ if err != nil {
+ return errors.Wrap(err, "failed to create logs table")
+ }
+
+ return nil
+}
+
+func (c *Driver) Accept(ctx context.Context, logs ...drivers.LogWithLedger) ([]error, error) {
+
+ batch, err := c.db.PrepareBatch(ctx, "insert into logs(ledger, id, type, date, data)")
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to prepare batch")
+ }
+
+ for _, log := range logs {
+
+ data, err := json.Marshal(log.Data)
+ if err != nil {
+ return nil, errors.Wrap(err, "marshalling data")
+ }
+
+ if err := batch.Append(
+ log.Ledger,
+ *log.ID,
+ log.Type,
+ // if no timezone is specified, clickhouse assume the timezone is its local timezone
+ // since all our date are in UTC, we just need to pass +00:00 to clickhouse to inform it
+ // see https://clickhouse.com/docs/integrations/go#complex-types
+ log.Date.Format("2006-01-02 15:04:05.999999")+" +00:00",
+ string(data),
+ ); err != nil {
+ return nil, errors.Wrap(err, "appending item to the batch")
+ }
+ }
+
+ return make([]error, len(logs)), errors.Wrap(batch.Send(), "failed to commit transaction")
+}
+
+func NewDriver(config Config, logger logging.Logger) (*Driver, error) {
+ return &Driver{
+ config: config,
+ logger: logger,
+ }, nil
+}
+
+var _ drivers.Driver = (*Driver)(nil)
+
+type Config struct {
+ DSN string `json:"dsn"`
+}
+
+func (cfg Config) Validate() error {
+ if cfg.DSN == "" {
+ return errors.New("dsn is required")
+ }
+
+ return nil
+}
+
+var _ config.Validator = (*Config)(nil)
+
+const createLogsTable = `
+ create table if not exists logs (
+ ledger String,
+ id Int64,
+ type String,
+ date DateTime64(6, 'UTC'),
+ data JSON(
+ transaction JSON(
+ id UInt256,
+ insertedAt DateTime64(6, 'UTC'),
+ postings Array(JSON(
+ source String,
+ destination String,
+ amount UInt256,
+ asset String
+ )),
+ metadata Map(String, String),
+ reference String,
+ preCommitVolumes Map(String, Map(String, JSON(input UInt256, output UInt256, balance Int256))),
+ postCommitVolumes Map(String, Map(String, JSON(input UInt256, output UInt256, balance Int256))),
+ preCommitEffectiveVolumes Map(String, Map(String, JSON(input UInt256, output UInt256, balance Int256))),
+ postCommitEffectiveVolumes Map(String, Map(String, JSON(input UInt256, output UInt256, balance Int256))),
+ reverted Bool,
+ timestamp DateTime64(6, 'UTC')
+ ),
+ accountMetadata Map(String, Map(String, String)),
+ targetId Variant(UInt256, String),
+ targetType Nullable(String),
+ metadata Map(String, String),
+ key Nullable(String),
+ revertedTransaction JSON(
+ id UInt256,
+ insertedAt DateTime64(6, 'UTC'),
+ postings Array(JSON(
+ source String,
+ destination String,
+ amount UInt256,
+ asset String
+ )),
+ metadata Map(String, String),
+ reference String,
+ preCommitVolumes Map(String, Map(String, JSON(input UInt256, output UInt256, balance Int256))),
+ postCommitVolumes Map(String, Map(String, JSON(input UInt256, output UInt256, balance Int256))),
+ preCommitEffectiveVolumes Map(String, Map(String, JSON(input UInt256, output UInt256, balance Int256))),
+ postCommitEffectiveVolumes Map(String, Map(String, JSON(input UInt256, output UInt256, balance Int256))),
+ reverted Bool,
+ timestamp DateTime64(6, 'UTC')
+ )
+ )
+ )
+ engine = ReplacingMergeTree
+ partition by ledger
+ primary key (ledger, id);
+`
+
+func OpenDB(logger logging.Logger, dsn string, debug bool) (driver.Conn, error) {
+ // Open database connection
+ options, err := clickhouse.ParseDSN(dsn)
+ if err != nil {
+ return nil, errors.Wrap(err, "parsing dsn")
+ }
+ if debug {
+ options.Debug = true
+ options.Debugf = logger.Debugf
+ }
+ options.Settings = map[string]any{
+ "date_time_input_format": "best_effort",
+ "date_time_output_format": "iso",
+ "allow_experimental_dynamic_type": true,
+ "enable_json_type": true,
+ "enable_variant_type": true,
+ "output_format_json_quote_64bit_integers": false,
+ }
+
+ db, err := clickhouse.Open(options)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to open db")
+ }
+
+ return db, nil
+}
diff --git a/internal/replication/drivers/clickhouse/driver_test.go b/internal/replication/drivers/clickhouse/driver_test.go
new file mode 100644
index 0000000000..402187641b
--- /dev/null
+++ b/internal/replication/drivers/clickhouse/driver_test.go
@@ -0,0 +1,115 @@
+//go:build it
+
+package clickhouse
+
+import (
+ "context"
+ "fmt"
+ "github.com/ClickHouse/clickhouse-go/v2/lib/driver"
+ "github.com/formancehq/go-libs/v3/logging"
+ "github.com/formancehq/go-libs/v3/pointer"
+ "github.com/formancehq/go-libs/v3/testing/docker"
+ "github.com/formancehq/go-libs/v3/testing/platform/clickhousetesting"
+ "github.com/formancehq/go-libs/v3/time"
+ ledger "github.com/formancehq/ledger/internal"
+ "github.com/formancehq/ledger/internal/replication/drivers"
+ "github.com/pkg/errors"
+ "github.com/stretchr/testify/require"
+ "testing"
+)
+
+func TestClickhouseDriver(t *testing.T) {
+ t.Parallel()
+
+ ctx := context.TODO()
+
+ // Start a new clickhouse server
+ dockerPool := docker.NewPool(t, logging.Testing())
+ srv := clickhousetesting.CreateServer(dockerPool, clickhousetesting.WithVersion("24.12"))
+
+ // Create our driver
+ driver, err := NewDriver(Config{
+ DSN: srv.GetDSN(),
+ }, logging.Testing())
+ require.NoError(t, err)
+ require.NoError(t, driver.Start(ctx))
+ t.Cleanup(func() {
+ require.NoError(t, driver.Stop(ctx))
+ })
+
+ // We will insert numberOfLogs logs split across numberOfModules modules
+ const (
+ numberOfLogs = 50
+ numberOfModules = 2
+ )
+ now := time.Now()
+ logs := make([]drivers.LogWithLedger, numberOfLogs)
+ for i := 0; i < numberOfLogs; i++ {
+ log := ledger.NewLog(ledger.CreatedTransaction{
+ Transaction: ledger.NewTransaction().
+ WithInsertedAt(now).
+ WithTimestamp(now),
+ })
+ log.ID = pointer.For(uint64(i))
+ log.Date = now
+ logs[i] = drivers.NewLogWithLedger(
+ fmt.Sprintf("module%d", i%numberOfModules),
+ log,
+ )
+ }
+
+ // Send all logs to the driver
+ itemsErrors, err := driver.Accept(ctx, logs...)
+ require.NoError(t, err)
+ require.Len(t, itemsErrors, numberOfLogs)
+ for index := range logs {
+ require.Nil(t, itemsErrors[index])
+ }
+
+ // Ensure data has been inserted
+ require.Equal(t, numberOfLogs, count(t, ctx, driver, `select count(*) from logs`))
+ _, err = readLogs(ctx, driver.db)
+ require.NoError(t, err)
+}
+
+func readLogs(ctx context.Context, client driver.Conn) ([]drivers.LogWithLedger, error) {
+ rows, err := client.Query(ctx, "select ledger, id, type, date, toJSONString(data) from logs final")
+ if err != nil {
+ return nil, err
+ }
+
+ ret := make([]drivers.LogWithLedger, 0)
+ for rows.Next() {
+ var (
+ payload string
+ id int64
+ )
+ newLog := drivers.LogWithLedger{}
+ if err := rows.Scan(&newLog.Ledger, &id, &newLog.Type, &newLog.Date, &payload); err != nil {
+ return nil, errors.Wrap(err, "scanning data from database")
+ }
+ newLog.ID = pointer.For(uint64(id))
+
+ newLog.Data, err = ledger.HydrateLog(newLog.Type, []byte(payload))
+ if err != nil {
+ return nil, errors.Wrap(err, "hydrating log data")
+ }
+
+ ret = append(ret, newLog)
+ }
+
+ return ret, nil
+}
+
+func count(t *testing.T, ctx context.Context, driver *Driver, query string) int {
+ rows, err := driver.db.Query(ctx, query)
+ require.NoError(t, err)
+ defer func() {
+ require.NoError(t, rows.Close())
+ }()
+ require.True(t, rows.Next())
+ var count uint64
+ require.NoError(t, rows.Scan(&count))
+
+ return int(count)
+}
diff --git a/internal/replication/drivers/driver.go b/internal/replication/drivers/driver.go
new file mode 100644
index 0000000000..364d4a352a
--- /dev/null
+++ b/internal/replication/drivers/driver.go
@@ -0,0 +1,12 @@
+package drivers
+
+import (
+ "context"
+)
+
+//go:generate mockgen -source driver.go -destination driver_generated.go -package drivers . Driver
+type Driver interface {
+ Start(ctx context.Context) error
+ Stop(ctx context.Context) error
+ Accept(ctx context.Context, logs ...LogWithLedger) ([]error, error)
+}
diff --git a/internal/replication/drivers/driver_generated.go b/internal/replication/drivers/driver_generated.go
new file mode 100644
index 0000000000..3334e69160
--- /dev/null
+++ b/internal/replication/drivers/driver_generated.go
@@ -0,0 +1,89 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: driver.go
+//
+// Generated by this command:
+//
+// mockgen -source driver.go -destination driver_generated.go -package drivers . Driver
+//
+
+// Package drivers is a generated GoMock package.
+package drivers
+
+import (
+ context "context"
+ reflect "reflect"
+
+ gomock "go.uber.org/mock/gomock"
+)
+
+// MockDriver is a mock of Driver interface.
+type MockDriver struct {
+ ctrl *gomock.Controller
+ recorder *MockDriverMockRecorder
+ isgomock struct{}
+}
+
+// MockDriverMockRecorder is the mock recorder for MockDriver.
+type MockDriverMockRecorder struct {
+ mock *MockDriver
+}
+
+// NewMockDriver creates a new mock instance.
+func NewMockDriver(ctrl *gomock.Controller) *MockDriver {
+ mock := &MockDriver{ctrl: ctrl}
+ mock.recorder = &MockDriverMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockDriver) EXPECT() *MockDriverMockRecorder {
+ return m.recorder
+}
+
+// Accept mocks base method.
+func (m *MockDriver) Accept(ctx context.Context, logs ...LogWithLedger) ([]error, error) {
+ m.ctrl.T.Helper()
+ varargs := []any{ctx}
+ for _, a := range logs {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "Accept", varargs...)
+ ret0, _ := ret[0].([]error)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Accept indicates an expected call of Accept.
+func (mr *MockDriverMockRecorder) Accept(ctx any, logs ...any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]any{ctx}, logs...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Accept", reflect.TypeOf((*MockDriver)(nil).Accept), varargs...)
+}
+
+// Start mocks base method.
+func (m *MockDriver) Start(ctx context.Context) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Start", ctx)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Start indicates an expected call of Start.
+func (mr *MockDriverMockRecorder) Start(ctx any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockDriver)(nil).Start), ctx)
+}
+
+// Stop mocks base method.
+func (m *MockDriver) Stop(ctx context.Context) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Stop", ctx)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Stop indicates an expected call of Stop.
+func (mr *MockDriverMockRecorder) Stop(ctx any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockDriver)(nil).Stop), ctx)
+}
diff --git a/internal/replication/drivers/elasticsearch/config.go b/internal/replication/drivers/elasticsearch/config.go
new file mode 100644
index 0000000000..fbf00ab850
--- /dev/null
+++ b/internal/replication/drivers/elasticsearch/config.go
@@ -0,0 +1,60 @@
+package elasticsearch
+
+import (
+ "github.com/formancehq/ledger/internal/replication/config"
+ "github.com/pkg/errors"
+)
+
+const (
+ DefaultIndex = "unified-stack-data"
+)
+
+type Authentication struct {
+ Username string `json:"username"`
+ Password string `json:"password"`
+ AWSEnabled bool `json:"awsEnabled"`
+}
+
+func (a Authentication) Validate() error {
+ switch {
+ case a.Username == "" && a.Password != "" ||
+ a.Username != "" && a.Password == "":
+ return errors.New("username and password must be defined together")
+ case a.AWSEnabled && a.Username != "":
+ return errors.New("username and password defined while aws is enabled")
+ }
+ return nil
+}
+
+type Config struct {
+ Endpoint string `json:"endpoint"`
+ Authentication *Authentication `json:"authentication"`
+ Index string `json:"index"`
+}
+
+func (e *Config) SetDefaults() {
+ if e.Index == "" {
+ e.Index = DefaultIndex
+ }
+}
+
+func (e *Config) Validate() error {
+ if e.Endpoint == "" {
+ return errors.New("elasticsearch endpoint is required")
+ }
+
+ if e.Authentication != nil {
+ if err := e.Authentication.Validate(); err != nil {
+ return errors.Wrap(err, "authentication configuration is invalid")
+ }
+ }
+
+ if e.Index == "" {
+ return errors.New("missing index")
+ }
+
+ return nil
+}
+
+var _ config.Validator = (*Config)(nil)
+var _ config.Defaulter = (*Config)(nil)
diff --git a/internal/replication/drivers/elasticsearch/config_test.go b/internal/replication/drivers/elasticsearch/config_test.go
new file mode 100644
index 0000000000..594c92f9d1
--- /dev/null
+++ b/internal/replication/drivers/elasticsearch/config_test.go
@@ -0,0 +1,97 @@
+package elasticsearch
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestConfig(t *testing.T) {
+ t.Parallel()
+
+ type testCase struct {
+ name string
+ config Config
+ expectError string
+ }
+
+ for _, testCase := range []testCase{
+ {
+ name: "minimal valid",
+ config: Config{
+ Endpoint: "http://localhost:9200",
+ Index: "index",
+ },
+ },
+ {
+ name: "minimal index",
+ config: Config{
+ Endpoint: "http://localhost:9200",
+ },
+ expectError: "missing index",
+ },
+ {
+ name: "missing endpoint",
+ config: Config{},
+ expectError: "elasticsearch endpoint is required",
+ },
+ {
+ name: "with authentication (username/password)",
+ config: Config{
+ Endpoint: "http://localhost:9200",
+ Authentication: &Authentication{
+ Username: "root",
+ Password: "password",
+ },
+ Index: "index",
+ },
+ },
+ {
+ name: "with authentication (aws)",
+ config: Config{
+ Endpoint: "http://localhost:9200",
+ Authentication: &Authentication{
+ AWSEnabled: true,
+ },
+ Index: "index",
+ },
+ },
+ {
+ name: "with username and no password",
+ config: Config{
+ Endpoint: "http://localhost:9200",
+ Authentication: &Authentication{
+ Username: "root",
+ },
+ Index: "index",
+ },
+ expectError: "authentication configuration is invalid: username and password must be defined together",
+ },
+ {
+ name: "with username defined and aws enabled",
+ config: Config{
+ Endpoint: "http://localhost:9200",
+ Authentication: &Authentication{
+ Username: "root",
+ Password: "password",
+ AWSEnabled: true,
+ },
+ Index: "index",
+ },
+ expectError: "authentication configuration is invalid: username and password defined while aws is enabled",
+ },
+ } {
+ testCase := testCase
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ err := testCase.config.Validate()
+ if testCase.expectError != "" {
+ require.NotNil(t, err)
+ require.Equal(t, testCase.expectError, err.Error())
+ } else {
+ require.NoError(t, err)
+ }
+ })
+ }
+}
diff --git a/internal/replication/drivers/elasticsearch/driver.go b/internal/replication/drivers/elasticsearch/driver.go
new file mode 100644
index 0000000000..d3e634530b
--- /dev/null
+++ b/internal/replication/drivers/elasticsearch/driver.go
@@ -0,0 +1,112 @@
+package elasticsearch
+
+import (
+ "context"
+ "encoding/base64"
+ "encoding/json"
+
+ "github.com/formancehq/go-libs/v3/logging"
+ "github.com/formancehq/ledger/internal/replication/drivers"
+ "github.com/olivere/elastic/v7"
+ "github.com/pkg/errors"
+)
+
+type Driver struct {
+ config Config
+ client *elastic.Client
+ logger logging.Logger
+}
+
+func (driver *Driver) Stop(_ context.Context) error {
+ driver.client.Stop()
+ return nil
+}
+
+func (driver *Driver) Start(_ context.Context) error {
+ options := []elastic.ClientOptionFunc{
+ elastic.SetURL(driver.config.Endpoint),
+ }
+ if driver.config.Authentication != nil {
+ options = append(options, elastic.SetBasicAuth(driver.config.Authentication.Username, driver.config.Authentication.Password))
+ }
+
+ var err error
+ driver.client, err = elastic.NewClient(options...)
+ if err != nil {
+ return errors.Wrap(err, "building es client")
+ }
+
+ return nil
+}
+
+func (driver *Driver) Client() *elastic.Client {
+ return driver.client
+}
+
+func (driver *Driver) Accept(ctx context.Context, logs ...drivers.LogWithLedger) ([]error, error) {
+
+ bulk := driver.client.Bulk().Refresh("true")
+ for _, log := range logs {
+
+ data, err := json.Marshal(log.Data)
+ if err != nil {
+ return nil, errors.Wrap(err, "marshalling data")
+ }
+
+ doc := struct {
+ ID string `json:"id"`
+ Payload json.RawMessage `json:"payload"`
+ Module string `json:"module"`
+ }{
+ ID: DocID{
+ Ledger: log.Ledger,
+ LogID: *log.ID,
+ }.String(),
+ Payload: json.RawMessage(data),
+ Module: log.Ledger,
+ }
+
+ bulk.Add(
+ elastic.NewBulkIndexRequest().
+ Index(driver.config.Index).
+ Id(doc.ID).
+ Doc(doc),
+ )
+ }
+
+ rsp, err := bulk.Do(ctx)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to query es")
+ }
+
+ ret := make([]error, len(logs))
+ for index, item := range rsp.Items {
+ errorDetails := item["index"].Error
+ if errorDetails == nil {
+ ret[index] = nil
+ } else {
+ ret[index] = errors.New(errorDetails.Reason)
+ }
+ }
+
+ return ret, nil
+}
+
+func NewDriver(config Config, logger logging.Logger) (*Driver, error) {
+ return &Driver{
+ config: config,
+ logger: logger,
+ }, nil
+}
+
+var _ drivers.Driver = (*Driver)(nil)
+
+type DocID struct {
+ LogID uint64 `json:"logID"`
+ Ledger string `json:"ledger,omitempty"`
+}
+
+func (docID DocID) String() string {
+ rawID, _ := json.Marshal(docID)
+ return base64.URLEncoding.EncodeToString(rawID)
+}
diff --git a/internal/replication/drivers/elasticsearch/driver_test.go b/internal/replication/drivers/elasticsearch/driver_test.go
new file mode 100644
index 0000000000..174abb828d
--- /dev/null
+++ b/internal/replication/drivers/elasticsearch/driver_test.go
@@ -0,0 +1,66 @@
+//go:build it
+
+package elasticsearch
+
+import (
+ "context"
+ "github.com/formancehq/go-libs/v3/logging"
+ "github.com/formancehq/go-libs/v3/pointer"
+ "github.com/formancehq/go-libs/v3/testing/docker"
+ "github.com/formancehq/go-libs/v3/testing/platform/elastictesting"
+ ledger "github.com/formancehq/ledger/internal"
+ "github.com/formancehq/ledger/internal/replication/drivers"
+ "github.com/stretchr/testify/require"
+ "sync"
+ "testing"
+ "time"
+)
+
+func TestElasticSearchDriver(t *testing.T) {
+ t.Parallel()
+
+ dockerPool := docker.NewPool(t, logging.Testing())
+ srv := elastictesting.CreateServer(dockerPool, elastictesting.WithTimeout(2*time.Minute))
+
+ ctx := context.TODO()
+ esConfig := Config{
+ Endpoint: srv.Endpoint(),
+ }
+ esConfig.SetDefaults()
+ driver, err := NewDriver(esConfig, logging.Testing())
+ require.NoError(t, err)
+ require.NoError(t, driver.Start(ctx))
+ t.Cleanup(func() {
+ require.NoError(t, driver.Stop(ctx))
+ })
+
+ const (
+ numberOfEvents = 50
+ ledgerName = "testing"
+ )
+
+ wg := sync.WaitGroup{}
+ for i := 0; i < numberOfEvents; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ log := ledger.NewLog(ledger.CreatedTransaction{
+ Transaction: ledger.NewTransaction(),
+ })
+ log.ID = pointer.For(uint64(i))
+ itemsErrors, err := driver.Accept(ctx, drivers.NewLogWithLedger(ledgerName, log))
+ require.NoError(t, err)
+ require.Len(t, itemsErrors, 1)
+ require.Nil(t, itemsErrors[0])
+ }()
+ }
+ wg.Wait()
+
+ // Ensure all documents has been inserted
+ require.Eventually(t, func() bool {
+ rsp, err := driver.Client().Search(DefaultIndex).Do(ctx)
+ require.NoError(t, err)
+
+ return int64(numberOfEvents) == rsp.Hits.TotalHits.Value
+ }, 2*time.Second, 50*time.Millisecond)
+}
diff --git a/internal/replication/drivers/errors.go b/internal/replication/drivers/errors.go
new file mode 100644
index 0000000000..4459160714
--- /dev/null
+++ b/internal/replication/drivers/errors.go
@@ -0,0 +1,59 @@
+package drivers
+
+import "fmt"
+
+type ErrMalformedConfiguration struct {
+ exporter string
+ err error
+}
+
+func (e *ErrMalformedConfiguration) Error() string {
+ return fmt.Sprintf("exporter '%s' has malformed configuration: %s", e.exporter, e.err)
+}
+
+func NewErrMalformedConfiguration(exporter string, err error) *ErrMalformedConfiguration {
+ return &ErrMalformedConfiguration{
+ exporter: exporter,
+ err: err,
+ }
+}
+
+type ErrInvalidConfiguration struct {
+ exporter string
+ err error
+}
+
+func (e *ErrInvalidConfiguration) Error() string {
+ return fmt.Sprintf("exporter '%s' has invalid configuration: %s", e.exporter, e.err)
+}
+
+func NewErrInvalidConfiguration(exporter string, err error) *ErrInvalidConfiguration {
+ return &ErrInvalidConfiguration{
+ exporter: exporter,
+ err: err,
+ }
+}
+
+type ErrDriverNotFound struct {
+ driver string
+}
+
+func (e *ErrDriverNotFound) Error() string {
+ return fmt.Sprintf("driver '%s' not found", e.driver)
+}
+
+func NewErrDriverNotFound(driver string) *ErrDriverNotFound {
+ return &ErrDriverNotFound{
+ driver: driver,
+ }
+}
+
+type ErrExporterNotFound string
+
+func (e ErrExporterNotFound) Error() string {
+ return fmt.Sprintf("exporter '%s' not found", string(e))
+}
+
+func NewErrExporterNotFound(id string) ErrExporterNotFound {
+ return ErrExporterNotFound(id)
+}
diff --git a/internal/replication/drivers/factory.go b/internal/replication/drivers/factory.go
new file mode 100644
index 0000000000..6dd8bad613
--- /dev/null
+++ b/internal/replication/drivers/factory.go
@@ -0,0 +1,52 @@
+package drivers
+
+import (
+ "context"
+ "encoding/json"
+
+ "github.com/formancehq/go-libs/v3/logging"
+ "github.com/pkg/errors"
+)
+
+//go:generate mockgen -source factory.go -destination factory_generated.go -package drivers . Factory
+type Factory interface {
+ // Create can return following errors:
+ // * ErrExporterNotFound
+ Create(ctx context.Context, id string) (Driver, json.RawMessage, error)
+}
+
+type DriverFactoryWithBatching struct {
+ underlying Factory
+ logger logging.Logger
+}
+
+func (c *DriverFactoryWithBatching) Create(ctx context.Context, id string) (Driver, json.RawMessage, error) {
+ exporter, rawConfig, err := c.underlying.Create(ctx, id)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ type batchingHolder struct {
+ Batching Batching `json:"batching"`
+ }
+ bh := batchingHolder{}
+ if err := json.Unmarshal(rawConfig, &bh); err != nil {
+ return nil, nil, errors.Wrap(err, "extracting batching config")
+ }
+
+ bh.Batching.SetDefaults()
+ if err := bh.Batching.Validate(); err != nil {
+ return nil, nil, errors.Wrap(err, "validating batching config")
+ }
+
+ return newBatcher(exporter, bh.Batching, c.logger), rawConfig, nil
+}
+
+var _ Factory = (*DriverFactoryWithBatching)(nil)
+
+func NewWithBatchingDriverFactory(underlying Factory, logger logging.Logger) *DriverFactoryWithBatching {
+ return &DriverFactoryWithBatching{
+ underlying: underlying,
+ logger: logger,
+ }
+}
diff --git a/internal/replication/drivers/factory_generated.go b/internal/replication/drivers/factory_generated.go
new file mode 100644
index 0000000000..cae12a3883
--- /dev/null
+++ b/internal/replication/drivers/factory_generated.go
@@ -0,0 +1,58 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: factory.go
+//
+// Generated by this command:
+//
+// mockgen -source factory.go -destination factory_generated.go -package drivers . Factory
+//
+
+// Package drivers is a generated GoMock package.
+package drivers
+
+import (
+ context "context"
+ json "encoding/json"
+ reflect "reflect"
+
+ gomock "go.uber.org/mock/gomock"
+)
+
+// MockFactory is a mock of Factory interface.
+type MockFactory struct {
+ ctrl *gomock.Controller
+ recorder *MockFactoryMockRecorder
+ isgomock struct{}
+}
+
+// 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(ctx context.Context, id string) (Driver, json.RawMessage, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Create", ctx, id)
+ ret0, _ := ret[0].(Driver)
+ ret1, _ := ret[1].(json.RawMessage)
+ ret2, _ := ret[2].(error)
+ return ret0, ret1, ret2
+}
+
+// Create indicates an expected call of Create.
+func (mr *MockFactoryMockRecorder) Create(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockFactory)(nil).Create), ctx, id)
+}
diff --git a/internal/replication/drivers/factory_test.go b/internal/replication/drivers/factory_test.go
new file mode 100644
index 0000000000..385d59930b
--- /dev/null
+++ b/internal/replication/drivers/factory_test.go
@@ -0,0 +1,92 @@
+package drivers
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/formancehq/go-libs/v3/logging"
+
+ "github.com/stretchr/testify/require"
+ "go.uber.org/mock/gomock"
+)
+
+func TestDriverFactoryWithBatching(t *testing.T) {
+ t.Parallel()
+
+ for _, testCase := range []struct {
+ name string
+ config map[string]any
+ expectError string
+ }{
+ {
+ name: "nominal",
+ },
+ {
+ name: "with only maxItems defined for batching",
+ config: map[string]any{
+ "batching": map[string]any{
+ "maxItems": 10,
+ },
+ },
+ },
+ {
+ name: "with only flushInterval defined for batching",
+ config: map[string]any{
+ "batching": map[string]any{
+ "flushInterval": "10ms",
+ },
+ },
+ },
+ {
+ name: "with maxItems and flushInterval defined for batching",
+ config: map[string]any{
+ "batching": map[string]any{
+ "maxItems": 10,
+ "flushInterval": "10ms",
+ },
+ },
+ },
+ {
+ name: "with invalid maxItems defined for batching",
+ config: map[string]any{
+ "batching": map[string]any{
+ "maxItems": -1,
+ },
+ },
+ expectError: "validating batching config: flushBytes must be greater than 0",
+ },
+ {
+ name: "with invalid flushInterval defined for batching",
+ config: map[string]any{
+ "batching": map[string]any{
+ "flushInterval": "-1",
+ },
+ },
+ expectError: "extracting batching config: time: missing unit in duration \"-1\"",
+ },
+ } {
+ testCase := testCase
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ ctrl := gomock.NewController(t)
+
+ rawConfig, _ := json.Marshal(testCase.config)
+
+ underlyingExporterFactory := NewMockFactory(ctrl)
+ underlyingExporterFactory.EXPECT().
+ Create(gomock.Any(), "test").
+ Return(&MockDriver{}, json.RawMessage(rawConfig), nil)
+
+ logger := logging.Testing()
+ f := NewWithBatchingDriverFactory(underlyingExporterFactory, logger)
+ exporter, _, err := f.Create(logging.TestingContext(), "test")
+ if testCase.expectError == "" {
+ require.NoError(t, err)
+ require.NotNil(t, exporter)
+ } else {
+ require.Equal(t, testCase.expectError, err.Error())
+ }
+ })
+ }
+}
diff --git a/internal/replication/drivers/http/driver.go b/internal/replication/drivers/http/driver.go
new file mode 100644
index 0000000000..ff341abfb5
--- /dev/null
+++ b/internal/replication/drivers/http/driver.go
@@ -0,0 +1,84 @@
+package http
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "github.com/formancehq/ledger/internal/replication/config"
+ "net/http"
+ "net/url"
+
+ "github.com/formancehq/go-libs/v3/logging"
+ "github.com/formancehq/ledger/internal/replication/drivers"
+
+ "github.com/pkg/errors"
+)
+
+type Driver struct {
+ config Config
+ httpClient *http.Client
+}
+
+func (c *Driver) Stop(_ context.Context) error {
+ return nil
+}
+
+func (c *Driver) Start(_ context.Context) error {
+ return nil
+}
+
+func (c *Driver) Accept(ctx context.Context, logs ...drivers.LogWithLedger) ([]error, error) {
+ buffer := bytes.NewBufferString("")
+ err := json.NewEncoder(buffer).Encode(logs)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest(http.MethodPost, c.config.URL, buffer)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+
+ rsp, err := c.httpClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ if rsp.StatusCode < 200 || rsp.StatusCode > 299 {
+ return nil, fmt.Errorf("invalid status code, expect something between 200 and 299, got %d", rsp.StatusCode)
+ }
+
+ return make([]error, len(logs)), nil
+}
+
+func NewDriver(config Config, _ logging.Logger) (*Driver, error) {
+ return &Driver{
+ config: config,
+ httpClient: http.DefaultClient,
+ }, nil
+}
+
+var _ drivers.Driver = (*Driver)(nil)
+
+type Config struct {
+ URL string `json:"url"`
+}
+
+func (c Config) Validate() error {
+ if c.URL == "" {
+ return errors.New("empty url")
+ }
+ parsedURL, err := url.Parse(c.URL)
+ if err != nil {
+ return errors.Wrap(err, "failed to parse url")
+ }
+ if parsedURL.Host == "" {
+ return errors.New("invalid url, host, must be defined")
+ }
+
+ return nil
+}
+
+var _ config.Validator = (*Config)(nil)
diff --git a/internal/replication/drivers/http/driver_test.go b/internal/replication/drivers/http/driver_test.go
new file mode 100644
index 0000000000..adc75d7efd
--- /dev/null
+++ b/internal/replication/drivers/http/driver_test.go
@@ -0,0 +1,65 @@
+package http
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ ledger "github.com/formancehq/ledger/internal"
+ "github.com/formancehq/ledger/internal/replication/drivers"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/formancehq/go-libs/v3/logging"
+ "github.com/stretchr/testify/require"
+)
+
+func TestHTTPDriver(t *testing.T) {
+ t.Parallel()
+
+ messages := make(chan []drivers.LogWithLedger, 1)
+ testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ newMessages := make([]drivers.LogWithLedger, 0)
+ require.NoError(t, json.NewDecoder(r.Body).Decode(&newMessages))
+
+ messages <- newMessages
+ }))
+ t.Cleanup(testServer.Close)
+
+ // Create our driver
+ driver, err := NewDriver(Config{
+ URL: testServer.URL,
+ }, logging.Testing())
+ require.NoError(t, err)
+
+ // We will insert numberOfLogs logs split across numberOfModules modules
+ const (
+ numberOfLogs = 50
+ numberOfModules = 2
+ )
+ logs := make([]drivers.LogWithLedger, numberOfLogs)
+ for i := 0; i < numberOfLogs; i++ {
+ logs[i] = drivers.NewLogWithLedger(
+ fmt.Sprintf("module%d", i%numberOfModules),
+ ledger.NewLog(ledger.CreatedTransaction{
+ Transaction: ledger.NewTransaction(),
+ }),
+ )
+ }
+
+ // Send all logs to the driver
+ itemsErrors, err := driver.Accept(context.TODO(), logs...)
+ require.NoError(t, err)
+ require.Len(t, itemsErrors, numberOfLogs)
+ for index := range logs {
+ require.Nil(t, itemsErrors[index])
+ }
+
+ // Ensure data has been inserted
+ select {
+ case receivedMessages := <-messages:
+ require.Len(t, receivedMessages, numberOfLogs)
+ default:
+ require.Fail(t, fmt.Sprintf("should have received %d messages", numberOfLogs))
+ }
+}
diff --git a/internal/replication/drivers/log.go b/internal/replication/drivers/log.go
new file mode 100644
index 0000000000..cd701fbaa4
--- /dev/null
+++ b/internal/replication/drivers/log.go
@@ -0,0 +1,17 @@
+package drivers
+
+import (
+ ledger "github.com/formancehq/ledger/internal"
+)
+
+type LogWithLedger struct {
+ ledger.Log
+ Ledger string
+}
+
+func NewLogWithLedger(ledger string, log ledger.Log) LogWithLedger {
+ return LogWithLedger{
+ Log: log,
+ Ledger: ledger,
+ }
+}
diff --git a/internal/replication/drivers/module.go b/internal/replication/drivers/module.go
new file mode 100644
index 0000000000..9433588893
--- /dev/null
+++ b/internal/replication/drivers/module.go
@@ -0,0 +1,16 @@
+package drivers
+
+import (
+ "github.com/formancehq/ledger/internal/storage/system"
+ "go.uber.org/fx"
+)
+
+// NewFXModule create a new fx module
+func NewFXModule() fx.Option {
+ return fx.Options(
+ fx.Provide(func(store *system.DefaultStore) Store {
+ return store
+ }),
+ fx.Provide(NewRegistry),
+ )
+}
diff --git a/internal/replication/drivers/noop/driver.go b/internal/replication/drivers/noop/driver.go
new file mode 100644
index 0000000000..bc0cd224a9
--- /dev/null
+++ b/internal/replication/drivers/noop/driver.go
@@ -0,0 +1,32 @@
+package noop
+
+import (
+ "context"
+
+ "github.com/formancehq/go-libs/v3/logging"
+ "github.com/formancehq/ledger/internal/replication/drivers"
+)
+
+type Driver struct{}
+
+func (driver *Driver) Stop(_ context.Context) error {
+ return nil
+}
+
+func (driver *Driver) Start(_ context.Context) error {
+ return nil
+}
+
+func (driver *Driver) ClearData(_ context.Context, _ string) error {
+ return nil
+}
+
+func (driver *Driver) Accept(_ context.Context, logs ...drivers.LogWithLedger) ([]error, error) {
+ return make([]error, len(logs)), nil
+}
+
+func NewDriver(_ struct{}, _ logging.Logger) (*Driver, error) {
+ return &Driver{}, nil
+}
+
+var _ drivers.Driver = (*Driver)(nil)
diff --git a/internal/replication/drivers/noop/driver_test.go b/internal/replication/drivers/noop/driver_test.go
new file mode 100644
index 0000000000..1eee557c89
--- /dev/null
+++ b/internal/replication/drivers/noop/driver_test.go
@@ -0,0 +1,48 @@
+package noop
+
+import (
+ "context"
+ "fmt"
+ "github.com/formancehq/go-libs/v3/logging"
+ ledger "github.com/formancehq/ledger/internal"
+ "github.com/formancehq/ledger/internal/replication/drivers"
+ "github.com/stretchr/testify/require"
+ "testing"
+)
+
+func TestNoOpDriver(t *testing.T) {
+ t.Parallel()
+
+ ctx := context.TODO()
+
+ // Create our driver
+ driver, err := NewDriver(struct{}{}, logging.Testing())
+ require.NoError(t, err)
+ require.NoError(t, driver.Start(ctx))
+ t.Cleanup(func() {
+ require.NoError(t, driver.Stop(ctx))
+ })
+
+ // We will insert numberOfLogs logs split across numberOfModules modules
+ const (
+ numberOfLogs = 50
+ numberOfModules = 2
+ )
+ logs := make([]drivers.LogWithLedger, numberOfLogs)
+ for i := 0; i < numberOfLogs; i++ {
+ logs[i] = drivers.NewLogWithLedger(
+ fmt.Sprintf("module%d", i%numberOfModules),
+ ledger.NewLog(ledger.CreatedTransaction{
+ Transaction: ledger.NewTransaction(),
+ }),
+ )
+ }
+
+ // Send all logs to the driver
+ itemsErrors, err := driver.Accept(ctx, logs...)
+ require.NoError(t, err)
+ require.Len(t, itemsErrors, numberOfLogs)
+ for index := range logs {
+ require.Nil(t, itemsErrors[index])
+ }
+}
diff --git a/internal/replication/drivers/registry.go b/internal/replication/drivers/registry.go
new file mode 100644
index 0000000000..1a60b82301
--- /dev/null
+++ b/internal/replication/drivers/registry.go
@@ -0,0 +1,156 @@
+package drivers
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "github.com/formancehq/ledger/internal/replication/config"
+ "github.com/formancehq/ledger/internal/storage/common"
+ "reflect"
+
+ "github.com/formancehq/go-libs/v3/logging"
+
+ "github.com/pkg/errors"
+)
+
+// Registry holds all available drivers
+// It implements Factory
+type Registry struct {
+ constructors map[string]any
+ logger logging.Logger
+ store Store
+}
+
+func (c *Registry) RegisterDriver(name string, constructor any) {
+ if err := c.registerDriver(name, constructor); err != nil {
+ panic(err)
+ }
+}
+
+func (c *Registry) registerDriver(name string, constructor any) error {
+ typeOfConstructor := reflect.TypeOf(constructor)
+ if typeOfConstructor.Kind() != reflect.Func {
+ return errors.New("constructor must be a func")
+ }
+
+ if typeOfConstructor.NumIn() != 2 {
+ return errors.New("constructor must take two parameters")
+ }
+
+ if typeOfConstructor.NumOut() != 2 {
+ return errors.New("constructor must return two values")
+ }
+
+ if !typeOfConstructor.In(1).AssignableTo(reflect.TypeOf(new(logging.Logger)).Elem()) {
+ return fmt.Errorf("constructor arg 2 must be of kind %s", reflect.TypeOf(new(logging.Logger)).Elem().String())
+ }
+
+ errorType := reflect.TypeOf(new(error)).Elem()
+ if !typeOfConstructor.Out(1).AssignableTo(errorType) {
+ return fmt.Errorf("return 1 must be of kind %s", errorType.String())
+ }
+
+ driverType := reflect.TypeOf(new(Driver)).Elem()
+ if !typeOfConstructor.Out(0).AssignableTo(driverType) {
+ return fmt.Errorf("return 0 must be of kind %s", driverType.String())
+ }
+
+ c.constructors[name] = constructor
+
+ return nil
+}
+
+func (c *Registry) extractConfigType(constructor any) any {
+ return reflect.New(reflect.TypeOf(constructor).In(0)).Interface()
+}
+
+func (c *Registry) Create(ctx context.Context, id string) (Driver, json.RawMessage, error) {
+ exporter, err := c.store.GetExporter(ctx, id)
+ if err != nil {
+ switch {
+ case errors.Is(err, common.ErrNotFound):
+ return nil, nil, NewErrExporterNotFound(id)
+ default:
+ return nil, nil, err
+ }
+ }
+
+ driverConstructor, ok := c.constructors[exporter.Driver]
+ if !ok {
+ return nil, nil, fmt.Errorf("cannot build exporter '%s', not exists", id)
+ }
+ driverConfig := c.extractConfigType(driverConstructor)
+
+ if err := json.Unmarshal(exporter.Config, driverConfig); err != nil {
+ return nil, nil, err
+ }
+
+ if v, ok := driverConfig.(config.Defaulter); ok {
+ v.SetDefaults()
+ }
+
+ ret := reflect.ValueOf(driverConstructor).Call([]reflect.Value{
+ reflect.ValueOf(driverConfig).Elem(),
+ reflect.ValueOf(c.logger),
+ })
+ if !ret[1].IsZero() {
+ return nil, nil, ret[1].Interface().(error)
+ }
+
+ return ret[0].Interface().(Driver), exporter.Config, nil
+}
+
+func (c *Registry) GetConfigType(driverName string) (any, error) {
+ driverConstructor, ok := c.constructors[driverName]
+ if !ok {
+ return nil, NewErrDriverNotFound(driverName)
+ }
+ return c.extractConfigType(driverConstructor), nil
+}
+
+func (c *Registry) ValidateConfig(driverName string, rawDriverConfig json.RawMessage) error {
+
+ driverConfig, err := c.GetConfigType(driverName)
+ if err != nil {
+ return errors.Wrapf(err, "validating config for exporter '%s'", driverName)
+ }
+
+ if err := json.Unmarshal(rawDriverConfig, driverConfig); err != nil {
+ return NewErrMalformedConfiguration(driverName, err)
+ }
+ if v, ok := driverConfig.(config.Defaulter); ok {
+ v.SetDefaults()
+ }
+ if v, ok := driverConfig.(config.Validator); ok {
+ if err := v.Validate(); err != nil {
+ return NewErrInvalidConfiguration(driverName, err)
+ }
+ }
+
+ type batchingHolder struct {
+ Batching Batching `json:"batching"`
+ }
+
+ bh := batchingHolder{}
+ if err := json.Unmarshal(rawDriverConfig, &bh); err != nil {
+ return NewErrMalformedConfiguration(driverName, err)
+ }
+
+ bh.Batching.SetDefaults()
+
+ if err := bh.Batching.Validate(); err != nil {
+ return NewErrInvalidConfiguration(driverName, err)
+ }
+
+ return nil
+}
+
+func NewRegistry(logger logging.Logger, store Store) *Registry {
+ return &Registry{
+ constructors: map[string]any{},
+ logger: logger,
+ store: store,
+ }
+}
+
+var _ Factory = (*Registry)(nil)
diff --git a/internal/replication/drivers/registry_test.go b/internal/replication/drivers/registry_test.go
new file mode 100644
index 0000000000..cfebd40afb
--- /dev/null
+++ b/internal/replication/drivers/registry_test.go
@@ -0,0 +1,86 @@
+package drivers
+
+import (
+ "testing"
+
+ "go.uber.org/mock/gomock"
+
+ "github.com/formancehq/go-libs/v3/logging"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestRegisterDriver(t *testing.T) {
+ t.Parallel()
+
+ type testCase struct {
+ name string
+ fn any
+ expectError string
+ }
+
+ for _, testCase := range []testCase{
+ {
+ name: "nominal",
+ fn: func(_ struct{}, _ logging.Logger) (*MockDriver, error) {
+ return &MockDriver{}, nil
+ },
+ },
+ {
+ name: "invalid third arg",
+ fn: func(_ struct{}, _ struct{}) (*MockDriver, error) {
+ return &MockDriver{}, nil
+ },
+ expectError: "constructor arg 2 must be of kind logging.Logger",
+ },
+ {
+ name: "invalid first return",
+ fn: func(_ struct{}, _ logging.Logger) (struct{}, error) {
+ return struct{}{}, nil
+ },
+ expectError: "return 0 must be of kind drivers.Driver",
+ },
+ {
+ name: "invalid second return",
+ fn: func(_ struct{}, _ logging.Logger) (*MockDriver, string) {
+ return &MockDriver{}, ""
+ },
+ expectError: "return 1 must be of kind error",
+ },
+ {
+ name: "invalid number of parameters",
+ fn: func() (*MockDriver, string) {
+ return &MockDriver{}, ""
+ },
+ expectError: "constructor must take two parameters",
+ },
+ {
+ name: "invalid number of returned values",
+ fn: func(_ struct{}, _ logging.Logger) *MockDriver {
+ return &MockDriver{}
+ },
+ expectError: "constructor must return two values",
+ },
+ {
+ name: "invalid constructor type",
+ fn: "foo",
+ expectError: "constructor must be a func",
+ },
+ } {
+ testCase := testCase
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ ctrl := gomock.NewController(t)
+ mockStore := NewMockStore(ctrl)
+
+ exporterRegistry := NewRegistry(logging.Testing(), mockStore)
+ err := exporterRegistry.registerDriver("testing", testCase.fn)
+ if testCase.expectError == "" {
+ require.NoError(t, err)
+ } else {
+ require.Equal(t, testCase.expectError, err.Error())
+ }
+ })
+ }
+}
diff --git a/internal/replication/drivers/stdout/driver.go b/internal/replication/drivers/stdout/driver.go
new file mode 100644
index 0000000000..8a84baa174
--- /dev/null
+++ b/internal/replication/drivers/stdout/driver.go
@@ -0,0 +1,48 @@
+package stdout
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+
+ "github.com/formancehq/go-libs/v3/logging"
+ "github.com/formancehq/ledger/internal/replication/drivers"
+)
+
+type Driver struct {
+ output io.Writer
+}
+
+func (driver *Driver) Stop(_ context.Context) error {
+ return nil
+}
+
+func (driver *Driver) Start(_ context.Context) error {
+ return nil
+}
+
+func (driver *Driver) ClearData(_ context.Context, _ string) error {
+ return nil
+}
+
+func (driver *Driver) Accept(_ context.Context, logs ...drivers.LogWithLedger) ([]error, error) {
+ for _, log := range logs {
+ data, err := json.MarshalIndent(log, "", " ")
+ if err != nil {
+ return nil, err
+ }
+ _, _ = fmt.Fprintln(driver.output, string(data))
+ }
+
+ return make([]error, len(logs)), nil
+}
+
+func NewDriver(_ struct{}, _ logging.Logger) (*Driver, error) {
+ return &Driver{
+ output: os.Stdout,
+ }, nil
+}
+
+var _ drivers.Driver = (*Driver)(nil)
diff --git a/internal/replication/drivers/stdout/driver_test.go b/internal/replication/drivers/stdout/driver_test.go
new file mode 100644
index 0000000000..8562952cfe
--- /dev/null
+++ b/internal/replication/drivers/stdout/driver_test.go
@@ -0,0 +1,51 @@
+package stdout
+
+import (
+ "context"
+ "fmt"
+ "github.com/formancehq/go-libs/v3/logging"
+ ledger "github.com/formancehq/ledger/internal"
+ "github.com/formancehq/ledger/internal/replication/drivers"
+ "github.com/stretchr/testify/require"
+ "io"
+ "testing"
+)
+
+func TestStdoutDriver(t *testing.T) {
+ t.Parallel()
+
+ ctx := context.TODO()
+
+ // Create our driver
+ driver, err := NewDriver(struct{}{}, logging.Testing())
+ require.NoError(t, err)
+ driver.output = io.Discard
+
+ require.NoError(t, driver.Start(ctx))
+ t.Cleanup(func() {
+ require.NoError(t, driver.Stop(ctx))
+ })
+
+ // We will insert numberOfLogs logs split across numberOfModules modules
+ const (
+ numberOfLogs = 50
+ numberOfModules = 2
+ )
+ logs := make([]drivers.LogWithLedger, numberOfLogs)
+ for i := 0; i < numberOfLogs; i++ {
+ logs[i] = drivers.NewLogWithLedger(
+ fmt.Sprintf("module%d", i%numberOfModules),
+ ledger.NewLog(ledger.CreatedTransaction{
+ Transaction: ledger.NewTransaction(),
+ }),
+ )
+ }
+
+ // Send all logs to the driver
+ itemsErrors, err := driver.Accept(ctx, logs...)
+ require.NoError(t, err)
+ require.Len(t, itemsErrors, numberOfLogs)
+ for index := range logs {
+ require.Nil(t, itemsErrors[index])
+ }
+}
diff --git a/internal/replication/drivers/store.go b/internal/replication/drivers/store.go
new file mode 100644
index 0000000000..14137815e6
--- /dev/null
+++ b/internal/replication/drivers/store.go
@@ -0,0 +1,11 @@
+package drivers
+
+import (
+ "context"
+ ledger "github.com/formancehq/ledger/internal"
+)
+
+//go:generate mockgen -source store.go -destination store_generated.go -package drivers . Store
+type Store interface {
+ GetExporter(ctx context.Context, id string) (*ledger.Exporter, error)
+}
diff --git a/internal/replication/drivers/store_generated.go b/internal/replication/drivers/store_generated.go
new file mode 100644
index 0000000000..0e7c005abb
--- /dev/null
+++ b/internal/replication/drivers/store_generated.go
@@ -0,0 +1,57 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: store.go
+//
+// Generated by this command:
+//
+// mockgen -source store.go -destination store_generated.go -package drivers . Store
+//
+
+// Package drivers is a generated GoMock package.
+package drivers
+
+import (
+ context "context"
+ reflect "reflect"
+
+ ledger "github.com/formancehq/ledger/internal"
+ gomock "go.uber.org/mock/gomock"
+)
+
+// MockStore is a mock of Store interface.
+type MockStore struct {
+ ctrl *gomock.Controller
+ recorder *MockStoreMockRecorder
+ isgomock struct{}
+}
+
+// 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
+}
+
+// GetExporter mocks base method.
+func (m *MockStore) GetExporter(ctx context.Context, id string) (*ledger.Exporter, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetExporter", ctx, id)
+ ret0, _ := ret[0].(*ledger.Exporter)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetExporter indicates an expected call of GetExporter.
+func (mr *MockStoreMockRecorder) GetExporter(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetExporter", reflect.TypeOf((*MockStore)(nil).GetExporter), ctx, id)
+}
diff --git a/internal/replication/exporters.go b/internal/replication/exporters.go
new file mode 100644
index 0000000000..b7491e768e
--- /dev/null
+++ b/internal/replication/exporters.go
@@ -0,0 +1,8 @@
+package replication
+
+import "encoding/json"
+
+//go:generate mockgen -source exporters.go -destination exporters_generated.go -package replication . ConfigValidator
+type ConfigValidator interface {
+ ValidateConfig(exporterName string, rawExporterConfig json.RawMessage) error
+}
diff --git a/internal/replication/exporters_generated.go b/internal/replication/exporters_generated.go
new file mode 100644
index 0000000000..602bd0f61d
--- /dev/null
+++ b/internal/replication/exporters_generated.go
@@ -0,0 +1,55 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: exporters.go
+//
+// Generated by this command:
+//
+// mockgen -source exporters.go -destination exporters_generated.go -package replication . ConfigValidator
+//
+
+// Package replication is a generated GoMock package.
+package replication
+
+import (
+ json "encoding/json"
+ reflect "reflect"
+
+ gomock "go.uber.org/mock/gomock"
+)
+
+// MockConfigValidator is a mock of ConfigValidator interface.
+type MockConfigValidator struct {
+ ctrl *gomock.Controller
+ recorder *MockConfigValidatorMockRecorder
+ isgomock struct{}
+}
+
+// MockConfigValidatorMockRecorder is the mock recorder for MockConfigValidator.
+type MockConfigValidatorMockRecorder struct {
+ mock *MockConfigValidator
+}
+
+// NewMockConfigValidator creates a new mock instance.
+func NewMockConfigValidator(ctrl *gomock.Controller) *MockConfigValidator {
+ mock := &MockConfigValidator{ctrl: ctrl}
+ mock.recorder = &MockConfigValidatorMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockConfigValidator) EXPECT() *MockConfigValidatorMockRecorder {
+ return m.recorder
+}
+
+// ValidateConfig mocks base method.
+func (m *MockConfigValidator) ValidateConfig(exporterName string, rawExporterConfig json.RawMessage) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ValidateConfig", exporterName, rawExporterConfig)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// ValidateConfig indicates an expected call of ValidateConfig.
+func (mr *MockConfigValidatorMockRecorder) ValidateConfig(exporterName, rawExporterConfig any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateConfig", reflect.TypeOf((*MockConfigValidator)(nil).ValidateConfig), exporterName, rawExporterConfig)
+}
diff --git a/internal/replication/grpc/replication_service.pb.go b/internal/replication/grpc/replication_service.pb.go
new file mode 100644
index 0000000000..27fd057bf5
--- /dev/null
+++ b/internal/replication/grpc/replication_service.pb.go
@@ -0,0 +1,1471 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.36.6
+// protoc v5.27.5
+// source: internal/replication/grpc/replication_service.proto
+
+package grpc
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ timestamppb "google.golang.org/protobuf/types/known/timestamppb"
+ reflect "reflect"
+ sync "sync"
+ unsafe "unsafe"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type Cursor struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Next string `protobuf:"bytes,1,opt,name=next,proto3" json:"next,omitempty"`
+ HasMore bool `protobuf:"varint,2,opt,name=has_more,json=hasMore,proto3" json:"has_more,omitempty"`
+ Prev string `protobuf:"bytes,3,opt,name=prev,proto3" json:"prev,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *Cursor) Reset() {
+ *x = Cursor{}
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *Cursor) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Cursor) ProtoMessage() {}
+
+func (x *Cursor) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[0]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use Cursor.ProtoReflect.Descriptor instead.
+func (*Cursor) Descriptor() ([]byte, []int) {
+ return file_internal_replication_grpc_replication_service_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *Cursor) GetNext() string {
+ if x != nil {
+ return x.Next
+ }
+ return ""
+}
+
+func (x *Cursor) GetHasMore() bool {
+ if x != nil {
+ return x.HasMore
+ }
+ return false
+}
+
+func (x *Cursor) GetPrev() string {
+ if x != nil {
+ return x.Prev
+ }
+ return ""
+}
+
+type ListExportersRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Cursor string `protobuf:"bytes,1,opt,name=cursor,proto3" json:"cursor,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ListExportersRequest) Reset() {
+ *x = ListExportersRequest{}
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ListExportersRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListExportersRequest) ProtoMessage() {}
+
+func (x *ListExportersRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[1]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListExportersRequest.ProtoReflect.Descriptor instead.
+func (*ListExportersRequest) Descriptor() ([]byte, []int) {
+ return file_internal_replication_grpc_replication_service_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *ListExportersRequest) GetCursor() string {
+ if x != nil {
+ return x.Cursor
+ }
+ return ""
+}
+
+type ListExportersResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Data []*Exporter `protobuf:"bytes,1,rep,name=data,proto3" json:"data,omitempty"`
+ Cursor *Cursor `protobuf:"bytes,2,opt,name=cursor,proto3" json:"cursor,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ListExportersResponse) Reset() {
+ *x = ListExportersResponse{}
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ListExportersResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListExportersResponse) ProtoMessage() {}
+
+func (x *ListExportersResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[2]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListExportersResponse.ProtoReflect.Descriptor instead.
+func (*ListExportersResponse) Descriptor() ([]byte, []int) {
+ return file_internal_replication_grpc_replication_service_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *ListExportersResponse) GetData() []*Exporter {
+ if x != nil {
+ return x.Data
+ }
+ return nil
+}
+
+func (x *ListExportersResponse) GetCursor() *Cursor {
+ if x != nil {
+ return x.Cursor
+ }
+ return nil
+}
+
+type Exporter struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
+ CreatedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
+ Config *ExporterConfiguration `protobuf:"bytes,3,opt,name=config,proto3" json:"config,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *Exporter) Reset() {
+ *x = Exporter{}
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *Exporter) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Exporter) ProtoMessage() {}
+
+func (x *Exporter) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[3]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use Exporter.ProtoReflect.Descriptor instead.
+func (*Exporter) Descriptor() ([]byte, []int) {
+ return file_internal_replication_grpc_replication_service_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *Exporter) GetId() string {
+ if x != nil {
+ return x.Id
+ }
+ return ""
+}
+
+func (x *Exporter) GetCreatedAt() *timestamppb.Timestamp {
+ if x != nil {
+ return x.CreatedAt
+ }
+ return nil
+}
+
+func (x *Exporter) GetConfig() *ExporterConfiguration {
+ if x != nil {
+ return x.Config
+ }
+ return nil
+}
+
+type GetExporterRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *GetExporterRequest) Reset() {
+ *x = GetExporterRequest{}
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[4]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *GetExporterRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetExporterRequest) ProtoMessage() {}
+
+func (x *GetExporterRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[4]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetExporterRequest.ProtoReflect.Descriptor instead.
+func (*GetExporterRequest) Descriptor() ([]byte, []int) {
+ return file_internal_replication_grpc_replication_service_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *GetExporterRequest) GetId() string {
+ if x != nil {
+ return x.Id
+ }
+ return ""
+}
+
+type GetExporterResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Exporter *Exporter `protobuf:"bytes,1,opt,name=exporter,proto3" json:"exporter,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *GetExporterResponse) Reset() {
+ *x = GetExporterResponse{}
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[5]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *GetExporterResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetExporterResponse) ProtoMessage() {}
+
+func (x *GetExporterResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[5]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetExporterResponse.ProtoReflect.Descriptor instead.
+func (*GetExporterResponse) Descriptor() ([]byte, []int) {
+ return file_internal_replication_grpc_replication_service_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *GetExporterResponse) GetExporter() *Exporter {
+ if x != nil {
+ return x.Exporter
+ }
+ return nil
+}
+
+type DeleteExporterRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *DeleteExporterRequest) Reset() {
+ *x = DeleteExporterRequest{}
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[6]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *DeleteExporterRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DeleteExporterRequest) ProtoMessage() {}
+
+func (x *DeleteExporterRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[6]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use DeleteExporterRequest.ProtoReflect.Descriptor instead.
+func (*DeleteExporterRequest) Descriptor() ([]byte, []int) {
+ return file_internal_replication_grpc_replication_service_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *DeleteExporterRequest) GetId() string {
+ if x != nil {
+ return x.Id
+ }
+ return ""
+}
+
+type DeleteExporterResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *DeleteExporterResponse) Reset() {
+ *x = DeleteExporterResponse{}
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[7]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *DeleteExporterResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DeleteExporterResponse) ProtoMessage() {}
+
+func (x *DeleteExporterResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[7]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use DeleteExporterResponse.ProtoReflect.Descriptor instead.
+func (*DeleteExporterResponse) Descriptor() ([]byte, []int) {
+ return file_internal_replication_grpc_replication_service_proto_rawDescGZIP(), []int{7}
+}
+
+type ExporterConfiguration struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Driver string `protobuf:"bytes,1,opt,name=driver,proto3" json:"driver,omitempty"`
+ Config string `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ExporterConfiguration) Reset() {
+ *x = ExporterConfiguration{}
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[8]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ExporterConfiguration) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ExporterConfiguration) ProtoMessage() {}
+
+func (x *ExporterConfiguration) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[8]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ExporterConfiguration.ProtoReflect.Descriptor instead.
+func (*ExporterConfiguration) Descriptor() ([]byte, []int) {
+ return file_internal_replication_grpc_replication_service_proto_rawDescGZIP(), []int{8}
+}
+
+func (x *ExporterConfiguration) GetDriver() string {
+ if x != nil {
+ return x.Driver
+ }
+ return ""
+}
+
+func (x *ExporterConfiguration) GetConfig() string {
+ if x != nil {
+ return x.Config
+ }
+ return ""
+}
+
+type CreateExporterRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Config *ExporterConfiguration `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *CreateExporterRequest) Reset() {
+ *x = CreateExporterRequest{}
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[9]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *CreateExporterRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CreateExporterRequest) ProtoMessage() {}
+
+func (x *CreateExporterRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[9]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CreateExporterRequest.ProtoReflect.Descriptor instead.
+func (*CreateExporterRequest) Descriptor() ([]byte, []int) {
+ return file_internal_replication_grpc_replication_service_proto_rawDescGZIP(), []int{9}
+}
+
+func (x *CreateExporterRequest) GetConfig() *ExporterConfiguration {
+ if x != nil {
+ return x.Config
+ }
+ return nil
+}
+
+type CreateExporterResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Exporter *Exporter `protobuf:"bytes,1,opt,name=exporter,proto3" json:"exporter,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *CreateExporterResponse) Reset() {
+ *x = CreateExporterResponse{}
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[10]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *CreateExporterResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CreateExporterResponse) ProtoMessage() {}
+
+func (x *CreateExporterResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[10]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CreateExporterResponse.ProtoReflect.Descriptor instead.
+func (*CreateExporterResponse) Descriptor() ([]byte, []int) {
+ return file_internal_replication_grpc_replication_service_proto_rawDescGZIP(), []int{10}
+}
+
+func (x *CreateExporterResponse) GetExporter() *Exporter {
+ if x != nil {
+ return x.Exporter
+ }
+ return nil
+}
+
+type ListPipelinesRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Cursor string `protobuf:"bytes,1,opt,name=cursor,proto3" json:"cursor,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ListPipelinesRequest) Reset() {
+ *x = ListPipelinesRequest{}
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[11]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ListPipelinesRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListPipelinesRequest) ProtoMessage() {}
+
+func (x *ListPipelinesRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[11]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListPipelinesRequest.ProtoReflect.Descriptor instead.
+func (*ListPipelinesRequest) Descriptor() ([]byte, []int) {
+ return file_internal_replication_grpc_replication_service_proto_rawDescGZIP(), []int{11}
+}
+
+func (x *ListPipelinesRequest) GetCursor() string {
+ if x != nil {
+ return x.Cursor
+ }
+ return ""
+}
+
+type ListPipelinesResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Data []*Pipeline `protobuf:"bytes,1,rep,name=data,proto3" json:"data,omitempty"`
+ Cursor *Cursor `protobuf:"bytes,2,opt,name=cursor,proto3" json:"cursor,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ListPipelinesResponse) Reset() {
+ *x = ListPipelinesResponse{}
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[12]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ListPipelinesResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListPipelinesResponse) ProtoMessage() {}
+
+func (x *ListPipelinesResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[12]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListPipelinesResponse.ProtoReflect.Descriptor instead.
+func (*ListPipelinesResponse) Descriptor() ([]byte, []int) {
+ return file_internal_replication_grpc_replication_service_proto_rawDescGZIP(), []int{12}
+}
+
+func (x *ListPipelinesResponse) GetData() []*Pipeline {
+ if x != nil {
+ return x.Data
+ }
+ return nil
+}
+
+func (x *ListPipelinesResponse) GetCursor() *Cursor {
+ if x != nil {
+ return x.Cursor
+ }
+ return nil
+}
+
+type PipelineConfiguration struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ ExporterId string `protobuf:"bytes,1,opt,name=exporter_id,json=exporterId,proto3" json:"exporter_id,omitempty"`
+ Ledger string `protobuf:"bytes,2,opt,name=ledger,proto3" json:"ledger,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *PipelineConfiguration) Reset() {
+ *x = PipelineConfiguration{}
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[13]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *PipelineConfiguration) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PipelineConfiguration) ProtoMessage() {}
+
+func (x *PipelineConfiguration) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[13]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use PipelineConfiguration.ProtoReflect.Descriptor instead.
+func (*PipelineConfiguration) Descriptor() ([]byte, []int) {
+ return file_internal_replication_grpc_replication_service_proto_rawDescGZIP(), []int{13}
+}
+
+func (x *PipelineConfiguration) GetExporterId() string {
+ if x != nil {
+ return x.ExporterId
+ }
+ return ""
+}
+
+func (x *PipelineConfiguration) GetLedger() string {
+ if x != nil {
+ return x.Ledger
+ }
+ return ""
+}
+
+type Pipeline struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Config *PipelineConfiguration `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"`
+ CreatedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=createdAt,proto3" json:"createdAt,omitempty"`
+ Id string `protobuf:"bytes,3,opt,name=id,proto3" json:"id,omitempty"`
+ Enabled bool `protobuf:"varint,4,opt,name=enabled,proto3" json:"enabled,omitempty"`
+ LastLogID *uint64 `protobuf:"varint,5,opt,name=lastLogID,proto3,oneof" json:"lastLogID,omitempty"`
+ Error string `protobuf:"bytes,6,opt,name=error,proto3" json:"error,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *Pipeline) Reset() {
+ *x = Pipeline{}
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[14]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *Pipeline) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Pipeline) ProtoMessage() {}
+
+func (x *Pipeline) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[14]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use Pipeline.ProtoReflect.Descriptor instead.
+func (*Pipeline) Descriptor() ([]byte, []int) {
+ return file_internal_replication_grpc_replication_service_proto_rawDescGZIP(), []int{14}
+}
+
+func (x *Pipeline) GetConfig() *PipelineConfiguration {
+ if x != nil {
+ return x.Config
+ }
+ return nil
+}
+
+func (x *Pipeline) GetCreatedAt() *timestamppb.Timestamp {
+ if x != nil {
+ return x.CreatedAt
+ }
+ return nil
+}
+
+func (x *Pipeline) GetId() string {
+ if x != nil {
+ return x.Id
+ }
+ return ""
+}
+
+func (x *Pipeline) GetEnabled() bool {
+ if x != nil {
+ return x.Enabled
+ }
+ return false
+}
+
+func (x *Pipeline) GetLastLogID() uint64 {
+ if x != nil && x.LastLogID != nil {
+ return *x.LastLogID
+ }
+ return 0
+}
+
+func (x *Pipeline) GetError() string {
+ if x != nil {
+ return x.Error
+ }
+ return ""
+}
+
+type GetPipelineRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *GetPipelineRequest) Reset() {
+ *x = GetPipelineRequest{}
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[15]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *GetPipelineRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetPipelineRequest) ProtoMessage() {}
+
+func (x *GetPipelineRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[15]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetPipelineRequest.ProtoReflect.Descriptor instead.
+func (*GetPipelineRequest) Descriptor() ([]byte, []int) {
+ return file_internal_replication_grpc_replication_service_proto_rawDescGZIP(), []int{15}
+}
+
+func (x *GetPipelineRequest) GetId() string {
+ if x != nil {
+ return x.Id
+ }
+ return ""
+}
+
+type GetPipelineResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Pipeline *Pipeline `protobuf:"bytes,1,opt,name=pipeline,proto3" json:"pipeline,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *GetPipelineResponse) Reset() {
+ *x = GetPipelineResponse{}
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[16]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *GetPipelineResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetPipelineResponse) ProtoMessage() {}
+
+func (x *GetPipelineResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[16]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetPipelineResponse.ProtoReflect.Descriptor instead.
+func (*GetPipelineResponse) Descriptor() ([]byte, []int) {
+ return file_internal_replication_grpc_replication_service_proto_rawDescGZIP(), []int{16}
+}
+
+func (x *GetPipelineResponse) GetPipeline() *Pipeline {
+ if x != nil {
+ return x.Pipeline
+ }
+ return nil
+}
+
+type CreatePipelineRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Config *PipelineConfiguration `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *CreatePipelineRequest) Reset() {
+ *x = CreatePipelineRequest{}
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[17]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *CreatePipelineRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CreatePipelineRequest) ProtoMessage() {}
+
+func (x *CreatePipelineRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[17]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CreatePipelineRequest.ProtoReflect.Descriptor instead.
+func (*CreatePipelineRequest) Descriptor() ([]byte, []int) {
+ return file_internal_replication_grpc_replication_service_proto_rawDescGZIP(), []int{17}
+}
+
+func (x *CreatePipelineRequest) GetConfig() *PipelineConfiguration {
+ if x != nil {
+ return x.Config
+ }
+ return nil
+}
+
+type CreatePipelineResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Pipeline *Pipeline `protobuf:"bytes,1,opt,name=pipeline,proto3" json:"pipeline,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *CreatePipelineResponse) Reset() {
+ *x = CreatePipelineResponse{}
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[18]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *CreatePipelineResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CreatePipelineResponse) ProtoMessage() {}
+
+func (x *CreatePipelineResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[18]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CreatePipelineResponse.ProtoReflect.Descriptor instead.
+func (*CreatePipelineResponse) Descriptor() ([]byte, []int) {
+ return file_internal_replication_grpc_replication_service_proto_rawDescGZIP(), []int{18}
+}
+
+func (x *CreatePipelineResponse) GetPipeline() *Pipeline {
+ if x != nil {
+ return x.Pipeline
+ }
+ return nil
+}
+
+type DeletePipelineRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *DeletePipelineRequest) Reset() {
+ *x = DeletePipelineRequest{}
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[19]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *DeletePipelineRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DeletePipelineRequest) ProtoMessage() {}
+
+func (x *DeletePipelineRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[19]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use DeletePipelineRequest.ProtoReflect.Descriptor instead.
+func (*DeletePipelineRequest) Descriptor() ([]byte, []int) {
+ return file_internal_replication_grpc_replication_service_proto_rawDescGZIP(), []int{19}
+}
+
+func (x *DeletePipelineRequest) GetId() string {
+ if x != nil {
+ return x.Id
+ }
+ return ""
+}
+
+type DeletePipelineResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *DeletePipelineResponse) Reset() {
+ *x = DeletePipelineResponse{}
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[20]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *DeletePipelineResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DeletePipelineResponse) ProtoMessage() {}
+
+func (x *DeletePipelineResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[20]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use DeletePipelineResponse.ProtoReflect.Descriptor instead.
+func (*DeletePipelineResponse) Descriptor() ([]byte, []int) {
+ return file_internal_replication_grpc_replication_service_proto_rawDescGZIP(), []int{20}
+}
+
+type StartPipelineRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *StartPipelineRequest) Reset() {
+ *x = StartPipelineRequest{}
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[21]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *StartPipelineRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*StartPipelineRequest) ProtoMessage() {}
+
+func (x *StartPipelineRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[21]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use StartPipelineRequest.ProtoReflect.Descriptor instead.
+func (*StartPipelineRequest) Descriptor() ([]byte, []int) {
+ return file_internal_replication_grpc_replication_service_proto_rawDescGZIP(), []int{21}
+}
+
+func (x *StartPipelineRequest) GetId() string {
+ if x != nil {
+ return x.Id
+ }
+ return ""
+}
+
+type StartPipelineResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *StartPipelineResponse) Reset() {
+ *x = StartPipelineResponse{}
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[22]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *StartPipelineResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*StartPipelineResponse) ProtoMessage() {}
+
+func (x *StartPipelineResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[22]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use StartPipelineResponse.ProtoReflect.Descriptor instead.
+func (*StartPipelineResponse) Descriptor() ([]byte, []int) {
+ return file_internal_replication_grpc_replication_service_proto_rawDescGZIP(), []int{22}
+}
+
+type StopPipelineRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *StopPipelineRequest) Reset() {
+ *x = StopPipelineRequest{}
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[23]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *StopPipelineRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*StopPipelineRequest) ProtoMessage() {}
+
+func (x *StopPipelineRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[23]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use StopPipelineRequest.ProtoReflect.Descriptor instead.
+func (*StopPipelineRequest) Descriptor() ([]byte, []int) {
+ return file_internal_replication_grpc_replication_service_proto_rawDescGZIP(), []int{23}
+}
+
+func (x *StopPipelineRequest) GetId() string {
+ if x != nil {
+ return x.Id
+ }
+ return ""
+}
+
+type StopPipelineResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *StopPipelineResponse) Reset() {
+ *x = StopPipelineResponse{}
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[24]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *StopPipelineResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*StopPipelineResponse) ProtoMessage() {}
+
+func (x *StopPipelineResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[24]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use StopPipelineResponse.ProtoReflect.Descriptor instead.
+func (*StopPipelineResponse) Descriptor() ([]byte, []int) {
+ return file_internal_replication_grpc_replication_service_proto_rawDescGZIP(), []int{24}
+}
+
+type ResetPipelineRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ResetPipelineRequest) Reset() {
+ *x = ResetPipelineRequest{}
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[25]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ResetPipelineRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ResetPipelineRequest) ProtoMessage() {}
+
+func (x *ResetPipelineRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[25]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ResetPipelineRequest.ProtoReflect.Descriptor instead.
+func (*ResetPipelineRequest) Descriptor() ([]byte, []int) {
+ return file_internal_replication_grpc_replication_service_proto_rawDescGZIP(), []int{25}
+}
+
+func (x *ResetPipelineRequest) GetId() string {
+ if x != nil {
+ return x.Id
+ }
+ return ""
+}
+
+type ResetPipelineResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ResetPipelineResponse) Reset() {
+ *x = ResetPipelineResponse{}
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[26]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ResetPipelineResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ResetPipelineResponse) ProtoMessage() {}
+
+func (x *ResetPipelineResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_replication_grpc_replication_service_proto_msgTypes[26]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ResetPipelineResponse.ProtoReflect.Descriptor instead.
+func (*ResetPipelineResponse) Descriptor() ([]byte, []int) {
+ return file_internal_replication_grpc_replication_service_proto_rawDescGZIP(), []int{26}
+}
+
+var File_internal_replication_grpc_replication_service_proto protoreflect.FileDescriptor
+
+const file_internal_replication_grpc_replication_service_proto_rawDesc = "" +
+ "\n" +
+ "3internal/replication/grpc/replication_service.proto\x12\vreplication\x1a\x1fgoogle/protobuf/timestamp.proto\"K\n" +
+ "\x06Cursor\x12\x12\n" +
+ "\x04next\x18\x01 \x01(\tR\x04next\x12\x19\n" +
+ "\bhas_more\x18\x02 \x01(\bR\ahasMore\x12\x12\n" +
+ "\x04prev\x18\x03 \x01(\tR\x04prev\".\n" +
+ "\x14ListExportersRequest\x12\x16\n" +
+ "\x06cursor\x18\x01 \x01(\tR\x06cursor\"o\n" +
+ "\x15ListExportersResponse\x12)\n" +
+ "\x04data\x18\x01 \x03(\v2\x15.replication.ExporterR\x04data\x12+\n" +
+ "\x06cursor\x18\x02 \x01(\v2\x13.replication.CursorR\x06cursor\"\x91\x01\n" +
+ "\bExporter\x12\x0e\n" +
+ "\x02id\x18\x01 \x01(\tR\x02id\x129\n" +
+ "\n" +
+ "created_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12:\n" +
+ "\x06config\x18\x03 \x01(\v2\".replication.ExporterConfigurationR\x06config\"$\n" +
+ "\x12GetExporterRequest\x12\x0e\n" +
+ "\x02id\x18\x01 \x01(\tR\x02id\"H\n" +
+ "\x13GetExporterResponse\x121\n" +
+ "\bexporter\x18\x01 \x01(\v2\x15.replication.ExporterR\bexporter\"'\n" +
+ "\x15DeleteExporterRequest\x12\x0e\n" +
+ "\x02id\x18\x01 \x01(\tR\x02id\"\x18\n" +
+ "\x16DeleteExporterResponse\"G\n" +
+ "\x15ExporterConfiguration\x12\x16\n" +
+ "\x06driver\x18\x01 \x01(\tR\x06driver\x12\x16\n" +
+ "\x06config\x18\x02 \x01(\tR\x06config\"S\n" +
+ "\x15CreateExporterRequest\x12:\n" +
+ "\x06config\x18\x01 \x01(\v2\".replication.ExporterConfigurationR\x06config\"K\n" +
+ "\x16CreateExporterResponse\x121\n" +
+ "\bexporter\x18\x01 \x01(\v2\x15.replication.ExporterR\bexporter\".\n" +
+ "\x14ListPipelinesRequest\x12\x16\n" +
+ "\x06cursor\x18\x01 \x01(\tR\x06cursor\"o\n" +
+ "\x15ListPipelinesResponse\x12)\n" +
+ "\x04data\x18\x01 \x03(\v2\x15.replication.PipelineR\x04data\x12+\n" +
+ "\x06cursor\x18\x02 \x01(\v2\x13.replication.CursorR\x06cursor\"P\n" +
+ "\x15PipelineConfiguration\x12\x1f\n" +
+ "\vexporter_id\x18\x01 \x01(\tR\n" +
+ "exporterId\x12\x16\n" +
+ "\x06ledger\x18\x02 \x01(\tR\x06ledger\"\xf1\x01\n" +
+ "\bPipeline\x12:\n" +
+ "\x06config\x18\x01 \x01(\v2\".replication.PipelineConfigurationR\x06config\x128\n" +
+ "\tcreatedAt\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12\x0e\n" +
+ "\x02id\x18\x03 \x01(\tR\x02id\x12\x18\n" +
+ "\aenabled\x18\x04 \x01(\bR\aenabled\x12!\n" +
+ "\tlastLogID\x18\x05 \x01(\x04H\x00R\tlastLogID\x88\x01\x01\x12\x14\n" +
+ "\x05error\x18\x06 \x01(\tR\x05errorB\f\n" +
+ "\n" +
+ "_lastLogID\"$\n" +
+ "\x12GetPipelineRequest\x12\x0e\n" +
+ "\x02id\x18\x01 \x01(\tR\x02id\"H\n" +
+ "\x13GetPipelineResponse\x121\n" +
+ "\bpipeline\x18\x01 \x01(\v2\x15.replication.PipelineR\bpipeline\"S\n" +
+ "\x15CreatePipelineRequest\x12:\n" +
+ "\x06config\x18\x01 \x01(\v2\".replication.PipelineConfigurationR\x06config\"K\n" +
+ "\x16CreatePipelineResponse\x121\n" +
+ "\bpipeline\x18\x01 \x01(\v2\x15.replication.PipelineR\bpipeline\"'\n" +
+ "\x15DeletePipelineRequest\x12\x0e\n" +
+ "\x02id\x18\x01 \x01(\tR\x02id\"\x18\n" +
+ "\x16DeletePipelineResponse\"&\n" +
+ "\x14StartPipelineRequest\x12\x0e\n" +
+ "\x02id\x18\x01 \x01(\tR\x02id\"\x17\n" +
+ "\x15StartPipelineResponse\"%\n" +
+ "\x13StopPipelineRequest\x12\x0e\n" +
+ "\x02id\x18\x01 \x01(\tR\x02id\"\x16\n" +
+ "\x14StopPipelineResponse\"&\n" +
+ "\x14ResetPipelineRequest\x12\x0e\n" +
+ "\x02id\x18\x01 \x01(\tR\x02id\"\x17\n" +
+ "\x15ResetPipelineResponse2\xd2\a\n" +
+ "\vReplication\x12Y\n" +
+ "\x0eCreateExporter\x12\".replication.CreateExporterRequest\x1a#.replication.CreateExporterResponse\x12V\n" +
+ "\rListExporters\x12!.replication.ListExportersRequest\x1a\".replication.ListExportersResponse\x12P\n" +
+ "\vGetExporter\x12\x1f.replication.GetExporterRequest\x1a .replication.GetExporterResponse\x12Y\n" +
+ "\x0eDeleteExporter\x12\".replication.DeleteExporterRequest\x1a#.replication.DeleteExporterResponse\x12V\n" +
+ "\rListPipelines\x12!.replication.ListPipelinesRequest\x1a\".replication.ListPipelinesResponse\x12P\n" +
+ "\vGetPipeline\x12\x1f.replication.GetPipelineRequest\x1a .replication.GetPipelineResponse\x12Y\n" +
+ "\x0eCreatePipeline\x12\".replication.CreatePipelineRequest\x1a#.replication.CreatePipelineResponse\x12Y\n" +
+ "\x0eDeletePipeline\x12\".replication.DeletePipelineRequest\x1a#.replication.DeletePipelineResponse\x12V\n" +
+ "\rStartPipeline\x12!.replication.StartPipelineRequest\x1a\".replication.StartPipelineResponse\x12S\n" +
+ "\fStopPipeline\x12 .replication.StopPipelineRequest\x1a!.replication.StopPipelineResponse\x12V\n" +
+ "\rResetPipeline\x12!.replication.ResetPipelineRequest\x1a\".replication.ResetPipelineResponseB8Z6github.com/formancehq/ledger/internal/replication/grpcb\x06proto3"
+
+var (
+ file_internal_replication_grpc_replication_service_proto_rawDescOnce sync.Once
+ file_internal_replication_grpc_replication_service_proto_rawDescData []byte
+)
+
+func file_internal_replication_grpc_replication_service_proto_rawDescGZIP() []byte {
+ file_internal_replication_grpc_replication_service_proto_rawDescOnce.Do(func() {
+ file_internal_replication_grpc_replication_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_internal_replication_grpc_replication_service_proto_rawDesc), len(file_internal_replication_grpc_replication_service_proto_rawDesc)))
+ })
+ return file_internal_replication_grpc_replication_service_proto_rawDescData
+}
+
+var file_internal_replication_grpc_replication_service_proto_msgTypes = make([]protoimpl.MessageInfo, 27)
+var file_internal_replication_grpc_replication_service_proto_goTypes = []any{
+ (*Cursor)(nil), // 0: replication.Cursor
+ (*ListExportersRequest)(nil), // 1: replication.ListExportersRequest
+ (*ListExportersResponse)(nil), // 2: replication.ListExportersResponse
+ (*Exporter)(nil), // 3: replication.Exporter
+ (*GetExporterRequest)(nil), // 4: replication.GetExporterRequest
+ (*GetExporterResponse)(nil), // 5: replication.GetExporterResponse
+ (*DeleteExporterRequest)(nil), // 6: replication.DeleteExporterRequest
+ (*DeleteExporterResponse)(nil), // 7: replication.DeleteExporterResponse
+ (*ExporterConfiguration)(nil), // 8: replication.ExporterConfiguration
+ (*CreateExporterRequest)(nil), // 9: replication.CreateExporterRequest
+ (*CreateExporterResponse)(nil), // 10: replication.CreateExporterResponse
+ (*ListPipelinesRequest)(nil), // 11: replication.ListPipelinesRequest
+ (*ListPipelinesResponse)(nil), // 12: replication.ListPipelinesResponse
+ (*PipelineConfiguration)(nil), // 13: replication.PipelineConfiguration
+ (*Pipeline)(nil), // 14: replication.Pipeline
+ (*GetPipelineRequest)(nil), // 15: replication.GetPipelineRequest
+ (*GetPipelineResponse)(nil), // 16: replication.GetPipelineResponse
+ (*CreatePipelineRequest)(nil), // 17: replication.CreatePipelineRequest
+ (*CreatePipelineResponse)(nil), // 18: replication.CreatePipelineResponse
+ (*DeletePipelineRequest)(nil), // 19: replication.DeletePipelineRequest
+ (*DeletePipelineResponse)(nil), // 20: replication.DeletePipelineResponse
+ (*StartPipelineRequest)(nil), // 21: replication.StartPipelineRequest
+ (*StartPipelineResponse)(nil), // 22: replication.StartPipelineResponse
+ (*StopPipelineRequest)(nil), // 23: replication.StopPipelineRequest
+ (*StopPipelineResponse)(nil), // 24: replication.StopPipelineResponse
+ (*ResetPipelineRequest)(nil), // 25: replication.ResetPipelineRequest
+ (*ResetPipelineResponse)(nil), // 26: replication.ResetPipelineResponse
+ (*timestamppb.Timestamp)(nil), // 27: google.protobuf.Timestamp
+}
+var file_internal_replication_grpc_replication_service_proto_depIdxs = []int32{
+ 3, // 0: replication.ListExportersResponse.data:type_name -> replication.Exporter
+ 0, // 1: replication.ListExportersResponse.cursor:type_name -> replication.Cursor
+ 27, // 2: replication.Exporter.created_at:type_name -> google.protobuf.Timestamp
+ 8, // 3: replication.Exporter.config:type_name -> replication.ExporterConfiguration
+ 3, // 4: replication.GetExporterResponse.exporter:type_name -> replication.Exporter
+ 8, // 5: replication.CreateExporterRequest.config:type_name -> replication.ExporterConfiguration
+ 3, // 6: replication.CreateExporterResponse.exporter:type_name -> replication.Exporter
+ 14, // 7: replication.ListPipelinesResponse.data:type_name -> replication.Pipeline
+ 0, // 8: replication.ListPipelinesResponse.cursor:type_name -> replication.Cursor
+ 13, // 9: replication.Pipeline.config:type_name -> replication.PipelineConfiguration
+ 27, // 10: replication.Pipeline.createdAt:type_name -> google.protobuf.Timestamp
+ 14, // 11: replication.GetPipelineResponse.pipeline:type_name -> replication.Pipeline
+ 13, // 12: replication.CreatePipelineRequest.config:type_name -> replication.PipelineConfiguration
+ 14, // 13: replication.CreatePipelineResponse.pipeline:type_name -> replication.Pipeline
+ 9, // 14: replication.Replication.CreateExporter:input_type -> replication.CreateExporterRequest
+ 1, // 15: replication.Replication.ListExporters:input_type -> replication.ListExportersRequest
+ 4, // 16: replication.Replication.GetExporter:input_type -> replication.GetExporterRequest
+ 6, // 17: replication.Replication.DeleteExporter:input_type -> replication.DeleteExporterRequest
+ 11, // 18: replication.Replication.ListPipelines:input_type -> replication.ListPipelinesRequest
+ 15, // 19: replication.Replication.GetPipeline:input_type -> replication.GetPipelineRequest
+ 17, // 20: replication.Replication.CreatePipeline:input_type -> replication.CreatePipelineRequest
+ 19, // 21: replication.Replication.DeletePipeline:input_type -> replication.DeletePipelineRequest
+ 21, // 22: replication.Replication.StartPipeline:input_type -> replication.StartPipelineRequest
+ 23, // 23: replication.Replication.StopPipeline:input_type -> replication.StopPipelineRequest
+ 25, // 24: replication.Replication.ResetPipeline:input_type -> replication.ResetPipelineRequest
+ 10, // 25: replication.Replication.CreateExporter:output_type -> replication.CreateExporterResponse
+ 2, // 26: replication.Replication.ListExporters:output_type -> replication.ListExportersResponse
+ 5, // 27: replication.Replication.GetExporter:output_type -> replication.GetExporterResponse
+ 7, // 28: replication.Replication.DeleteExporter:output_type -> replication.DeleteExporterResponse
+ 12, // 29: replication.Replication.ListPipelines:output_type -> replication.ListPipelinesResponse
+ 16, // 30: replication.Replication.GetPipeline:output_type -> replication.GetPipelineResponse
+ 18, // 31: replication.Replication.CreatePipeline:output_type -> replication.CreatePipelineResponse
+ 20, // 32: replication.Replication.DeletePipeline:output_type -> replication.DeletePipelineResponse
+ 22, // 33: replication.Replication.StartPipeline:output_type -> replication.StartPipelineResponse
+ 24, // 34: replication.Replication.StopPipeline:output_type -> replication.StopPipelineResponse
+ 26, // 35: replication.Replication.ResetPipeline:output_type -> replication.ResetPipelineResponse
+ 25, // [25:36] is the sub-list for method output_type
+ 14, // [14:25] is the sub-list for method input_type
+ 14, // [14:14] is the sub-list for extension type_name
+ 14, // [14:14] is the sub-list for extension extendee
+ 0, // [0:14] is the sub-list for field type_name
+}
+
+func init() { file_internal_replication_grpc_replication_service_proto_init() }
+func file_internal_replication_grpc_replication_service_proto_init() {
+ if File_internal_replication_grpc_replication_service_proto != nil {
+ return
+ }
+ file_internal_replication_grpc_replication_service_proto_msgTypes[14].OneofWrappers = []any{}
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: unsafe.Slice(unsafe.StringData(file_internal_replication_grpc_replication_service_proto_rawDesc), len(file_internal_replication_grpc_replication_service_proto_rawDesc)),
+ NumEnums: 0,
+ NumMessages: 27,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_internal_replication_grpc_replication_service_proto_goTypes,
+ DependencyIndexes: file_internal_replication_grpc_replication_service_proto_depIdxs,
+ MessageInfos: file_internal_replication_grpc_replication_service_proto_msgTypes,
+ }.Build()
+ File_internal_replication_grpc_replication_service_proto = out.File
+ file_internal_replication_grpc_replication_service_proto_goTypes = nil
+ file_internal_replication_grpc_replication_service_proto_depIdxs = nil
+}
diff --git a/internal/replication/grpc/replication_service.proto b/internal/replication/grpc/replication_service.proto
new file mode 100644
index 0000000000..5aa47eb473
--- /dev/null
+++ b/internal/replication/grpc/replication_service.proto
@@ -0,0 +1,132 @@
+syntax = "proto3";
+option go_package = "github.com/formancehq/ledger/internal/replication/grpc";
+
+import "google/protobuf/timestamp.proto";
+
+package replication;
+
+service Replication {
+ rpc CreateExporter(CreateExporterRequest) returns (CreateExporterResponse);
+ rpc ListExporters(ListExportersRequest) returns (ListExportersResponse);
+ rpc GetExporter(GetExporterRequest) returns (GetExporterResponse);
+ rpc DeleteExporter(DeleteExporterRequest) returns (DeleteExporterResponse);
+ rpc ListPipelines(ListPipelinesRequest) returns (ListPipelinesResponse);
+ rpc GetPipeline(GetPipelineRequest) returns (GetPipelineResponse);
+ rpc CreatePipeline(CreatePipelineRequest) returns (CreatePipelineResponse);
+ rpc DeletePipeline(DeletePipelineRequest) returns (DeletePipelineResponse);
+ rpc StartPipeline(StartPipelineRequest) returns (StartPipelineResponse);
+ rpc StopPipeline(StopPipelineRequest) returns (StopPipelineResponse);
+ rpc ResetPipeline(ResetPipelineRequest) returns (ResetPipelineResponse);
+}
+
+message Cursor {
+ string next = 1;
+ bool has_more = 2;
+ string prev = 3;
+}
+
+message ListExportersRequest {
+ string cursor = 1;
+}
+
+message ListExportersResponse {
+ repeated Exporter data = 1;
+ Cursor cursor = 2;
+}
+
+message Exporter {
+ string id = 1;
+ google.protobuf.Timestamp created_at = 2;
+ ExporterConfiguration config = 3;
+}
+
+message GetExporterRequest {
+ string id = 1;
+}
+
+message GetExporterResponse {
+ Exporter exporter = 1;
+}
+
+message DeleteExporterRequest {
+ string id = 1;
+}
+
+message DeleteExporterResponse {}
+
+message ExporterConfiguration {
+ string driver = 1;
+ string config = 2;
+}
+
+message CreateExporterRequest {
+ ExporterConfiguration config = 1;
+}
+
+message CreateExporterResponse {
+ Exporter exporter = 1;
+}
+
+message ListPipelinesRequest {
+ string cursor = 1;
+}
+
+message ListPipelinesResponse {
+ repeated Pipeline data = 1;
+ Cursor cursor = 2;
+}
+
+message PipelineConfiguration {
+ string exporter_id = 1;
+ string ledger = 2;
+}
+
+message Pipeline {
+ PipelineConfiguration config = 1;
+ google.protobuf.Timestamp createdAt = 2;
+ string id = 3;
+ bool enabled = 4;
+ optional uint64 lastLogID = 5;
+ string error = 6;
+}
+
+message GetPipelineRequest {
+ string id = 1;
+}
+
+message GetPipelineResponse {
+ Pipeline pipeline = 1;
+}
+
+message CreatePipelineRequest {
+ PipelineConfiguration config = 1;
+}
+
+message CreatePipelineResponse {
+ Pipeline pipeline = 1;
+}
+
+message DeletePipelineRequest {
+ string id = 1;
+}
+
+message DeletePipelineResponse {}
+
+message StartPipelineRequest {
+ string id = 1;
+}
+
+message StartPipelineResponse {}
+
+message StopPipelineRequest {
+ string id = 1;
+}
+
+message StopPipelineResponse {}
+
+message ResetPipelineRequest {
+ string id = 1;
+}
+
+message ResetPipelineResponse {}
+
diff --git a/internal/replication/grpc/replication_service_grpc.pb.go b/internal/replication/grpc/replication_service_grpc.pb.go
new file mode 100644
index 0000000000..273380f238
--- /dev/null
+++ b/internal/replication/grpc/replication_service_grpc.pb.go
@@ -0,0 +1,501 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.5.1
+// - protoc v5.27.5
+// source: internal/replication/grpc/replication_service.proto
+
+package grpc
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.64.0 or later.
+const _ = grpc.SupportPackageIsVersion9
+
+const (
+ Replication_CreateExporter_FullMethodName = "/replication.Replication/CreateExporter"
+ Replication_ListExporters_FullMethodName = "/replication.Replication/ListExporters"
+ Replication_GetExporter_FullMethodName = "/replication.Replication/GetExporter"
+ Replication_DeleteExporter_FullMethodName = "/replication.Replication/DeleteExporter"
+ Replication_ListPipelines_FullMethodName = "/replication.Replication/ListPipelines"
+ Replication_GetPipeline_FullMethodName = "/replication.Replication/GetPipeline"
+ Replication_CreatePipeline_FullMethodName = "/replication.Replication/CreatePipeline"
+ Replication_DeletePipeline_FullMethodName = "/replication.Replication/DeletePipeline"
+ Replication_StartPipeline_FullMethodName = "/replication.Replication/StartPipeline"
+ Replication_StopPipeline_FullMethodName = "/replication.Replication/StopPipeline"
+ Replication_ResetPipeline_FullMethodName = "/replication.Replication/ResetPipeline"
+)
+
+// ReplicationClient is the client API for Replication service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type ReplicationClient interface {
+ CreateExporter(ctx context.Context, in *CreateExporterRequest, opts ...grpc.CallOption) (*CreateExporterResponse, error)
+ ListExporters(ctx context.Context, in *ListExportersRequest, opts ...grpc.CallOption) (*ListExportersResponse, error)
+ GetExporter(ctx context.Context, in *GetExporterRequest, opts ...grpc.CallOption) (*GetExporterResponse, error)
+ DeleteExporter(ctx context.Context, in *DeleteExporterRequest, opts ...grpc.CallOption) (*DeleteExporterResponse, error)
+ ListPipelines(ctx context.Context, in *ListPipelinesRequest, opts ...grpc.CallOption) (*ListPipelinesResponse, error)
+ GetPipeline(ctx context.Context, in *GetPipelineRequest, opts ...grpc.CallOption) (*GetPipelineResponse, error)
+ CreatePipeline(ctx context.Context, in *CreatePipelineRequest, opts ...grpc.CallOption) (*CreatePipelineResponse, error)
+ DeletePipeline(ctx context.Context, in *DeletePipelineRequest, opts ...grpc.CallOption) (*DeletePipelineResponse, error)
+ StartPipeline(ctx context.Context, in *StartPipelineRequest, opts ...grpc.CallOption) (*StartPipelineResponse, error)
+ StopPipeline(ctx context.Context, in *StopPipelineRequest, opts ...grpc.CallOption) (*StopPipelineResponse, error)
+ ResetPipeline(ctx context.Context, in *ResetPipelineRequest, opts ...grpc.CallOption) (*ResetPipelineResponse, error)
+}
+
+type replicationClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewReplicationClient(cc grpc.ClientConnInterface) ReplicationClient {
+ return &replicationClient{cc}
+}
+
+func (c *replicationClient) CreateExporter(ctx context.Context, in *CreateExporterRequest, opts ...grpc.CallOption) (*CreateExporterResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(CreateExporterResponse)
+ err := c.cc.Invoke(ctx, Replication_CreateExporter_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *replicationClient) ListExporters(ctx context.Context, in *ListExportersRequest, opts ...grpc.CallOption) (*ListExportersResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(ListExportersResponse)
+ err := c.cc.Invoke(ctx, Replication_ListExporters_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *replicationClient) GetExporter(ctx context.Context, in *GetExporterRequest, opts ...grpc.CallOption) (*GetExporterResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(GetExporterResponse)
+ err := c.cc.Invoke(ctx, Replication_GetExporter_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *replicationClient) DeleteExporter(ctx context.Context, in *DeleteExporterRequest, opts ...grpc.CallOption) (*DeleteExporterResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(DeleteExporterResponse)
+ err := c.cc.Invoke(ctx, Replication_DeleteExporter_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *replicationClient) ListPipelines(ctx context.Context, in *ListPipelinesRequest, opts ...grpc.CallOption) (*ListPipelinesResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(ListPipelinesResponse)
+ err := c.cc.Invoke(ctx, Replication_ListPipelines_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *replicationClient) GetPipeline(ctx context.Context, in *GetPipelineRequest, opts ...grpc.CallOption) (*GetPipelineResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(GetPipelineResponse)
+ err := c.cc.Invoke(ctx, Replication_GetPipeline_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *replicationClient) CreatePipeline(ctx context.Context, in *CreatePipelineRequest, opts ...grpc.CallOption) (*CreatePipelineResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(CreatePipelineResponse)
+ err := c.cc.Invoke(ctx, Replication_CreatePipeline_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *replicationClient) DeletePipeline(ctx context.Context, in *DeletePipelineRequest, opts ...grpc.CallOption) (*DeletePipelineResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(DeletePipelineResponse)
+ err := c.cc.Invoke(ctx, Replication_DeletePipeline_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *replicationClient) StartPipeline(ctx context.Context, in *StartPipelineRequest, opts ...grpc.CallOption) (*StartPipelineResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(StartPipelineResponse)
+ err := c.cc.Invoke(ctx, Replication_StartPipeline_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *replicationClient) StopPipeline(ctx context.Context, in *StopPipelineRequest, opts ...grpc.CallOption) (*StopPipelineResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(StopPipelineResponse)
+ err := c.cc.Invoke(ctx, Replication_StopPipeline_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *replicationClient) ResetPipeline(ctx context.Context, in *ResetPipelineRequest, opts ...grpc.CallOption) (*ResetPipelineResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(ResetPipelineResponse)
+ err := c.cc.Invoke(ctx, Replication_ResetPipeline_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// ReplicationServer is the server API for Replication service.
+// All implementations must embed UnimplementedReplicationServer
+// for forward compatibility.
+type ReplicationServer interface {
+ CreateExporter(context.Context, *CreateExporterRequest) (*CreateExporterResponse, error)
+ ListExporters(context.Context, *ListExportersRequest) (*ListExportersResponse, error)
+ GetExporter(context.Context, *GetExporterRequest) (*GetExporterResponse, error)
+ DeleteExporter(context.Context, *DeleteExporterRequest) (*DeleteExporterResponse, error)
+ ListPipelines(context.Context, *ListPipelinesRequest) (*ListPipelinesResponse, error)
+ GetPipeline(context.Context, *GetPipelineRequest) (*GetPipelineResponse, error)
+ CreatePipeline(context.Context, *CreatePipelineRequest) (*CreatePipelineResponse, error)
+ DeletePipeline(context.Context, *DeletePipelineRequest) (*DeletePipelineResponse, error)
+ StartPipeline(context.Context, *StartPipelineRequest) (*StartPipelineResponse, error)
+ StopPipeline(context.Context, *StopPipelineRequest) (*StopPipelineResponse, error)
+ ResetPipeline(context.Context, *ResetPipelineRequest) (*ResetPipelineResponse, error)
+ mustEmbedUnimplementedReplicationServer()
+}
+
+// UnimplementedReplicationServer must be embedded to have
+// forward compatible implementations.
+//
+// NOTE: this should be embedded by value instead of pointer to avoid a nil
+// pointer dereference when methods are called.
+type UnimplementedReplicationServer struct{}
+
+func (UnimplementedReplicationServer) CreateExporter(context.Context, *CreateExporterRequest) (*CreateExporterResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method CreateExporter not implemented")
+}
+func (UnimplementedReplicationServer) ListExporters(context.Context, *ListExportersRequest) (*ListExportersResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method ListExporters not implemented")
+}
+func (UnimplementedReplicationServer) GetExporter(context.Context, *GetExporterRequest) (*GetExporterResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method GetExporter not implemented")
+}
+func (UnimplementedReplicationServer) DeleteExporter(context.Context, *DeleteExporterRequest) (*DeleteExporterResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method DeleteExporter not implemented")
+}
+func (UnimplementedReplicationServer) ListPipelines(context.Context, *ListPipelinesRequest) (*ListPipelinesResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method ListPipelines not implemented")
+}
+func (UnimplementedReplicationServer) GetPipeline(context.Context, *GetPipelineRequest) (*GetPipelineResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method GetPipeline not implemented")
+}
+func (UnimplementedReplicationServer) CreatePipeline(context.Context, *CreatePipelineRequest) (*CreatePipelineResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method CreatePipeline not implemented")
+}
+func (UnimplementedReplicationServer) DeletePipeline(context.Context, *DeletePipelineRequest) (*DeletePipelineResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method DeletePipeline not implemented")
+}
+func (UnimplementedReplicationServer) StartPipeline(context.Context, *StartPipelineRequest) (*StartPipelineResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method StartPipeline not implemented")
+}
+func (UnimplementedReplicationServer) StopPipeline(context.Context, *StopPipelineRequest) (*StopPipelineResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method StopPipeline not implemented")
+}
+func (UnimplementedReplicationServer) ResetPipeline(context.Context, *ResetPipelineRequest) (*ResetPipelineResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method ResetPipeline not implemented")
+}
+func (UnimplementedReplicationServer) mustEmbedUnimplementedReplicationServer() {}
+func (UnimplementedReplicationServer) testEmbeddedByValue() {}
+
+// UnsafeReplicationServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to ReplicationServer will
+// result in compilation errors.
+type UnsafeReplicationServer interface {
+ mustEmbedUnimplementedReplicationServer()
+}
+
+func RegisterReplicationServer(s grpc.ServiceRegistrar, srv ReplicationServer) {
+ // If the following call pancis, it indicates UnimplementedReplicationServer was
+ // embedded by pointer and is nil. This will cause panics if an
+ // unimplemented method is ever invoked, so we test this at initialization
+ // time to prevent it from happening at runtime later due to I/O.
+ if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
+ t.testEmbeddedByValue()
+ }
+ s.RegisterService(&Replication_ServiceDesc, srv)
+}
+
+func _Replication_CreateExporter_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(CreateExporterRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(ReplicationServer).CreateExporter(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: Replication_CreateExporter_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(ReplicationServer).CreateExporter(ctx, req.(*CreateExporterRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _Replication_ListExporters_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(ListExportersRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(ReplicationServer).ListExporters(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: Replication_ListExporters_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(ReplicationServer).ListExporters(ctx, req.(*ListExportersRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _Replication_GetExporter_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(GetExporterRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(ReplicationServer).GetExporter(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: Replication_GetExporter_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(ReplicationServer).GetExporter(ctx, req.(*GetExporterRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _Replication_DeleteExporter_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(DeleteExporterRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(ReplicationServer).DeleteExporter(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: Replication_DeleteExporter_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(ReplicationServer).DeleteExporter(ctx, req.(*DeleteExporterRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _Replication_ListPipelines_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(ListPipelinesRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(ReplicationServer).ListPipelines(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: Replication_ListPipelines_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(ReplicationServer).ListPipelines(ctx, req.(*ListPipelinesRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _Replication_GetPipeline_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(GetPipelineRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(ReplicationServer).GetPipeline(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: Replication_GetPipeline_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(ReplicationServer).GetPipeline(ctx, req.(*GetPipelineRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _Replication_CreatePipeline_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(CreatePipelineRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(ReplicationServer).CreatePipeline(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: Replication_CreatePipeline_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(ReplicationServer).CreatePipeline(ctx, req.(*CreatePipelineRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _Replication_DeletePipeline_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(DeletePipelineRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(ReplicationServer).DeletePipeline(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: Replication_DeletePipeline_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(ReplicationServer).DeletePipeline(ctx, req.(*DeletePipelineRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _Replication_StartPipeline_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(StartPipelineRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(ReplicationServer).StartPipeline(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: Replication_StartPipeline_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(ReplicationServer).StartPipeline(ctx, req.(*StartPipelineRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _Replication_StopPipeline_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(StopPipelineRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(ReplicationServer).StopPipeline(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: Replication_StopPipeline_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(ReplicationServer).StopPipeline(ctx, req.(*StopPipelineRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _Replication_ResetPipeline_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(ResetPipelineRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(ReplicationServer).ResetPipeline(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: Replication_ResetPipeline_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(ReplicationServer).ResetPipeline(ctx, req.(*ResetPipelineRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// Replication_ServiceDesc is the grpc.ServiceDesc for Replication service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var Replication_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "replication.Replication",
+ HandlerType: (*ReplicationServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "CreateExporter",
+ Handler: _Replication_CreateExporter_Handler,
+ },
+ {
+ MethodName: "ListExporters",
+ Handler: _Replication_ListExporters_Handler,
+ },
+ {
+ MethodName: "GetExporter",
+ Handler: _Replication_GetExporter_Handler,
+ },
+ {
+ MethodName: "DeleteExporter",
+ Handler: _Replication_DeleteExporter_Handler,
+ },
+ {
+ MethodName: "ListPipelines",
+ Handler: _Replication_ListPipelines_Handler,
+ },
+ {
+ MethodName: "GetPipeline",
+ Handler: _Replication_GetPipeline_Handler,
+ },
+ {
+ MethodName: "CreatePipeline",
+ Handler: _Replication_CreatePipeline_Handler,
+ },
+ {
+ MethodName: "DeletePipeline",
+ Handler: _Replication_DeletePipeline_Handler,
+ },
+ {
+ MethodName: "StartPipeline",
+ Handler: _Replication_StartPipeline_Handler,
+ },
+ {
+ MethodName: "StopPipeline",
+ Handler: _Replication_StopPipeline_Handler,
+ },
+ {
+ MethodName: "ResetPipeline",
+ Handler: _Replication_ResetPipeline_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "internal/replication/grpc/replication_service.proto",
+}
diff --git a/internal/replication/manager.go b/internal/replication/manager.go
new file mode 100644
index 0000000000..b9ff55a781
--- /dev/null
+++ b/internal/replication/manager.go
@@ -0,0 +1,467 @@
+package replication
+
+import (
+ "context"
+ "fmt"
+ "github.com/formancehq/go-libs/v3/bun/bunpaginate"
+ "github.com/formancehq/go-libs/v3/otlp"
+ ledger "github.com/formancehq/ledger/internal"
+ "github.com/formancehq/ledger/internal/controller/system"
+ "github.com/formancehq/ledger/internal/storage/common"
+ "sync"
+ "time"
+
+ "github.com/formancehq/go-libs/v3/logging"
+ "github.com/formancehq/ledger/internal/replication/drivers"
+ "github.com/pkg/errors"
+)
+
+type Manager struct {
+ mu sync.Mutex
+
+ stopChannel chan chan error
+ storage Storage
+ pipelines map[string]*PipelineHandler
+ pipelinesWaitGroup sync.WaitGroup
+ logger logging.Logger
+
+ driverFactory drivers.Factory
+ drivers map[string]*DriverFacade
+
+ pipelineOptions []PipelineOption
+ exportersConfigValidator ConfigValidator
+ syncPeriod time.Duration
+ started chan struct{}
+}
+
+func (m *Manager) CreateExporter(ctx context.Context, configuration ledger.ExporterConfiguration) (*ledger.Exporter, error) {
+ if err := m.exportersConfigValidator.ValidateConfig(configuration.Driver, configuration.Config); err != nil {
+ return nil, system.NewErrInvalidDriverConfiguration(configuration.Driver, err)
+ }
+
+ exporter := ledger.NewExporter(configuration)
+ if err := m.storage.CreateExporter(ctx, exporter); err != nil {
+ return nil, err
+ }
+ return &exporter, nil
+}
+
+func (m *Manager) initExporter(exporterID string) error {
+
+ _, ok := m.drivers[exporterID]
+ if ok {
+ return nil
+ }
+
+ driver, _, err := m.driverFactory.Create(context.Background(), exporterID)
+ if err != nil {
+ return err
+ }
+
+ driverFacade := newDriverFacade(driver, m.logger, 2*time.Second)
+ driverFacade.Run(context.Background())
+
+ m.drivers[exporterID] = driverFacade
+
+ return nil
+}
+
+func (m *Manager) stopDriver(ctx context.Context, driver drivers.Driver) {
+ if err := driver.Stop(ctx); err != nil {
+ m.logger.Errorf("stopping driver: %s", err)
+ }
+ for name, registeredExporter := range m.drivers {
+ if driver == registeredExporter {
+ delete(m.drivers, name)
+ return
+ }
+ }
+}
+
+func (m *Manager) StartPipeline(ctx context.Context, pipelineID string) error {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+
+ pipeline, err := m.storage.GetPipeline(ctx, pipelineID)
+ if err != nil {
+ return err
+ }
+
+ _, err = m.startPipeline(ctx, *pipeline)
+ return err
+}
+
+func (m *Manager) startPipeline(ctx context.Context, pipeline ledger.Pipeline) (*PipelineHandler, error) {
+ m.logger.Infof("initializing pipeline")
+ _, ok := m.pipelines[pipeline.ID]
+ if ok {
+ return nil, ledger.NewErrAlreadyStarted(pipeline.ID)
+ }
+
+ ctx = logging.ContextWithLogger(
+ ctx,
+ m.logger.WithFields(map[string]any{
+ "ledger": pipeline.Ledger,
+ "exporter": pipeline.ExporterID,
+ }),
+ )
+
+ // Detach the context as once the process of pipeline initialisation is started, we must not stop it
+ ctx = context.WithoutCancel(ctx)
+
+ m.logger.Infof("initializing exporter")
+ if err := m.initExporter(pipeline.ExporterID); err != nil {
+ return nil, fmt.Errorf("initializing exporter: %w", err)
+ }
+
+ store, _, err := m.storage.OpenLedger(ctx, pipeline.Ledger)
+ if err != nil {
+ return nil, errors.Wrap(err, "opening ledger")
+ }
+
+ pipelineHandler := NewPipelineHandler(
+ pipeline,
+ store,
+ m.drivers[pipeline.ExporterID],
+ m.logger,
+ m.pipelineOptions...,
+ )
+ m.pipelines[pipeline.ID] = pipelineHandler
+ m.pipelinesWaitGroup.Add(1)
+
+ // ignore the cancel function, as it will be called by the pipeline at its end
+ subscription := make(chan uint64)
+
+ m.logger.Infof("starting handler")
+ go func() {
+ for lastLogID := range subscription {
+ if err := m.storage.StorePipelineState(ctx, pipeline.ID, lastLogID); err != nil {
+ m.logger.Errorf("Unable to store state: %s", err)
+ }
+ }
+ }()
+ go func() {
+ defer func() {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+ defer m.pipelinesWaitGroup.Done()
+ close(subscription)
+ }()
+ pipelineHandler.Run(ctx, subscription)
+ }()
+
+ return pipelineHandler, nil
+}
+
+func (m *Manager) stopPipeline(ctx context.Context, id string) error {
+ handler, ok := m.pipelines[id]
+ if !ok {
+ return ledger.NewErrPipelineNotFound(id)
+ }
+
+ if err := handler.Shutdown(ctx); err != nil {
+ return fmt.Errorf("error stopping pipeline: %w", err)
+ }
+ delete(m.pipelines, id)
+
+ m.logger.Infof("pipeline terminated, pruning exporter...")
+ m.stopExporterIfNeeded(ctx, handler)
+
+ return nil
+}
+
+func (m *Manager) StopPipeline(ctx context.Context, id string) error {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+
+ return m.stopPipeline(ctx, id)
+}
+
+func (m *Manager) stopPipelines(ctx context.Context) {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+
+ for id := range m.pipelines {
+ if err := m.stopPipeline(ctx, id); err != nil {
+ m.logger.Errorf("error stopping pipeline: %s", err)
+ }
+ }
+}
+
+func (m *Manager) stopExporterIfNeeded(ctx context.Context, handler *PipelineHandler) {
+ // Check if the exporter associated to the pipeline is still in used
+ exporter := handler.exporter
+ for _, anotherPipeline := range m.pipelines {
+ if anotherPipeline.exporter == exporter {
+ // Exporter still used, keep it
+ return
+ }
+ }
+
+ m.logger.Infof("exporter %s no more used, stopping it...", handler.pipeline.ExporterID)
+ m.stopDriver(ctx, exporter)
+}
+
+func (m *Manager) synchronizePipelines(ctx context.Context) error {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+
+ m.logger.Debug("restore pipelines from store")
+ defer func() {
+ m.logger.Debug("restoration terminated")
+ }()
+ pipelines, err := m.storage.ListEnabledPipelines(ctx)
+ if err != nil {
+ return fmt.Errorf("reading pipelines from store: %w", err)
+ }
+
+ for _, pipeline := range pipelines {
+ m.logger.Debugf("restoring pipeline %s", pipeline.ID)
+ if _, err := m.startPipeline(ctx, pipeline); err != nil {
+ return err
+ }
+ }
+
+l:
+ for id := range m.pipelines {
+ for _, pipeline := range pipelines {
+ if id == pipeline.ID {
+ continue l
+ }
+ }
+
+ if err := m.stopPipeline(ctx, id); err != nil {
+ m.logger.Errorf("error stopping pipeline: %s", err)
+ continue
+ }
+ }
+
+ return nil
+}
+
+func (m *Manager) Started() <-chan struct{} {
+ return m.started
+}
+
+func (m *Manager) Run(ctx context.Context) {
+ if err := m.synchronizePipelines(ctx); err != nil {
+ m.logger.Errorf("restoring pipeline: %s", err)
+ }
+
+ close(m.started)
+
+ for {
+ select {
+ case signalChannel := <-m.stopChannel:
+ m.logger.Debugf("got stop signal")
+ m.stopPipelines(ctx)
+ m.pipelinesWaitGroup.Wait()
+ close(signalChannel)
+ return
+ case <-time.After(m.syncPeriod):
+ if err := m.synchronizePipelines(ctx); err != nil {
+ m.logger.Errorf("synchronizing pipelines: %s", err)
+ }
+ }
+ }
+}
+
+func (m *Manager) GetPipeline(ctx context.Context, id string) (*ledger.Pipeline, error) {
+ pipeline, err := m.storage.GetPipeline(ctx, id)
+ if err != nil {
+ if errors.Is(err, common.ErrNotFound) {
+ return nil, ledger.NewErrPipelineNotFound(id)
+ }
+ return nil, err
+ }
+
+ return pipeline, nil
+}
+
+func (m *Manager) Stop(ctx context.Context) error {
+ m.logger.Info("stopping manager")
+ signalChannel := make(chan error, 1)
+
+ select {
+ case m.stopChannel <- signalChannel:
+ m.logger.Debug("stopping manager signal sent")
+ select {
+ case <-signalChannel:
+ m.logger.Info("manager stopped")
+ return nil
+ case <-ctx.Done():
+ m.logger.Error("context canceled while waiting for manager termination")
+ return ctx.Err()
+ }
+ case <-ctx.Done():
+ m.logger.Error("context canceled while waiting for manager signal handling")
+ return ctx.Err()
+ }
+}
+
+func (m *Manager) GetDriver(name string) *DriverFacade {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+
+ return m.drivers[name]
+}
+
+func (m *Manager) GetExporter(ctx context.Context, id string) (*ledger.Exporter, error) {
+ exporter, err := m.storage.GetExporter(ctx, id)
+ if err != nil {
+ switch {
+ case errors.Is(err, common.ErrNotFound):
+ return nil, system.NewErrExporterNotFound(id)
+ default:
+ return nil, err
+ }
+ }
+ return exporter, nil
+}
+
+func (m *Manager) ListExporters(ctx context.Context) (*bunpaginate.Cursor[ledger.Exporter], error) {
+ return m.storage.ListExporters(ctx)
+}
+
+func (m *Manager) DeleteExporter(ctx context.Context, id string) error {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+
+ driver, ok := m.drivers[id]
+ if ok {
+ for id, config := range m.pipelines {
+ if config.pipeline.ExporterID == id {
+ if err := m.stopPipeline(ctx, id); err != nil {
+ return fmt.Errorf("stopping pipeline: %w", err)
+ }
+ }
+ }
+
+ m.stopDriver(ctx, driver)
+ }
+
+ if err := m.storage.DeleteExporter(ctx, id); err != nil {
+ switch {
+ case errors.Is(err, common.ErrNotFound):
+ return system.NewErrExporterNotFound(id)
+ default:
+ return err
+ }
+ }
+ return nil
+}
+
+func (m *Manager) ListPipelines(ctx context.Context) (*bunpaginate.Cursor[ledger.Pipeline], error) {
+ return m.storage.ListPipelines(ctx)
+}
+
+func (m *Manager) CreatePipeline(ctx context.Context, config ledger.PipelineConfiguration) (*ledger.Pipeline, error) {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+
+ pipeline := ledger.NewPipeline(config)
+
+ err := m.storage.CreatePipeline(ctx, pipeline)
+ if err != nil {
+ return nil, err
+ }
+
+ if _, err := m.startPipeline(ctx, pipeline); err != nil {
+ logging.FromContext(ctx).Error("starting pipeline %s: %s", pipeline.ID, err)
+ otlp.RecordError(ctx, err)
+ }
+
+ return &pipeline, nil
+}
+
+func (m *Manager) DeletePipeline(ctx context.Context, id string) error {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+
+ if err := m.stopPipeline(ctx, id); err != nil {
+ return err
+ }
+
+ if err := m.storage.DeletePipeline(ctx, id); err != nil {
+ if errors.Is(err, common.ErrNotFound) {
+ return ledger.NewErrPipelineNotFound(id)
+ }
+ return err
+ }
+
+ return nil
+}
+
+func (m *Manager) ResetPipeline(ctx context.Context, id string) error {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+
+ started := m.pipelines[id] != nil
+
+ if started {
+ if err := m.stopPipeline(ctx, id); err != nil {
+ return fmt.Errorf("stopping pipeline: %w", err)
+ }
+ }
+
+ pipeline, err := m.storage.UpdatePipeline(ctx, id, map[string]any{
+ "enabled": true,
+ "last_log_id": nil,
+ })
+ if err != nil {
+ if errors.Is(err, common.ErrNotFound) {
+ return ledger.NewErrPipelineNotFound(id)
+ }
+ return fmt.Errorf("updating pipeline: %w", err)
+ }
+
+ if started {
+ if _, err := m.startPipeline(ctx, *pipeline); err != nil {
+ logging.FromContext(ctx).Error("starting pipeline %s: %s", pipeline.ID, err)
+ }
+ }
+ return nil
+}
+
+func NewManager(
+ storageDriver Storage,
+ driverFactory drivers.Factory,
+ logger logging.Logger,
+ exportersConfigValidator ConfigValidator,
+ options ...Option,
+) *Manager {
+ ret := &Manager{
+ storage: storageDriver,
+ stopChannel: make(chan chan error, 1),
+ pipelines: map[string]*PipelineHandler{},
+ driverFactory: driverFactory,
+ drivers: map[string]*DriverFacade{},
+ logger: logger.WithField("component", "manager"),
+ exportersConfigValidator: exportersConfigValidator,
+ started: make(chan struct{}),
+ }
+
+ for _, option := range append(defaultOptions, options...) {
+ option(ret)
+ }
+
+ return ret
+}
+
+type Option func(r *Manager)
+
+func WithPipelineOptions(options ...PipelineOption) Option {
+ return func(r *Manager) {
+ r.pipelineOptions = append(r.pipelineOptions, options...)
+ }
+}
+
+func WithSyncPeriod(period time.Duration) Option {
+ return func(r *Manager) {
+ r.syncPeriod = period
+ }
+}
+
+var defaultOptions = []Option{
+ WithSyncPeriod(time.Minute),
+}
diff --git a/internal/replication/manager_test.go b/internal/replication/manager_test.go
new file mode 100644
index 0000000000..4ee2f5ddcb
--- /dev/null
+++ b/internal/replication/manager_test.go
@@ -0,0 +1,145 @@
+package replication
+
+import (
+ "context"
+ "github.com/formancehq/go-libs/v3/bun/bunpaginate"
+ "github.com/formancehq/go-libs/v3/pointer"
+ ledger "github.com/formancehq/ledger/internal"
+ "github.com/formancehq/ledger/internal/storage/common"
+ "testing"
+ "time"
+
+ "github.com/formancehq/go-libs/v3/logging"
+ "github.com/formancehq/ledger/internal/replication/drivers"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/mock/gomock"
+)
+
+func startRunner(
+ t *testing.T,
+ ctx context.Context,
+ storageDriver Storage,
+ driverFactory drivers.Factory,
+ exportersConfigValidator ConfigValidator,
+) *Manager {
+ t.Helper()
+
+ runner := NewManager(
+ storageDriver,
+ driverFactory,
+ logging.Testing(),
+ exportersConfigValidator,
+ )
+ go runner.Run(ctx)
+ t.Cleanup(func() {
+ ctx, cancel := context.WithTimeout(ctx, time.Second)
+ defer cancel()
+
+ require.NoError(t, runner.Stop(ctx))
+ })
+
+ return runner
+}
+
+func TestManager(t *testing.T) {
+ t.Parallel()
+
+ ctx := logging.TestingContext()
+ ctrl := gomock.NewController(t)
+ storage := NewMockStorage(ctrl)
+ logFetcher := NewMockLogFetcher(ctrl)
+ exporterConfigValidator := NewMockConfigValidator(ctrl)
+ exporterFactory := drivers.NewMockFactory(ctrl)
+ exporter := drivers.NewMockDriver(ctrl)
+
+ pipelineConfiguration := ledger.NewPipelineConfiguration("module1", "exporter")
+ pipeline := ledger.NewPipeline(pipelineConfiguration)
+
+ exporterFactory.EXPECT().
+ Create(gomock.Any(), pipelineConfiguration.ExporterID).
+ Return(exporter, nil, nil)
+ exporter.EXPECT().Start(gomock.Any()).Return(nil)
+
+ log := ledger.NewLog(ledger.CreatedTransaction{
+ Transaction: ledger.NewTransaction(),
+ })
+ log.ID = pointer.For(uint64(1))
+ deliver := make(chan struct{})
+ delivered := make(chan struct{})
+
+ logFetcher.EXPECT().
+ ListLogs(gomock.Any(), common.ColumnPaginatedQuery[any]{
+ PageSize: 100,
+ Column: "id",
+ Options: common.ResourceQuery[any]{},
+ Order: pointer.For(bunpaginate.Order(bunpaginate.OrderAsc)),
+ }).
+ AnyTimes().
+ DoAndReturn(func(ctx context.Context, paginatedQuery common.ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Log], error) {
+ select {
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ case <-deliver:
+ select {
+ case <-delivered:
+ default:
+ close(delivered)
+ return &bunpaginate.Cursor[ledger.Log]{
+ Data: []ledger.Log{log},
+ }, nil
+ }
+ }
+ return &bunpaginate.Cursor[ledger.Log]{}, nil
+ })
+
+ storage.EXPECT().
+ ListEnabledPipelines(gomock.Any()).
+ AnyTimes().
+ Return([]ledger.Pipeline{pipeline}, nil)
+
+ storage.EXPECT().
+ GetPipeline(gomock.Any(), pipeline.ID).
+ Return(&pipeline, nil)
+
+ storage.EXPECT().
+ OpenLedger(gomock.Any(), pipelineConfiguration.Ledger).
+ Return(logFetcher, &ledger.Ledger{}, nil)
+
+ storage.EXPECT().
+ StorePipelineState(gomock.Any(), pipeline.ID, uint64(1)).
+ Return(nil)
+
+ exporter.EXPECT().
+ Accept(gomock.Any(), drivers.NewLogWithLedger(pipelineConfiguration.Ledger, log)).
+ Return([]error{nil}, nil)
+
+ runner := startRunner(
+ t,
+ ctx,
+ storage,
+ exporterFactory,
+ exporterConfigValidator,
+ )
+ <-runner.Started()
+
+ err := runner.StartPipeline(ctx, pipeline.ID)
+ require.Error(t, err)
+
+ require.Eventually(t, func() bool {
+ return runner.GetDriver("exporter") != nil
+ }, 5*time.Second, 10*time.Millisecond)
+
+ select {
+ case <-runner.GetDriver("exporter").Ready():
+ case <-time.After(time.Second):
+ require.Fail(t, "exporter should be ready")
+ }
+
+ close(deliver)
+
+ require.Eventually(t, ctrl.Satisfied, 2*time.Second, 10*time.Millisecond)
+
+ // notes(gfyrag): add this expectation AFTER the previous Eventually.
+ // If configured before the Eventually, it will never finish as the stop call is made in a t.Cleanup defined earlier
+ exporter.EXPECT().Stop(gomock.Any()).Return(nil)
+}
diff --git a/internal/replication/mapping.go b/internal/replication/mapping.go
new file mode 100644
index 0000000000..c520b37c0b
--- /dev/null
+++ b/internal/replication/mapping.go
@@ -0,0 +1,99 @@
+package replication
+
+import (
+ "encoding/json"
+ "github.com/formancehq/go-libs/v3/bun/bunpaginate"
+ "github.com/formancehq/go-libs/v3/time"
+ ledger "github.com/formancehq/ledger/internal"
+ "github.com/formancehq/ledger/internal/replication/grpc"
+ "google.golang.org/protobuf/types/known/timestamppb"
+)
+
+func mapExporter(exporter ledger.Exporter) *grpc.Exporter {
+ return &grpc.Exporter{
+ Id: exporter.ID,
+ CreatedAt: ×tamppb.Timestamp{
+ Seconds: exporter.CreatedAt.Unix(),
+ Nanos: int32(exporter.CreatedAt.Nanosecond()),
+ },
+ Config: mapExporterConfiguration(exporter.ExporterConfiguration),
+ }
+}
+
+func mapPipelineConfiguration(cfg ledger.PipelineConfiguration) *grpc.PipelineConfiguration {
+ return &grpc.PipelineConfiguration{
+ ExporterId: cfg.ExporterID,
+ Ledger: cfg.Ledger,
+ }
+}
+
+func mapPipelineConfigurationFromGRPC(cfg *grpc.PipelineConfiguration) ledger.PipelineConfiguration {
+ return ledger.PipelineConfiguration{
+ ExporterID: cfg.ExporterId,
+ Ledger: cfg.Ledger,
+ }
+}
+
+func mapPipeline(pipeline ledger.Pipeline) *grpc.Pipeline {
+ return &grpc.Pipeline{
+ Config: mapPipelineConfiguration(pipeline.PipelineConfiguration),
+ CreatedAt: ×tamppb.Timestamp{
+ Seconds: pipeline.CreatedAt.Unix(),
+ Nanos: int32(pipeline.CreatedAt.Nanosecond()),
+ },
+ Id: pipeline.ID,
+ Enabled: pipeline.Enabled,
+ LastLogID: pipeline.LastLogID,
+ Error: pipeline.Error,
+ }
+}
+
+func mapPipelineFromGRPC(pipeline *grpc.Pipeline) ledger.Pipeline {
+ return ledger.Pipeline{
+ PipelineConfiguration: mapPipelineConfigurationFromGRPC(pipeline.Config),
+ CreatedAt: time.New(pipeline.CreatedAt.AsTime()),
+ ID: pipeline.Id,
+ Enabled: pipeline.Enabled,
+ LastLogID: pipeline.LastLogID,
+ Error: pipeline.Error,
+ }
+}
+
+func mapCursor[V any](ret *bunpaginate.Cursor[V]) *grpc.Cursor {
+ return &grpc.Cursor{
+ Next: ret.Next,
+ HasMore: ret.HasMore,
+ Prev: ret.Previous,
+ }
+}
+
+func mapCursorFromGRPC[V any](ret *grpc.Cursor, data []V) *bunpaginate.Cursor[V] {
+ return &bunpaginate.Cursor[V]{
+ Next: ret.Next,
+ HasMore: ret.HasMore,
+ Previous: ret.Prev,
+ Data: data,
+ }
+}
+
+func mapExporterFromGRPC(exporter *grpc.Exporter) ledger.Exporter {
+ return ledger.Exporter{
+ ID: exporter.Id,
+ CreatedAt: time.New(exporter.CreatedAt.AsTime()),
+ ExporterConfiguration: mapExporterConfigurationFromGRPC(exporter.Config),
+ }
+}
+
+func mapExporterConfigurationFromGRPC(from *grpc.ExporterConfiguration) ledger.ExporterConfiguration {
+ return ledger.ExporterConfiguration{
+ Driver: from.Driver,
+ Config: json.RawMessage(from.Config),
+ }
+}
+
+func mapExporterConfiguration(configuration ledger.ExporterConfiguration) *grpc.ExporterConfiguration {
+ return &grpc.ExporterConfiguration{
+ Driver: configuration.Driver,
+ Config: string(configuration.Config),
+ }
+}
diff --git a/internal/replication/module.go b/internal/replication/module.go
new file mode 100644
index 0000000000..30153a0910
--- /dev/null
+++ b/internal/replication/module.go
@@ -0,0 +1,100 @@
+package replication
+
+import (
+ "context"
+ "github.com/formancehq/go-libs/v3/logging"
+ "github.com/formancehq/ledger/internal/controller/system"
+ "github.com/formancehq/ledger/internal/replication/drivers"
+ innergrpc "github.com/formancehq/ledger/internal/replication/grpc"
+ "go.uber.org/fx"
+ "google.golang.org/grpc"
+ "time"
+)
+
+type WorkerModuleConfig struct {
+ PushRetryPeriod time.Duration
+ PullInterval time.Duration
+ SyncPeriod time.Duration
+ LogsPageSize uint64
+}
+
+// NewWorkerFXModule create a new fx module
+func NewWorkerFXModule(cfg WorkerModuleConfig) fx.Option {
+ return fx.Options(
+ fx.Provide(fx.Annotate(NewStorageAdapter, fx.As(new(Storage)))),
+ fx.Provide(func(
+ storageDriver Storage,
+ driverFactory drivers.Factory,
+ exportersConfigValidator ConfigValidator,
+ logger logging.Logger,
+ ) *Manager {
+ options := make([]Option, 0)
+ if cfg.PushRetryPeriod > 0 {
+ options = append(options, WithPipelineOptions(
+ WithPushRetryPeriod(cfg.PushRetryPeriod),
+ ))
+ }
+ if cfg.PullInterval > 0 {
+ options = append(options, WithPipelineOptions(
+ WithPullPeriod(cfg.PullInterval),
+ ))
+ }
+ if cfg.LogsPageSize > 0 {
+ options = append(options, WithPipelineOptions(
+ WithLogsPageSize(cfg.LogsPageSize),
+ ))
+ }
+ if cfg.SyncPeriod > 0 {
+ options = append(options, WithSyncPeriod(cfg.SyncPeriod))
+ }
+ return NewManager(
+ storageDriver,
+ driverFactory,
+ logger,
+ exportersConfigValidator,
+ options...,
+ )
+ }),
+ fx.Provide(func(registry *drivers.Registry) drivers.Factory {
+ return registry
+ }),
+ // decorate the original Factory (implemented by *Registry)
+ // to abstract the fact we want to batch logs
+ fx.Decorate(fx.Annotate(
+ drivers.NewWithBatchingDriverFactory,
+ fx.As(new(drivers.Factory)),
+ )),
+ fx.Provide(fx.Annotate(NewReplicationServiceImpl, fx.As(new(innergrpc.ReplicationServer)))),
+ fx.Provide(func(driversRegistry *drivers.Registry) ConfigValidator {
+ return driversRegistry
+ }),
+ fx.Invoke(func(lc fx.Lifecycle, runner *Manager) {
+ lc.Append(fx.Hook{
+ OnStart: func(ctx context.Context) error {
+ go runner.Run(context.WithoutCancel(ctx))
+ return nil
+ },
+ OnStop: func(ctx context.Context) error {
+ return runner.Stop(ctx)
+ },
+ })
+ }),
+ )
+}
+
+func NewFXGRPCClientModule() fx.Option {
+ return fx.Options(
+ fx.Provide(func(conn *grpc.ClientConn) innergrpc.ReplicationClient {
+ return innergrpc.NewReplicationClient(conn)
+ }),
+ fx.Provide(fx.Annotate(NewThroughGRPCBackend, fx.As(new(system.ReplicationBackend)))),
+ )
+}
+
+func NewFXEmbeddedClientModule() fx.Option {
+ return fx.Options(
+ fx.Provide(func(manager *Manager) system.ReplicationBackend {
+ return manager
+ }),
+ )
+}
diff --git a/internal/replication/pipeline.go b/internal/replication/pipeline.go
new file mode 100644
index 0000000000..576f8be254
--- /dev/null
+++ b/internal/replication/pipeline.go
@@ -0,0 +1,177 @@
+package replication
+
+import (
+ "context"
+ "github.com/formancehq/go-libs/v3/bun/bunpaginate"
+ "github.com/formancehq/go-libs/v3/collectionutils"
+ "github.com/formancehq/go-libs/v3/pointer"
+ "github.com/formancehq/go-libs/v3/query"
+ "github.com/formancehq/ledger/internal"
+ "github.com/formancehq/ledger/internal/storage/common"
+ "time"
+
+ "github.com/formancehq/go-libs/v3/logging"
+ "github.com/formancehq/ledger/internal/replication/drivers"
+)
+
+var (
+ DefaultPullInterval = 10 * time.Second
+ DefaultPushRetryPeriod = 10 * time.Second
+)
+
+type PipelineHandlerConfig struct {
+ PullInterval time.Duration
+ PushRetryPeriod time.Duration
+ LogsPageSize uint64
+}
+
+type PipelineOption func(config *PipelineHandlerConfig)
+
+func WithPullPeriod(v time.Duration) PipelineOption {
+ return func(config *PipelineHandlerConfig) {
+ config.PullInterval = v
+ }
+}
+
+func WithPushRetryPeriod(v time.Duration) PipelineOption {
+ return func(config *PipelineHandlerConfig) {
+ config.PushRetryPeriod = v
+ }
+}
+
+func WithLogsPageSize(v uint64) PipelineOption {
+ return func(config *PipelineHandlerConfig) {
+ config.LogsPageSize = v
+ }
+}
+
+var (
+ defaultPipelineOptions = []PipelineOption{
+ WithPullPeriod(DefaultPullInterval),
+ WithPushRetryPeriod(DefaultPushRetryPeriod),
+ WithLogsPageSize(100),
+ }
+)
+
+type PipelineHandler struct {
+ pipeline ledger.Pipeline
+ stopChannel chan chan error
+ store LogFetcher
+ exporter drivers.Driver
+ pipelineConfig PipelineHandlerConfig
+ logger logging.Logger
+}
+
+func (p *PipelineHandler) Run(ctx context.Context, ingestedLogs chan uint64) {
+ nextInterval := time.Duration(0)
+ for {
+ select {
+ case ch := <-p.stopChannel:
+ close(ch)
+ return
+ case <-time.After(nextInterval):
+ var builder query.Builder
+ if p.pipeline.LastLogID != nil {
+ builder = query.Gt("id", *p.pipeline.LastLogID)
+ }
+ logs, err := p.store.ListLogs(ctx, common.ColumnPaginatedQuery[any]{
+ PageSize: p.pipelineConfig.LogsPageSize,
+ Column: "id",
+ Options: common.ResourceQuery[any]{
+ Builder: builder,
+ },
+ Order: pointer.For(bunpaginate.Order(bunpaginate.OrderAsc)),
+ })
+ if err != nil {
+ p.logger.Errorf("Error fetching logs: %s", err)
+ select {
+ case <-ctx.Done():
+ return
+ case <-time.After(p.pipelineConfig.PullInterval):
+ continue
+ }
+ }
+
+ if len(logs.Data) == 0 {
+ nextInterval = p.pipelineConfig.PullInterval
+ continue
+ }
+
+ for {
+ _, err := p.exporter.Accept(ctx, collectionutils.Map(logs.Data, func(log ledger.Log) drivers.LogWithLedger {
+ return drivers.LogWithLedger{
+ Log: log,
+ Ledger: p.pipeline.Ledger,
+ }
+ })...)
+ if err != nil {
+ p.logger.Errorf("Error pushing data on exporter: %s, waiting for: %s", err, p.pipelineConfig.PushRetryPeriod)
+ select {
+ case <-ctx.Done():
+ return
+ case <-time.After(p.pipelineConfig.PushRetryPeriod):
+ continue
+ }
+ }
+ break
+ }
+
+ lastLogID := logs.Data[len(logs.Data)-1].ID
+ p.pipeline.LastLogID = lastLogID
+
+ select {
+ case <-ctx.Done():
+ return
+ case ingestedLogs <- *lastLogID:
+ }
+
+ if !logs.HasMore {
+ nextInterval = p.pipelineConfig.PullInterval
+ } else {
+ nextInterval = 0
+ }
+ }
+ }
+}
+
+func (p *PipelineHandler) Shutdown(ctx context.Context) error {
+ p.logger.Infof("shutdowning pipeline")
+ errorChannel := make(chan error, 1)
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case p.stopChannel <- errorChannel:
+ p.logger.Debugf("shutdowning pipeline signal sent")
+ select {
+ case err := <-errorChannel:
+ return err
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+ }
+}
+
+func NewPipelineHandler(
+ pipeline ledger.Pipeline,
+ store LogFetcher,
+ driver drivers.Driver,
+ logger logging.Logger,
+ opts ...PipelineOption,
+) *PipelineHandler {
+ config := PipelineHandlerConfig{}
+ for _, opt := range append(defaultPipelineOptions, opts...) {
+ opt(&config)
+ }
+
+ return &PipelineHandler{
+ pipeline: pipeline,
+ stopChannel: make(chan chan error, 1),
+ store: store,
+ exporter: driver,
+ pipelineConfig: config,
+ logger: logger.
+ WithField("component", "pipeline").
+ WithField("module", pipeline.Ledger).
+ WithField("driver", pipeline.ExporterID),
+ }
+}
diff --git a/internal/replication/pipeline_test.go b/internal/replication/pipeline_test.go
new file mode 100644
index 0000000000..f27275801e
--- /dev/null
+++ b/internal/replication/pipeline_test.go
@@ -0,0 +1,95 @@
+package replication
+
+import (
+ "context"
+ "github.com/formancehq/go-libs/v3/bun/bunpaginate"
+ "github.com/formancehq/go-libs/v3/pointer"
+ ledger "github.com/formancehq/ledger/internal"
+ "github.com/formancehq/ledger/internal/storage/common"
+ "testing"
+ "time"
+
+ "github.com/formancehq/ledger/internal/replication/drivers"
+
+ "github.com/formancehq/go-libs/v3/logging"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/mock/gomock"
+)
+
+func runPipeline(t *testing.T, ctx context.Context, pipeline ledger.Pipeline, store LogFetcher, driver drivers.Driver) (*PipelineHandler, <-chan uint64) {
+ t.Helper()
+
+ handler := NewPipelineHandler(
+ pipeline,
+ store,
+ driver,
+ logging.Testing(),
+ )
+
+ lastLogIDChannel := make(chan uint64)
+
+ go handler.Run(ctx, lastLogIDChannel)
+ t.Cleanup(func() {
+ require.NoError(t, handler.Shutdown(ctx))
+ })
+
+ return handler, lastLogIDChannel
+}
+
+func TestPipeline(t *testing.T) {
+ t.Parallel()
+
+ ctx := logging.TestingContext()
+ ctrl := gomock.NewController(t)
+ logFetcher := NewMockLogFetcher(ctrl)
+ driver := drivers.NewMockDriver(ctrl)
+ log := ledger.NewLog(
+ ledger.CreatedTransaction{
+ Transaction: ledger.NewTransaction(),
+ },
+ )
+ log.ID = pointer.For(uint64(1))
+
+ deliver := make(chan struct{})
+ delivered := make(chan struct{})
+
+ logFetcher.EXPECT().
+ ListLogs(gomock.Any(), common.ColumnPaginatedQuery[any]{
+ PageSize: 100,
+ Column: "id",
+ Options: common.ResourceQuery[any]{},
+ Order: pointer.For(bunpaginate.Order(bunpaginate.OrderAsc)),
+ }).
+ AnyTimes().
+ DoAndReturn(func(ctx context.Context, paginatedQuery common.ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Log], error) {
+ select {
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ case <-deliver:
+ select {
+ case <-delivered:
+ default:
+ close(delivered)
+ return &bunpaginate.Cursor[ledger.Log]{
+ Data: []ledger.Log{log},
+ }, nil
+ }
+ }
+ return &bunpaginate.Cursor[ledger.Log]{}, nil
+ })
+
+ driver.EXPECT().
+ Accept(gomock.Any(), drivers.NewLogWithLedger("testing", log)).
+ Return([]error{nil}, nil)
+
+ pipelineConfiguration := ledger.NewPipelineConfiguration("testing", "testing")
+ pipeline := ledger.NewPipeline(pipelineConfiguration)
+
+ _, lastLogIDChannel := runPipeline(t, ctx, pipeline, logFetcher, driver)
+
+ close(deliver)
+
+ ShouldReceive(t, 1, lastLogIDChannel)
+
+ require.Eventually(t, ctrl.Satisfied, time.Second, 10*time.Millisecond)
+}
diff --git a/internal/replication/store.go b/internal/replication/store.go
new file mode 100644
index 0000000000..543bb71509
--- /dev/null
+++ b/internal/replication/store.go
@@ -0,0 +1,75 @@
+package replication
+
+import (
+ "context"
+ "github.com/formancehq/go-libs/v3/bun/bunpaginate"
+ ledger "github.com/formancehq/ledger/internal"
+ "github.com/formancehq/ledger/internal/storage/common"
+ "github.com/formancehq/ledger/internal/storage/driver"
+ systemstore "github.com/formancehq/ledger/internal/storage/system"
+)
+
+//go:generate mockgen -write_source_comment=false -write_package_comment=false -source store.go -destination store_generated_test.go -package replication . LogFetcher
+
+type LogFetcher interface {
+ ListLogs(ctx context.Context, query common.ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Log], error)
+}
+
+type LogFetcherFn func(ctx context.Context, query common.ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Log], error)
+
+func (fn LogFetcherFn) ListLogs(ctx context.Context, query common.ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Log], error) {
+ return fn(ctx, query)
+}
+
+//go:generate mockgen -write_source_comment=false -write_package_comment=false -source store.go -destination store_generated_test.go -package replication . StorageDriver
+
+type Storage interface {
+ OpenLedger(context.Context, string) (LogFetcher, *ledger.Ledger, error)
+ StorePipelineState(ctx context.Context, id string, lastLogID uint64) error
+
+ ListExporters(ctx context.Context) (*bunpaginate.Cursor[ledger.Exporter], error)
+ CreateExporter(ctx context.Context, exporter ledger.Exporter) error
+ DeleteExporter(ctx context.Context, id string) error
+ GetExporter(ctx context.Context, id string) (*ledger.Exporter, error)
+
+ CreatePipeline(ctx context.Context, pipeline ledger.Pipeline) error
+ DeletePipeline(ctx context.Context, id string) error
+ UpdatePipeline(ctx context.Context, id string, o map[string]any) (*ledger.Pipeline, error)
+ ListPipelines(ctx context.Context) (*bunpaginate.Cursor[ledger.Pipeline], error)
+ ListEnabledPipelines(ctx context.Context) ([]ledger.Pipeline, error)
+ GetPipeline(ctx context.Context, id string) (*ledger.Pipeline, error)
+}
+
+type storageAdapter struct {
+ *systemstore.DefaultStore
+ storageDriver *driver.Driver
+}
+
+func (s *storageAdapter) GetPipeline(ctx context.Context, id string) (*ledger.Pipeline, error) {
+ return s.DefaultStore.GetPipeline(ctx, id)
+}
+
+func (s *storageAdapter) OpenLedger(ctx context.Context, name string) (LogFetcher, *ledger.Ledger, error) {
+ store, l, err := s.storageDriver.OpenLedger(ctx, name)
+
+ return LogFetcherFn(func(ctx context.Context, query common.ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Log], error) {
+ return store.Logs().Paginate(ctx, query)
+ }), l, err
+}
+
+func (s *storageAdapter) StorePipelineState(ctx context.Context, id string, lastLogID uint64) error {
+ return s.DefaultStore.StorePipelineState(ctx, id, lastLogID)
+}
+
+func (s *storageAdapter) ListEnabledPipelines(ctx context.Context) ([]ledger.Pipeline, error) {
+ return s.DefaultStore.ListEnabledPipelines(ctx)
+}
+
+func NewStorageAdapter(storageDriver *driver.Driver, systemStore *systemstore.DefaultStore) Storage {
+ return &storageAdapter{
+ storageDriver: storageDriver,
+ DefaultStore: systemStore,
+ }
+}
+
+var _ Storage = (*storageAdapter)(nil)
diff --git a/internal/replication/store_generated_test.go b/internal/replication/store_generated_test.go
new file mode 100644
index 0000000000..4633f1987e
--- /dev/null
+++ b/internal/replication/store_generated_test.go
@@ -0,0 +1,257 @@
+// Code generated by MockGen. DO NOT EDIT.
+//
+// Generated by this command:
+//
+// mockgen -write_source_comment=false -write_package_comment=false -source store.go -destination store_generated_test.go -package replication . StorageDriver
+//
+
+package replication
+
+import (
+ context "context"
+ reflect "reflect"
+
+ bunpaginate "github.com/formancehq/go-libs/v3/bun/bunpaginate"
+ ledger "github.com/formancehq/ledger/internal"
+ common "github.com/formancehq/ledger/internal/storage/common"
+ gomock "go.uber.org/mock/gomock"
+)
+
+// MockLogFetcher is a mock of LogFetcher interface.
+type MockLogFetcher struct {
+ ctrl *gomock.Controller
+ recorder *MockLogFetcherMockRecorder
+ isgomock struct{}
+}
+
+// MockLogFetcherMockRecorder is the mock recorder for MockLogFetcher.
+type MockLogFetcherMockRecorder struct {
+ mock *MockLogFetcher
+}
+
+// NewMockLogFetcher creates a new mock instance.
+func NewMockLogFetcher(ctrl *gomock.Controller) *MockLogFetcher {
+ mock := &MockLogFetcher{ctrl: ctrl}
+ mock.recorder = &MockLogFetcherMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockLogFetcher) EXPECT() *MockLogFetcherMockRecorder {
+ return m.recorder
+}
+
+// ListLogs mocks base method.
+func (m *MockLogFetcher) ListLogs(ctx context.Context, query common.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])
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ListLogs indicates an expected call of ListLogs.
+func (mr *MockLogFetcherMockRecorder) ListLogs(ctx, query any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLogs", reflect.TypeOf((*MockLogFetcher)(nil).ListLogs), ctx, query)
+}
+
+// MockStorage is a mock of Storage interface.
+type MockStorage struct {
+ ctrl *gomock.Controller
+ recorder *MockStorageMockRecorder
+ isgomock struct{}
+}
+
+// MockStorageMockRecorder is the mock recorder for MockStorage.
+type MockStorageMockRecorder struct {
+ mock *MockStorage
+}
+
+// NewMockStorage creates a new mock instance.
+func NewMockStorage(ctrl *gomock.Controller) *MockStorage {
+ mock := &MockStorage{ctrl: ctrl}
+ mock.recorder = &MockStorageMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockStorage) EXPECT() *MockStorageMockRecorder {
+ return m.recorder
+}
+
+// CreateExporter mocks base method.
+func (m *MockStorage) CreateExporter(ctx context.Context, exporter ledger.Exporter) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "CreateExporter", ctx, exporter)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// CreateExporter indicates an expected call of CreateExporter.
+func (mr *MockStorageMockRecorder) CreateExporter(ctx, exporter any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateExporter", reflect.TypeOf((*MockStorage)(nil).CreateExporter), ctx, exporter)
+}
+
+// CreatePipeline mocks base method.
+func (m *MockStorage) CreatePipeline(ctx context.Context, pipeline ledger.Pipeline) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "CreatePipeline", ctx, pipeline)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// CreatePipeline indicates an expected call of CreatePipeline.
+func (mr *MockStorageMockRecorder) CreatePipeline(ctx, pipeline any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePipeline", reflect.TypeOf((*MockStorage)(nil).CreatePipeline), ctx, pipeline)
+}
+
+// DeleteExporter mocks base method.
+func (m *MockStorage) DeleteExporter(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "DeleteExporter", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// DeleteExporter indicates an expected call of DeleteExporter.
+func (mr *MockStorageMockRecorder) DeleteExporter(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteExporter", reflect.TypeOf((*MockStorage)(nil).DeleteExporter), ctx, id)
+}
+
+// DeletePipeline mocks base method.
+func (m *MockStorage) DeletePipeline(ctx context.Context, id string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "DeletePipeline", ctx, id)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// DeletePipeline indicates an expected call of DeletePipeline.
+func (mr *MockStorageMockRecorder) DeletePipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePipeline", reflect.TypeOf((*MockStorage)(nil).DeletePipeline), ctx, id)
+}
+
+// GetExporter mocks base method.
+func (m *MockStorage) GetExporter(ctx context.Context, id string) (*ledger.Exporter, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetExporter", ctx, id)
+ ret0, _ := ret[0].(*ledger.Exporter)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetExporter indicates an expected call of GetExporter.
+func (mr *MockStorageMockRecorder) GetExporter(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetExporter", reflect.TypeOf((*MockStorage)(nil).GetExporter), ctx, id)
+}
+
+// GetPipeline mocks base method.
+func (m *MockStorage) GetPipeline(ctx context.Context, id string) (*ledger.Pipeline, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetPipeline", ctx, id)
+ ret0, _ := ret[0].(*ledger.Pipeline)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetPipeline indicates an expected call of GetPipeline.
+func (mr *MockStorageMockRecorder) GetPipeline(ctx, id any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPipeline", reflect.TypeOf((*MockStorage)(nil).GetPipeline), ctx, id)
+}
+
+// ListEnabledPipelines mocks base method.
+func (m *MockStorage) ListEnabledPipelines(ctx context.Context) ([]ledger.Pipeline, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ListEnabledPipelines", ctx)
+ ret0, _ := ret[0].([]ledger.Pipeline)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ListEnabledPipelines indicates an expected call of ListEnabledPipelines.
+func (mr *MockStorageMockRecorder) ListEnabledPipelines(ctx any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListEnabledPipelines", reflect.TypeOf((*MockStorage)(nil).ListEnabledPipelines), ctx)
+}
+
+// ListExporters mocks base method.
+func (m *MockStorage) ListExporters(ctx context.Context) (*bunpaginate.Cursor[ledger.Exporter], error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ListExporters", ctx)
+ ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Exporter])
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ListExporters indicates an expected call of ListExporters.
+func (mr *MockStorageMockRecorder) ListExporters(ctx any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListExporters", reflect.TypeOf((*MockStorage)(nil).ListExporters), ctx)
+}
+
+// ListPipelines mocks base method.
+func (m *MockStorage) ListPipelines(ctx context.Context) (*bunpaginate.Cursor[ledger.Pipeline], error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ListPipelines", ctx)
+ ret0, _ := ret[0].(*bunpaginate.Cursor[ledger.Pipeline])
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ListPipelines indicates an expected call of ListPipelines.
+func (mr *MockStorageMockRecorder) ListPipelines(ctx any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPipelines", reflect.TypeOf((*MockStorage)(nil).ListPipelines), ctx)
+}
+
+// OpenLedger mocks base method.
+func (m *MockStorage) OpenLedger(arg0 context.Context, arg1 string) (LogFetcher, *ledger.Ledger, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "OpenLedger", arg0, arg1)
+ ret0, _ := ret[0].(LogFetcher)
+ ret1, _ := ret[1].(*ledger.Ledger)
+ ret2, _ := ret[2].(error)
+ return ret0, ret1, ret2
+}
+
+// OpenLedger indicates an expected call of OpenLedger.
+func (mr *MockStorageMockRecorder) OpenLedger(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenLedger", reflect.TypeOf((*MockStorage)(nil).OpenLedger), arg0, arg1)
+}
+
+// StorePipelineState mocks base method.
+func (m *MockStorage) StorePipelineState(ctx context.Context, id string, lastLogID uint64) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "StorePipelineState", ctx, id, lastLogID)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// StorePipelineState indicates an expected call of StorePipelineState.
+func (mr *MockStorageMockRecorder) StorePipelineState(ctx, id, lastLogID any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StorePipelineState", reflect.TypeOf((*MockStorage)(nil).StorePipelineState), ctx, id, lastLogID)
+}
+
+// UpdatePipeline mocks base method.
+func (m *MockStorage) UpdatePipeline(ctx context.Context, id string, o map[string]any) (*ledger.Pipeline, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "UpdatePipeline", ctx, id, o)
+ ret0, _ := ret[0].(*ledger.Pipeline)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// UpdatePipeline indicates an expected call of UpdatePipeline.
+func (mr *MockStorageMockRecorder) UpdatePipeline(ctx, id, o any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePipeline", reflect.TypeOf((*MockStorage)(nil).UpdatePipeline), ctx, id, o)
+}
diff --git a/internal/replication/utils_test.go b/internal/replication/utils_test.go
new file mode 100644
index 0000000000..7cea858252
--- /dev/null
+++ b/internal/replication/utils_test.go
@@ -0,0 +1,22 @@
+package replication
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+)
+
+func ShouldReceive[T any](t *testing.T, expected T, ch <-chan T) {
+ t.Helper()
+
+ require.Eventually(t, func() bool {
+ select {
+ case item := <-ch:
+ require.Equal(t, expected, item)
+ return true
+ default:
+ return false
+ }
+ }, time.Second, 20*time.Millisecond)
+}
diff --git a/internal/storage/common/errors.go b/internal/storage/common/errors.go
index 5ca6ab7c80..ff51df34ab 100644
--- a/internal/storage/common/errors.go
+++ b/internal/storage/common/errors.go
@@ -1,6 +1,11 @@
package common
-import "fmt"
+import (
+ "database/sql"
+ "fmt"
+)
+
+var ErrNotFound = sql.ErrNoRows
type ErrInvalidQuery struct {
msg string
diff --git a/internal/storage/driver/adapters.go b/internal/storage/driver/adapters.go
deleted file mode 100644
index de9ef7341b..0000000000
--- a/internal/storage/driver/adapters.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package driver
-
-import (
- "context"
- ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
-
- ledger "github.com/formancehq/ledger/internal"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
- systemcontroller "github.com/formancehq/ledger/internal/controller/system"
-)
-
-type DefaultStorageDriverAdapter struct {
- *Driver
-}
-
-func (d *DefaultStorageDriverAdapter) OpenLedger(ctx context.Context, name string) (ledgercontroller.Store, *ledger.Ledger, error) {
- store, l, err := d.Driver.OpenLedger(ctx, name)
- if err != nil {
- return nil, nil, err
- }
-
- return ledgerstore.NewDefaultStoreAdapter(store), l, nil
-}
-
-func (d *DefaultStorageDriverAdapter) CreateLedger(ctx context.Context, l *ledger.Ledger) error {
- _, err := d.Driver.CreateLedger(ctx, l)
- return err
-}
-
-func NewControllerStorageDriverAdapter(d *Driver) *DefaultStorageDriverAdapter {
- return &DefaultStorageDriverAdapter{Driver: d}
-}
-
-var _ systemcontroller.Store = (*DefaultStorageDriverAdapter)(nil)
diff --git a/internal/storage/driver/driver.go b/internal/storage/driver/driver.go
index 549dd561ef..e12fce4d48 100644
--- a/internal/storage/driver/driver.go
+++ b/internal/storage/driver/driver.go
@@ -18,12 +18,13 @@ import (
"github.com/formancehq/go-libs/v3/metadata"
"github.com/formancehq/go-libs/v3/platform/postgres"
ledger "github.com/formancehq/ledger/internal"
- systemcontroller "github.com/formancehq/ledger/internal/controller/system"
"github.com/formancehq/ledger/internal/storage/bucket"
ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
"github.com/uptrace/bun"
)
+var ErrBucketOutdated = errors.New("bucket is outdated, you need to upgrade it before adding a new ledger")
+
type Driver struct {
ledgerStoreFactory ledgerstore.Factory
db *bun.DB
@@ -43,9 +44,9 @@ func (d *Driver) CreateLedger(ctx context.Context, l *ledger.Ledger) (*ledgersto
if err := systemStore.CreateLedger(ctx, l); err != nil {
if errors.Is(postgres.ResolveError(err), postgres.ErrConstraintsFailed{}) {
- return systemcontroller.ErrLedgerAlreadyExists
+ return systemstore.ErrLedgerAlreadyExists
}
- return err
+ return postgres.ResolveError(err)
}
b := d.bucketFactory.Create(l.Bucket)
@@ -60,7 +61,7 @@ func (d *Driver) CreateLedger(ctx context.Context, l *ledger.Ledger) (*ledgersto
}
if !upToDate {
- return systemcontroller.ErrBucketOutdated
+ return ErrBucketOutdated
}
if err := b.AddLedger(ctx, tx, *l); err != nil {
@@ -180,7 +181,7 @@ func (d *Driver) DeleteLedgerMetadata(ctx context.Context, name string, key stri
return d.systemStoreFactory.Create(d.db).DeleteLedgerMetadata(ctx, name, key)
}
-func (d *Driver) ListLedgers(ctx context.Context, q common.ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Ledger], error) {
+func (d *Driver) ListLedgers(ctx context.Context, q common.ColumnPaginatedQuery[systemstore.ListLedgersQueryPayload]) (*bunpaginate.Cursor[ledger.Ledger], error) {
return d.systemStoreFactory.Create(d.db).Ledgers().Paginate(ctx, q)
}
diff --git a/internal/storage/driver/driver_test.go b/internal/storage/driver/driver_test.go
index b3ff78bece..5aff40705b 100644
--- a/internal/storage/driver/driver_test.go
+++ b/internal/storage/driver/driver_test.go
@@ -8,11 +8,10 @@ import (
"github.com/formancehq/go-libs/v3/metadata"
"github.com/formancehq/go-libs/v3/query"
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"
- "github.com/formancehq/ledger/internal/storage/system"
+ systemstore "github.com/formancehq/ledger/internal/storage/system"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"math/rand"
@@ -28,7 +27,7 @@ func TestLedgersCreate(t *testing.T) {
db,
ledgerstore.NewFactory(db),
bucket.NewDefaultFactory(),
- system.NewStoreFactory(),
+ systemstore.NewStoreFactory(),
)
buckets := []string{"bucket1", "bucket2"}
@@ -81,7 +80,7 @@ func TestLedgersList(t *testing.T) {
db,
ledgerstore.NewFactory(db),
bucket.NewDefaultFactory(),
- system.NewStoreFactory(),
+ systemstore.NewStoreFactory(),
)
bucket := uuid.NewString()[:8]
@@ -102,7 +101,7 @@ func TestLedgersList(t *testing.T) {
_, err = d.CreateLedger(ctx, l2)
require.NoError(t, err)
- q := ledgercontroller.NewListLedgersQuery(15)
+ q := systemstore.NewListLedgersQuery(15)
q.Options.Builder = query.Match("bucket", bucket)
cursor, err := d.ListLedgers(ctx, q)
@@ -120,7 +119,7 @@ func TestLedgerUpdateMetadata(t *testing.T) {
db,
ledgerstore.NewFactory(db),
bucket.NewDefaultFactory(),
- system.NewStoreFactory(),
+ systemstore.NewStoreFactory(),
)
l := ledger.MustNewWithDefault(uuid.NewString())
@@ -142,7 +141,7 @@ func TestLedgerDeleteMetadata(t *testing.T) {
db,
ledgerstore.NewFactory(db),
bucket.NewDefaultFactory(),
- system.NewStoreFactory(),
+ systemstore.NewStoreFactory(),
)
l := ledger.MustNewWithDefault(uuid.NewString()).WithMetadata(metadata.Metadata{
diff --git a/internal/storage/driver/mocks.go b/internal/storage/driver/mocks.go
index ef937ef5ff..c92092cdb9 100644
--- a/internal/storage/driver/mocks.go
+++ b/internal/storage/driver/mocks.go
@@ -1,6 +1,5 @@
//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/module.go b/internal/storage/driver/module.go
index 938bacc903..e334551cb8 100644
--- a/internal/storage/driver/module.go
+++ b/internal/storage/driver/module.go
@@ -8,8 +8,6 @@ import (
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/trace"
- systemcontroller "github.com/formancehq/ledger/internal/controller/system"
-
"github.com/uptrace/bun"
"github.com/formancehq/go-libs/v3/logging"
@@ -53,7 +51,6 @@ func NewFXModule() fx.Option {
WithTracer(tracerProvider.Tracer("StorageDriver")),
), nil
}),
- fx.Provide(fx.Annotate(NewControllerStorageDriverAdapter, fx.As(new(systemcontroller.Store)))),
fx.Invoke(func(driver *Driver, lifecycle fx.Lifecycle, logger logging.Logger) error {
lifecycle.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
diff --git a/internal/storage/driver/store.go b/internal/storage/driver/store.go
new file mode 100644
index 0000000000..d7966f37dc
--- /dev/null
+++ b/internal/storage/driver/store.go
@@ -0,0 +1,23 @@
+package driver
+
+import (
+ "context"
+ "github.com/formancehq/go-libs/v3/metadata"
+ "github.com/formancehq/go-libs/v3/migrations"
+ "github.com/formancehq/ledger/internal"
+)
+
+//go:generate mockgen -write_source_comment=false -write_package_comment=false -source store.go -destination store_generated_test.go -package driver_test . SystemStore
+
+type SystemStore 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 systemstore.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
+ IsUpToDate(ctx context.Context) (bool, error)
+}
diff --git a/internal/storage/driver/system_generated_test.go b/internal/storage/driver/store_generated_test.go
similarity index 51%
rename from internal/storage/driver/system_generated_test.go
rename to internal/storage/driver/store_generated_test.go
index 0e24c5ebff..2a76e6fd1c 100644
--- a/internal/storage/driver/system_generated_test.go
+++ b/internal/storage/driver/store_generated_test.go
@@ -2,10 +2,10 @@
//
// 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
+// mockgen -write_source_comment=false -write_package_comment=false -source store.go -destination store_generated_test.go -package driver_test . SystemStore
//
-package driver
+package driver_test
import (
context "context"
@@ -14,36 +14,35 @@ import (
metadata "github.com/formancehq/go-libs/v3/metadata"
migrations "github.com/formancehq/go-libs/v3/migrations"
ledger "github.com/formancehq/ledger/internal"
- common "github.com/formancehq/ledger/internal/storage/common"
gomock "go.uber.org/mock/gomock"
)
-// SystemStore is a mock of Store interface.
-type SystemStore struct {
+// MockSystemStore is a mock of SystemStore interface.
+type MockSystemStore struct {
ctrl *gomock.Controller
- recorder *SystemStoreMockRecorder
+ recorder *MockSystemStoreMockRecorder
isgomock struct{}
}
-// SystemStoreMockRecorder is the mock recorder for SystemStore.
-type SystemStoreMockRecorder struct {
- mock *SystemStore
+// MockSystemStoreMockRecorder is the mock recorder for MockSystemStore.
+type MockSystemStoreMockRecorder struct {
+ mock *MockSystemStore
}
-// NewSystemStore creates a new mock instance.
-func NewSystemStore(ctrl *gomock.Controller) *SystemStore {
- mock := &SystemStore{ctrl: ctrl}
- mock.recorder = &SystemStoreMockRecorder{mock}
+// NewMockSystemStore creates a new mock instance.
+func NewMockSystemStore(ctrl *gomock.Controller) *MockSystemStore {
+ mock := &MockSystemStore{ctrl: ctrl}
+ mock.recorder = &MockSystemStoreMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
-func (m *SystemStore) EXPECT() *SystemStoreMockRecorder {
+func (m *MockSystemStore) EXPECT() *MockSystemStoreMockRecorder {
return m.recorder
}
// CreateLedger mocks base method.
-func (m *SystemStore) CreateLedger(ctx context.Context, l *ledger.Ledger) error {
+func (m *MockSystemStore) CreateLedger(ctx context.Context, l *ledger.Ledger) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateLedger", ctx, l)
ret0, _ := ret[0].(error)
@@ -51,13 +50,13 @@ func (m *SystemStore) CreateLedger(ctx context.Context, l *ledger.Ledger) error
}
// CreateLedger indicates an expected call of CreateLedger.
-func (mr *SystemStoreMockRecorder) CreateLedger(ctx, l any) *gomock.Call {
+func (mr *MockSystemStoreMockRecorder) 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)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateLedger", reflect.TypeOf((*MockSystemStore)(nil).CreateLedger), ctx, l)
}
// DeleteLedgerMetadata mocks base method.
-func (m *SystemStore) DeleteLedgerMetadata(ctx context.Context, name, key string) error {
+func (m *MockSystemStore) 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)
@@ -65,13 +64,13 @@ func (m *SystemStore) DeleteLedgerMetadata(ctx context.Context, name, key string
}
// DeleteLedgerMetadata indicates an expected call of DeleteLedgerMetadata.
-func (mr *SystemStoreMockRecorder) DeleteLedgerMetadata(ctx, name, key any) *gomock.Call {
+func (mr *MockSystemStoreMockRecorder) 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)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLedgerMetadata", reflect.TypeOf((*MockSystemStore)(nil).DeleteLedgerMetadata), ctx, name, key)
}
// GetDistinctBuckets mocks base method.
-func (m *SystemStore) GetDistinctBuckets(ctx context.Context) ([]string, error) {
+func (m *MockSystemStore) GetDistinctBuckets(ctx context.Context) ([]string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetDistinctBuckets", ctx)
ret0, _ := ret[0].([]string)
@@ -80,13 +79,13 @@ func (m *SystemStore) GetDistinctBuckets(ctx context.Context) ([]string, error)
}
// GetDistinctBuckets indicates an expected call of GetDistinctBuckets.
-func (mr *SystemStoreMockRecorder) GetDistinctBuckets(ctx any) *gomock.Call {
+func (mr *MockSystemStoreMockRecorder) GetDistinctBuckets(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDistinctBuckets", reflect.TypeOf((*SystemStore)(nil).GetDistinctBuckets), ctx)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDistinctBuckets", reflect.TypeOf((*MockSystemStore)(nil).GetDistinctBuckets), ctx)
}
// GetLedger mocks base method.
-func (m *SystemStore) GetLedger(ctx context.Context, name string) (*ledger.Ledger, error) {
+func (m *MockSystemStore) 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)
@@ -95,13 +94,13 @@ func (m *SystemStore) GetLedger(ctx context.Context, name string) (*ledger.Ledge
}
// GetLedger indicates an expected call of GetLedger.
-func (mr *SystemStoreMockRecorder) GetLedger(ctx, name any) *gomock.Call {
+func (mr *MockSystemStoreMockRecorder) 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)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLedger", reflect.TypeOf((*MockSystemStore)(nil).GetLedger), ctx, name)
}
// GetMigrator mocks base method.
-func (m *SystemStore) GetMigrator(options ...migrations.Option) *migrations.Migrator {
+func (m *MockSystemStore) GetMigrator(options ...migrations.Option) *migrations.Migrator {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range options {
@@ -113,13 +112,13 @@ func (m *SystemStore) GetMigrator(options ...migrations.Option) *migrations.Migr
}
// GetMigrator indicates an expected call of GetMigrator.
-func (mr *SystemStoreMockRecorder) GetMigrator(options ...any) *gomock.Call {
+func (mr *MockSystemStoreMockRecorder) GetMigrator(options ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMigrator", reflect.TypeOf((*SystemStore)(nil).GetMigrator), options...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMigrator", reflect.TypeOf((*MockSystemStore)(nil).GetMigrator), options...)
}
// IsUpToDate mocks base method.
-func (m *SystemStore) IsUpToDate(ctx context.Context) (bool, error) {
+func (m *MockSystemStore) IsUpToDate(ctx context.Context) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsUpToDate", ctx)
ret0, _ := ret[0].(bool)
@@ -128,27 +127,13 @@ func (m *SystemStore) IsUpToDate(ctx context.Context) (bool, error) {
}
// IsUpToDate indicates an expected call of IsUpToDate.
-func (mr *SystemStoreMockRecorder) IsUpToDate(ctx any) *gomock.Call {
+func (mr *MockSystemStoreMockRecorder) IsUpToDate(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsUpToDate", reflect.TypeOf((*SystemStore)(nil).IsUpToDate), ctx)
-}
-
-// Ledgers mocks base method.
-func (m *SystemStore) Ledgers() common.PaginatedResource[ledger.Ledger, any, common.ColumnPaginatedQuery[any]] {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "Ledgers")
- ret0, _ := ret[0].(common.PaginatedResource[ledger.Ledger, any, common.ColumnPaginatedQuery[any]])
- return ret0
-}
-
-// Ledgers indicates an expected call of Ledgers.
-func (mr *SystemStoreMockRecorder) Ledgers() *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ledgers", reflect.TypeOf((*SystemStore)(nil).Ledgers))
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsUpToDate", reflect.TypeOf((*MockSystemStore)(nil).IsUpToDate), ctx)
}
// Migrate mocks base method.
-func (m *SystemStore) Migrate(ctx context.Context, options ...migrations.Option) error {
+func (m *MockSystemStore) Migrate(ctx context.Context, options ...migrations.Option) error {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range options {
@@ -160,14 +145,14 @@ func (m *SystemStore) Migrate(ctx context.Context, options ...migrations.Option)
}
// Migrate indicates an expected call of Migrate.
-func (mr *SystemStoreMockRecorder) Migrate(ctx any, options ...any) *gomock.Call {
+func (mr *MockSystemStoreMockRecorder) 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...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Migrate", reflect.TypeOf((*MockSystemStore)(nil).Migrate), varargs...)
}
// UpdateLedgerMetadata mocks base method.
-func (m_2 *SystemStore) UpdateLedgerMetadata(ctx context.Context, name string, m metadata.Metadata) error {
+func (m_2 *MockSystemStore) 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)
@@ -175,7 +160,7 @@ func (m_2 *SystemStore) UpdateLedgerMetadata(ctx context.Context, name string, m
}
// UpdateLedgerMetadata indicates an expected call of UpdateLedgerMetadata.
-func (mr *SystemStoreMockRecorder) UpdateLedgerMetadata(ctx, name, m any) *gomock.Call {
+func (mr *MockSystemStoreMockRecorder) 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)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateLedgerMetadata", reflect.TypeOf((*MockSystemStore)(nil).UpdateLedgerMetadata), ctx, name, m)
}
diff --git a/internal/storage/ledger/adapters.go b/internal/storage/ledger/adapters.go
deleted file mode 100644
index 9d469ab0f6..0000000000
--- a/internal/storage/ledger/adapters.go
+++ /dev/null
@@ -1,55 +0,0 @@
-package ledger
-
-import (
- "context"
- "database/sql"
- ledger "github.com/formancehq/ledger/internal"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
- "github.com/formancehq/ledger/internal/storage/common"
- "github.com/uptrace/bun"
-)
-
-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, *bun.Tx, error) {
- store, tx, err := d.Store.BeginTX(ctx, opts)
- if err != nil {
- return nil, nil, err
- }
-
- return &DefaultStoreAdapter{
- Store: store,
- }, tx, nil
-}
-
-func (d *DefaultStoreAdapter) AggregatedBalances() common.Resource[ledger.AggregatedVolumes, ledgercontroller.GetAggregatedVolumesOptions] {
- return d.AggregatedVolumes()
-}
-
-func (d *DefaultStoreAdapter) LockLedger(ctx context.Context) (ledgercontroller.Store, bun.IDB, func() error, error) {
- lockLedger, b, f, err := d.Store.LockLedger(ctx)
- if err != nil {
- return nil, nil, nil, err
- }
- return &DefaultStoreAdapter{
- Store: lockLedger,
- }, b, f, err
-}
-
-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 6c3f4fd2c2..23e6df5805 100644
--- a/internal/storage/ledger/balances.go
+++ b/internal/storage/ledger/balances.go
@@ -11,16 +11,15 @@ import (
"github.com/formancehq/ledger/internal/tracing"
ledger "github.com/formancehq/ledger/internal"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
)
-func (store *Store) GetBalances(ctx context.Context, query ledgercontroller.BalanceQuery) (ledgercontroller.Balances, error) {
+func (store *Store) GetBalances(ctx context.Context, query BalanceQuery) (ledger.Balances, error) {
return tracing.TraceWithMetric(
ctx,
"GetBalances",
store.tracer,
store.getBalancesHistogram,
- func(ctx context.Context) (ledgercontroller.Balances, error) {
+ func(ctx context.Context) (ledger.Balances, error) {
conditions := make([]string, 0)
args := make([]any, 0)
for account, assets := range query {
@@ -88,7 +87,7 @@ func (store *Store) GetBalances(ctx context.Context, query ledgercontroller.Bala
return nil, postgres.ResolveError(err)
}
- ret := ledgercontroller.Balances{}
+ ret := ledger.Balances{}
for _, volumes := range accountsVolumes {
if _, ok := ret[volumes.Account]; !ok {
ret[volumes.Account] = map[string]*big.Int{}
diff --git a/internal/storage/ledger/balances_test.go b/internal/storage/ledger/balances_test.go
index c56936b01f..0236983c84 100644
--- a/internal/storage/ledger/balances_test.go
+++ b/internal/storage/ledger/balances_test.go
@@ -5,15 +5,14 @@ package ledger_test
import (
"database/sql"
"github.com/formancehq/ledger/internal/storage/common"
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
"math/big"
"testing"
- "github.com/formancehq/go-libs/v3/metadata"
- "github.com/formancehq/go-libs/v3/time"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
-
"github.com/formancehq/go-libs/v3/logging"
+ "github.com/formancehq/go-libs/v3/metadata"
"github.com/formancehq/go-libs/v3/pointer"
+ "github.com/formancehq/go-libs/v3/time"
libtime "time"
@@ -47,7 +46,7 @@ func TestBalancesGet(t *testing.T) {
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{
+ balances, err := store.GetBalances(ctx, ledgerstore.BalanceQuery{
"orders:1234": []string{"USD"},
})
require.NoError(t, err)
@@ -87,7 +86,7 @@ func TestBalancesGet(t *testing.T) {
})
store2 := store.WithDB(tx2)
- bq := ledgercontroller.BalanceQuery{
+ bq := ledgerstore.BalanceQuery{
"world": []string{"USD"},
}
@@ -137,7 +136,7 @@ func TestBalancesGet(t *testing.T) {
require.NoError(t, err)
require.Equal(t, 1, count)
- balances, err := store.GetBalances(ctx, ledgercontroller.BalanceQuery{
+ balances, err := store.GetBalances(ctx, ledgerstore.BalanceQuery{
"world": {"USD"},
"not-existing": {"USD"},
})
@@ -215,7 +214,7 @@ func TestBalancesAggregates(t *testing.T) {
t.Run("aggregate on all", func(t *testing.T) {
t.Parallel()
- ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{})
+ ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{})
require.NoError(t, err)
RequireEqual(t, ledger.AggregatedVolumes{
Aggregated: ledger.VolumesByAssets{
@@ -239,7 +238,7 @@ func TestBalancesAggregates(t *testing.T) {
t.Run("filter on address", func(t *testing.T) {
t.Parallel()
- ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{
+ ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{
Builder: query.Match("address", "users:"),
})
require.NoError(t, err)
@@ -257,7 +256,7 @@ func TestBalancesAggregates(t *testing.T) {
})
t.Run("using pit on effective date", func(t *testing.T) {
t.Parallel()
- ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{
+ ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{
Builder: query.Match("address", "users:"),
PIT: pointer.For(now.Add(-time.Second)),
})
@@ -276,10 +275,10 @@ func TestBalancesAggregates(t *testing.T) {
})
t.Run("using pit on insertion date", func(t *testing.T) {
t.Parallel()
- ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{
+ ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{
Builder: query.Match("address", "users:"),
PIT: pointer.For(now),
- Opts: ledgercontroller.GetAggregatedVolumesOptions{
+ Opts: ledgerstore.GetAggregatedVolumesOptions{
UseInsertionDate: true,
},
})
@@ -298,7 +297,7 @@ func TestBalancesAggregates(t *testing.T) {
})
t.Run("using a metadata and pit", func(t *testing.T) {
t.Parallel()
- ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{
+ ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{
PIT: pointer.For(now.Add(time.Minute)),
Builder: query.Match("metadata[category]", "premium"),
})
@@ -317,7 +316,7 @@ func TestBalancesAggregates(t *testing.T) {
})
t.Run("using a metadata without pit", func(t *testing.T) {
t.Parallel()
- ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{
+ ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{
Builder: query.Match("metadata[category]", "premium"),
})
require.NoError(t, err)
@@ -333,7 +332,7 @@ func TestBalancesAggregates(t *testing.T) {
})
t.Run("when no matching", func(t *testing.T) {
t.Parallel()
- ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{
+ ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{
Builder: query.Match("metadata[category]", "guest"),
})
require.NoError(t, err)
@@ -344,7 +343,7 @@ func TestBalancesAggregates(t *testing.T) {
t.Run("using a filter exist on metadata", func(t *testing.T) {
t.Parallel()
- ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{
+ ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{
Builder: query.Exists("metadata", "category"),
})
require.NoError(t, err)
@@ -363,7 +362,7 @@ func TestBalancesAggregates(t *testing.T) {
t.Run("using a filter on metadata and on address", func(t *testing.T) {
t.Parallel()
- ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{
+ ret, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{
Builder: query.And(
query.Match("address", "users:"),
query.Match("metadata[category]", "premium"),
diff --git a/internal/storage/ledger/errors.go b/internal/storage/ledger/errors.go
new file mode 100644
index 0000000000..37bdf6f1e2
--- /dev/null
+++ b/internal/storage/ledger/errors.go
@@ -0,0 +1,99 @@
+package ledger
+
+import "fmt"
+
+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...),
+ }
+}
+
+type ErrMissingFeature struct {
+ feature string
+}
+
+func (e ErrMissingFeature) Error() string {
+ return fmt.Sprintf("missing feature %q", e.feature)
+}
+
+func (e ErrMissingFeature) Is(err error) bool {
+ _, ok := err.(ErrMissingFeature)
+ return ok
+}
+
+func NewErrMissingFeature(feature string) ErrMissingFeature {
+ return ErrMissingFeature{
+ feature: feature,
+ }
+}
+
+type ErrIdempotencyKeyConflict struct {
+ ik string
+}
+
+func (e ErrIdempotencyKeyConflict) Error() string {
+ return fmt.Sprintf("duplicate idempotency key %q", e.ik)
+}
+
+func (e ErrIdempotencyKeyConflict) Is(err error) bool {
+ _, ok := err.(ErrIdempotencyKeyConflict)
+ return ok
+}
+
+func NewErrIdempotencyKeyConflict(ik string) ErrIdempotencyKeyConflict {
+ return ErrIdempotencyKeyConflict{
+ ik: ik,
+ }
+}
+
+type ErrTransactionReferenceConflict struct {
+ reference string
+}
+
+func (e ErrTransactionReferenceConflict) Error() string {
+ return fmt.Sprintf("duplicate reference %q", e.reference)
+}
+
+func (e ErrTransactionReferenceConflict) Is(err error) bool {
+ _, ok := err.(ErrTransactionReferenceConflict)
+ return ok
+}
+
+func NewErrTransactionReferenceConflict(reference string) ErrTransactionReferenceConflict {
+ return ErrTransactionReferenceConflict{
+ reference: reference,
+ }
+}
+
+// ErrConcurrentTransaction can be raised in case of conflicting between an import and a single transaction
+type ErrConcurrentTransaction struct {
+ id uint64
+}
+
+func (e ErrConcurrentTransaction) Error() string {
+ return fmt.Sprintf("duplicate id insertion %d", e.id)
+}
+
+func (e ErrConcurrentTransaction) Is(err error) bool {
+ _, ok := err.(ErrConcurrentTransaction)
+ return ok
+}
+
+func NewErrConcurrentTransaction(id uint64) ErrConcurrentTransaction {
+ return ErrConcurrentTransaction{
+ id: id,
+ }
+}
diff --git a/internal/storage/ledger/logs.go b/internal/storage/ledger/logs.go
index 0a88c4cc24..471dfa02d2 100644
--- a/internal/storage/ledger/logs.go
+++ b/internal/storage/ledger/logs.go
@@ -12,7 +12,6 @@ import (
"github.com/formancehq/go-libs/v3/platform/postgres"
"github.com/formancehq/go-libs/v3/pointer"
ledger "github.com/formancehq/ledger/internal"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
)
// Log override ledger.Log to be able to properly read/write payload which is jsonb
@@ -97,7 +96,7 @@ func (store *Store) InsertLog(ctx context.Context, log *ledger.Log) error {
switch {
case errors.Is(err, postgres.ErrConstraintsFailed{}):
if err.(postgres.ErrConstraintsFailed).GetConstraint() == "logs_idempotency_key" {
- return ledgercontroller.NewErrIdempotencyKeyConflict(log.IdempotencyKey)
+ return NewErrIdempotencyKeyConflict(log.IdempotencyKey)
}
default:
return fmt.Errorf("inserting log: %w", err)
diff --git a/internal/storage/ledger/logs_test.go b/internal/storage/ledger/logs_test.go
index 9ae4871944..2bed440954 100644
--- a/internal/storage/ledger/logs_test.go
+++ b/internal/storage/ledger/logs_test.go
@@ -8,13 +8,12 @@ import (
"github.com/formancehq/go-libs/v3/metadata"
"github.com/formancehq/go-libs/v3/pointer"
"github.com/formancehq/ledger/internal/storage/common"
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
"golang.org/x/sync/errgroup"
"math/big"
"testing"
"errors"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
-
"github.com/formancehq/go-libs/v3/bun/bunpaginate"
"github.com/formancehq/go-libs/v3/time"
@@ -98,7 +97,7 @@ func TestLogsInsert(t *testing.T) {
WithIdempotencyKey("foo")
err = store.InsertLog(ctx, &logTx)
require.Error(t, err)
- require.True(t, errors.Is(err, ledgercontroller.ErrIdempotencyKeyConflict{}))
+ require.True(t, errors.Is(err, ledgerstore.ErrIdempotencyKeyConflict{}))
})
t.Run("hash consistency over high concurrency", func(t *testing.T) {
diff --git a/internal/storage/ledger/moves_test.go b/internal/storage/ledger/moves_test.go
index 63cfc647f8..c7671e3a98 100644
--- a/internal/storage/ledger/moves_test.go
+++ b/internal/storage/ledger/moves_test.go
@@ -6,6 +6,7 @@ import (
"database/sql"
"fmt"
"github.com/formancehq/ledger/internal/storage/common"
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
"math/big"
"math/rand"
"testing"
@@ -17,7 +18,6 @@ import (
"github.com/formancehq/go-libs/v3/platform/postgres"
"github.com/formancehq/go-libs/v3/time"
ledger "github.com/formancehq/ledger/internal"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
"github.com/stretchr/testify/require"
)
@@ -171,8 +171,8 @@ func TestMovesInsert(t *testing.T) {
}
wp.StopAndWait()
- aggregatedVolumes, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions]{
- Opts: ledgercontroller.GetAggregatedVolumesOptions{
+ aggregatedVolumes, err := store.AggregatedVolumes().GetOne(ctx, common.ResourceQuery[ledgerstore.GetAggregatedVolumesOptions]{
+ Opts: ledgerstore.GetAggregatedVolumesOptions{
UseInsertionDate: true,
},
})
diff --git a/internal/storage/ledger/queries.go b/internal/storage/ledger/queries.go
new file mode 100644
index 0000000000..e61af57459
--- /dev/null
+++ b/internal/storage/ledger/queries.go
@@ -0,0 +1,12 @@
+package ledger
+
+type GetAggregatedVolumesOptions struct {
+ UseInsertionDate bool `json:"useInsertionDate"`
+}
+
+type GetVolumesOptions struct {
+ UseInsertionDate bool `json:"useInsertionDate"`
+ GroupLvl int `json:"groupLvl"`
+}
+
+type BalanceQuery = map[string][]string
diff --git a/internal/storage/ledger/resource_accounts.go b/internal/storage/ledger/resource_accounts.go
index afe7842406..a120f5fb5b 100644
--- a/internal/storage/ledger/resource_accounts.go
+++ b/internal/storage/ledger/resource_accounts.go
@@ -2,7 +2,6 @@ package ledger
import (
"fmt"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
"github.com/formancehq/ledger/internal/storage/common"
"github.com/formancehq/ledger/pkg/features"
"github.com/stoewer/go-strcase"
@@ -99,7 +98,7 @@ func (h accountsResourceHandler) ResolveFilter(opts common.ResourceQuery[any], o
if opts.PIT != nil && !opts.PIT.IsZero() {
if !h.store.ledger.HasFeature(features.FeatureMovesHistory, "ON") {
- return "", nil, ledgercontroller.NewErrMissingFeature(features.FeatureMovesHistory)
+ return "", nil, NewErrMissingFeature(features.FeatureMovesHistory)
}
selectBalance = selectBalance.
ModelTableExpr(h.store.GetPrefixedRelationName("moves")).
diff --git a/internal/storage/ledger/resource_aggregated_balances.go b/internal/storage/ledger/resource_aggregated_balances.go
index d97ee27303..9bbf853af8 100644
--- a/internal/storage/ledger/resource_aggregated_balances.go
+++ b/internal/storage/ledger/resource_aggregated_balances.go
@@ -3,7 +3,6 @@ package ledger
import (
"errors"
"fmt"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
"github.com/formancehq/ledger/internal/storage/common"
"github.com/formancehq/ledger/pkg/features"
"github.com/uptrace/bun"
@@ -48,7 +47,7 @@ func (h aggregatedBalancesResourceRepositoryHandler) Filters() []common.Filter {
}
}
-func (h aggregatedBalancesResourceRepositoryHandler) BuildDataset(query common.RepositoryHandlerBuildContext[ledgercontroller.GetAggregatedVolumesOptions]) (*bun.SelectQuery, error) {
+func (h aggregatedBalancesResourceRepositoryHandler) BuildDataset(query common.RepositoryHandlerBuildContext[GetAggregatedVolumesOptions]) (*bun.SelectQuery, error) {
if query.UsePIT() {
ret := h.store.db.NewSelect().
@@ -58,7 +57,7 @@ func (h aggregatedBalancesResourceRepositoryHandler) BuildDataset(query common.R
Where("ledger = ?", h.store.ledger.Name)
if query.Opts.UseInsertionDate {
if !h.store.ledger.HasFeature(features.FeatureMovesHistory, "ON") {
- return nil, ledgercontroller.NewErrMissingFeature(features.FeatureMovesHistory)
+ return nil, NewErrMissingFeature(features.FeatureMovesHistory)
}
ret = ret.
@@ -66,7 +65,7 @@ func (h aggregatedBalancesResourceRepositoryHandler) BuildDataset(query common.R
Where("insertion_date <= ?", query.PIT)
} else {
if !h.store.ledger.HasFeature(features.FeatureMovesHistoryPostCommitEffectiveVolumes, "SYNC") {
- return nil, ledgercontroller.NewErrMissingFeature(features.FeatureMovesHistoryPostCommitEffectiveVolumes)
+ return nil, NewErrMissingFeature(features.FeatureMovesHistoryPostCommitEffectiveVolumes)
}
ret = ret.
@@ -136,7 +135,7 @@ func (h aggregatedBalancesResourceRepositoryHandler) BuildDataset(query common.R
}
}
-func (h aggregatedBalancesResourceRepositoryHandler) ResolveFilter(_ common.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions], operator, property string, value any) (string, []any, error) {
+func (h aggregatedBalancesResourceRepositoryHandler) ResolveFilter(_ common.ResourceQuery[GetAggregatedVolumesOptions], operator, property string, value any) (string, []any, error) {
switch {
case property == "address":
return filterAccountAddress(value.(string), "accounts_address"), nil, nil
@@ -155,12 +154,12 @@ func (h aggregatedBalancesResourceRepositoryHandler) ResolveFilter(_ common.Reso
}
}
-func (h aggregatedBalancesResourceRepositoryHandler) Expand(_ common.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions], property string) (*bun.SelectQuery, *common.JoinCondition, error) {
+func (h aggregatedBalancesResourceRepositoryHandler) Expand(_ common.ResourceQuery[GetAggregatedVolumesOptions], property string) (*bun.SelectQuery, *common.JoinCondition, error) {
return nil, nil, errors.New("no expand available for aggregated balances")
}
func (h aggregatedBalancesResourceRepositoryHandler) Project(
- _ common.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions],
+ _ common.ResourceQuery[GetAggregatedVolumesOptions],
selectQuery *bun.SelectQuery,
) (*bun.SelectQuery, error) {
sumVolumesForAsset := h.store.db.NewSelect().
@@ -174,4 +173,4 @@ func (h aggregatedBalancesResourceRepositoryHandler) Project(
ColumnExpr("public.aggregate_objects(json_build_object(asset, volumes)::jsonb) as aggregated"), nil
}
-var _ common.RepositoryHandler[ledgercontroller.GetAggregatedVolumesOptions] = aggregatedBalancesResourceRepositoryHandler{}
+var _ common.RepositoryHandler[GetAggregatedVolumesOptions] = aggregatedBalancesResourceRepositoryHandler{}
diff --git a/internal/storage/ledger/resource_volumes.go b/internal/storage/ledger/resource_volumes.go
index c019700472..d15488e9b4 100644
--- a/internal/storage/ledger/resource_volumes.go
+++ b/internal/storage/ledger/resource_volumes.go
@@ -3,7 +3,6 @@ package ledger
import (
"errors"
"fmt"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
"github.com/formancehq/ledger/internal/storage/common"
"github.com/formancehq/ledger/pkg/features"
"github.com/uptrace/bun"
@@ -62,7 +61,7 @@ func (h volumesResourceHandler) Filters() []common.Filter {
}
}
-func (h volumesResourceHandler) BuildDataset(query common.RepositoryHandlerBuildContext[ledgercontroller.GetVolumesOptions]) (*bun.SelectQuery, error) {
+func (h volumesResourceHandler) BuildDataset(query common.RepositoryHandlerBuildContext[GetVolumesOptions]) (*bun.SelectQuery, error) {
var selectVolumes *bun.SelectQuery
@@ -103,7 +102,7 @@ func (h volumesResourceHandler) BuildDataset(query common.RepositoryHandlerBuild
}
} else {
if !h.store.ledger.HasFeature(features.FeatureMovesHistory, "ON") {
- return nil, ledgercontroller.NewErrMissingFeature(features.FeatureMovesHistory)
+ return nil, NewErrMissingFeature(features.FeatureMovesHistory)
}
selectVolumes = h.store.db.NewSelect().
@@ -165,7 +164,7 @@ func (h volumesResourceHandler) BuildDataset(query common.RepositoryHandlerBuild
}
func (h volumesResourceHandler) ResolveFilter(
- _ common.ResourceQuery[ledgercontroller.GetVolumesOptions],
+ _ common.ResourceQuery[GetVolumesOptions],
operator, property string,
value any,
) (string, []any, error) {
@@ -204,7 +203,7 @@ func (h volumesResourceHandler) ResolveFilter(
}
func (h volumesResourceHandler) Project(
- query common.ResourceQuery[ledgercontroller.GetVolumesOptions],
+ query common.ResourceQuery[GetVolumesOptions],
selectQuery *bun.SelectQuery,
) (*bun.SelectQuery, error) {
selectQuery = selectQuery.DistinctOn("account, asset")
@@ -227,8 +226,8 @@ func (h volumesResourceHandler) Project(
GroupExpr("account, asset"), nil
}
-func (h volumesResourceHandler) Expand(_ common.ResourceQuery[ledgercontroller.GetVolumesOptions], property string) (*bun.SelectQuery, *common.JoinCondition, error) {
+func (h volumesResourceHandler) Expand(_ common.ResourceQuery[GetVolumesOptions], property string) (*bun.SelectQuery, *common.JoinCondition, error) {
return nil, nil, errors.New("no expansion available")
}
-var _ common.RepositoryHandler[ledgercontroller.GetVolumesOptions] = volumesResourceHandler{}
+var _ common.RepositoryHandler[GetVolumesOptions] = volumesResourceHandler{}
diff --git a/internal/storage/ledger/store.go b/internal/storage/ledger/store.go
index b5be23bcb3..f0c314ad67 100644
--- a/internal/storage/ledger/store.go
+++ b/internal/storage/ledger/store.go
@@ -7,7 +7,7 @@ import (
"github.com/formancehq/go-libs/v3/bun/bunpaginate"
"github.com/formancehq/go-libs/v3/migrations"
"github.com/formancehq/go-libs/v3/platform/postgres"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
+ ledger "github.com/formancehq/ledger/internal"
"github.com/formancehq/ledger/internal/storage/bucket"
"github.com/formancehq/ledger/internal/storage/common"
"go.opentelemetry.io/otel/metric"
@@ -16,7 +16,6 @@ import (
nooptracer "go.opentelemetry.io/otel/trace/noop"
"errors"
- ledger "github.com/formancehq/ledger/internal"
"github.com/uptrace/bun"
)
@@ -46,16 +45,16 @@ type Store struct {
func (store *Store) Volumes() common.PaginatedResource[
ledger.VolumesWithBalanceByAssetByAccount,
- ledgercontroller.GetVolumesOptions,
- common.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]] {
- return common.NewPaginatedResourceRepository(&volumesResourceHandler{store: store}, common.OffsetPaginator[ledger.VolumesWithBalanceByAssetByAccount, ledgercontroller.GetVolumesOptions]{
+ GetVolumesOptions,
+ common.OffsetPaginatedQuery[GetVolumesOptions]] {
+ return common.NewPaginatedResourceRepository(&volumesResourceHandler{store: store}, common.OffsetPaginator[ledger.VolumesWithBalanceByAssetByAccount, GetVolumesOptions]{
DefaultPaginationColumn: "account",
DefaultOrder: bunpaginate.OrderAsc,
})
}
-func (store *Store) AggregatedVolumes() common.Resource[ledger.AggregatedVolumes, ledgercontroller.GetAggregatedVolumesOptions] {
- return common.NewResourceRepository[ledger.AggregatedVolumes, ledgercontroller.GetAggregatedVolumesOptions](&aggregatedBalancesResourceRepositoryHandler{
+func (store *Store) AggregatedVolumes() common.Resource[ledger.AggregatedVolumes, GetAggregatedVolumesOptions] {
+ return common.NewResourceRepository[ledger.AggregatedVolumes, GetAggregatedVolumesOptions](&aggregatedBalancesResourceRepositoryHandler{
store: store,
})
}
diff --git a/internal/storage/ledger/transactions.go b/internal/storage/ledger/transactions.go
index d5c2ca26eb..9a75b4557c 100644
--- a/internal/storage/ledger/transactions.go
+++ b/internal/storage/ledger/transactions.go
@@ -14,7 +14,6 @@ import (
"errors"
"github.com/formancehq/go-libs/v3/platform/postgres"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
@@ -151,10 +150,10 @@ func (store *Store) InsertTransaction(ctx context.Context, tx *ledger.Transactio
switch {
case errors.Is(err, postgres.ErrConstraintsFailed{}):
if err.(postgres.ErrConstraintsFailed).GetConstraint() == "transactions_reference" {
- return nil, ledgercontroller.NewErrTransactionReferenceConflict(tx.Reference)
+ return nil, NewErrTransactionReferenceConflict(tx.Reference)
}
if err.(postgres.ErrConstraintsFailed).GetConstraint() == "transactions_ledger" {
- return nil, ledgercontroller.NewErrConcurrentTransaction(*tx.ID)
+ return nil, NewErrConcurrentTransaction(*tx.ID)
}
return nil, err
diff --git a/internal/storage/ledger/transactions_test.go b/internal/storage/ledger/transactions_test.go
index 27b1ceda55..a0b9f955ad 100644
--- a/internal/storage/ledger/transactions_test.go
+++ b/internal/storage/ledger/transactions_test.go
@@ -8,15 +8,14 @@ import (
"fmt"
"github.com/alitto/pond"
"github.com/formancehq/ledger/internal/storage/common"
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
"math/big"
"slices"
"testing"
+ "errors"
"github.com/formancehq/go-libs/v3/platform/postgres"
"github.com/formancehq/go-libs/v3/time"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
-
- "errors"
"github.com/formancehq/go-libs/v3/logging"
"github.com/formancehq/go-libs/v3/pointer"
@@ -319,7 +318,7 @@ func TestTransactionsCommit(t *testing.T) {
t.Cleanup(cancel)
go func() {
// Simulate a transaction with bounded sources by asking for balances before calling CommitTransaction
- _, err := storeWithTxWithAccount1AsSource.GetBalances(tx1Context, ledgercontroller.BalanceQuery{
+ _, err := storeWithTxWithAccount1AsSource.GetBalances(tx1Context, ledgerstore.BalanceQuery{
"account:1": {"USD"},
})
require.NoError(t, err)
@@ -358,7 +357,7 @@ func TestTransactionsCommit(t *testing.T) {
t.Cleanup(cancel)
go func() {
// Simulate a transaction with bounded sources by asking for balances before calling CommitTransaction
- _, err := storeWithTxWithAccount2AsSource.GetBalances(tx2Context, ledgercontroller.BalanceQuery{
+ _, err := storeWithTxWithAccount2AsSource.GetBalances(tx2Context, ledgerstore.BalanceQuery{
"account:2": {"USD"},
})
require.NoError(t, err)
@@ -620,7 +619,7 @@ func TestTransactionsInsert(t *testing.T) {
}
err = store.InsertTransaction(ctx, &tx2)
require.Error(t, err)
- require.True(t, errors.Is(err, ledgercontroller.ErrTransactionReferenceConflict{}))
+ require.True(t, errors.Is(err, ledgerstore.ErrTransactionReferenceConflict{}))
})
t.Run("check denormalization", func(t *testing.T) {
t.Parallel()
diff --git a/internal/storage/ledger/volumes_test.go b/internal/storage/ledger/volumes_test.go
index 4296282665..8f794ffc7b 100644
--- a/internal/storage/ledger/volumes_test.go
+++ b/internal/storage/ledger/volumes_test.go
@@ -6,14 +6,13 @@ import (
"database/sql"
"github.com/formancehq/go-libs/v3/pointer"
"github.com/formancehq/ledger/internal/storage/common"
+ ledgerstore "github.com/formancehq/ledger/internal/storage/ledger"
"math/big"
"testing"
libtime "time"
"errors"
"github.com/formancehq/go-libs/v3/platform/postgres"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
-
"github.com/formancehq/go-libs/v3/time"
"github.com/formancehq/go-libs/v3/logging"
@@ -106,8 +105,8 @@ func TestVolumesList(t *testing.T) {
t.Run("Get all volumes with first account usage filter", func(t *testing.T) {
t.Parallel()
- volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
- Options: common.ResourceQuery[ledgercontroller.GetVolumesOptions]{
+ volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
+ Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{
Builder: query.Lt("first_usage", now.Add(-3*time.Minute)),
},
})
@@ -135,8 +134,8 @@ func TestVolumesList(t *testing.T) {
t.Run("Get all volumes with first account usage filter and PIT", func(t *testing.T) {
t.Parallel()
- volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
- Options: common.ResourceQuery[ledgercontroller.GetVolumesOptions]{
+ volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
+ Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{
Builder: query.Lt("first_usage", now.Add(-3*time.Minute)),
PIT: pointer.For(now.Add(-3 * time.Minute)),
},
@@ -165,9 +164,9 @@ func TestVolumesList(t *testing.T) {
t.Run("Get all volumes with balance for insertion date", func(t *testing.T) {
t.Parallel()
- volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
- Options: common.ResourceQuery[ledgercontroller.GetVolumesOptions]{
- Opts: ledgercontroller.GetVolumesOptions{
+ volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
+ Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{
+ Opts: ledgerstore.GetVolumesOptions{
UseInsertionDate: true,
},
},
@@ -178,16 +177,16 @@ func TestVolumesList(t *testing.T) {
t.Run("Get all volumes with balance for effective date", func(t *testing.T) {
t.Parallel()
- volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{})
+ volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{})
require.NoError(t, err)
require.Len(t, volumes.Data, 4)
})
t.Run("Get all volumes with balance for insertion date with previous pit", func(t *testing.T) {
t.Parallel()
- volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
- Options: common.ResourceQuery[ledgercontroller.GetVolumesOptions]{
- Opts: ledgercontroller.GetVolumesOptions{
+ volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
+ Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{
+ Opts: ledgerstore.GetVolumesOptions{
UseInsertionDate: true,
},
PIT: &previousPIT,
@@ -209,9 +208,9 @@ func TestVolumesList(t *testing.T) {
t.Run("Get all volumes with balance for insertion date with futur pit", func(t *testing.T) {
t.Parallel()
- volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
- Options: common.ResourceQuery[ledgercontroller.GetVolumesOptions]{
- Opts: ledgercontroller.GetVolumesOptions{
+ volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
+ Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{
+ Opts: ledgerstore.GetVolumesOptions{
UseInsertionDate: true,
},
PIT: &futurPIT,
@@ -223,9 +222,9 @@ func TestVolumesList(t *testing.T) {
t.Run("Get all volumes with balance for insertion date with previous oot", func(t *testing.T) {
t.Parallel()
- volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
- Options: common.ResourceQuery[ledgercontroller.GetVolumesOptions]{
- Opts: ledgercontroller.GetVolumesOptions{
+ volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
+ Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{
+ Opts: ledgerstore.GetVolumesOptions{
UseInsertionDate: true,
},
OOT: &previousOOT,
@@ -237,9 +236,9 @@ func TestVolumesList(t *testing.T) {
t.Run("Get all volumes with balance for insertion date with future oot", func(t *testing.T) {
t.Parallel()
- volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
- Options: common.ResourceQuery[ledgercontroller.GetVolumesOptions]{
- Opts: ledgercontroller.GetVolumesOptions{
+ volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
+ Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{
+ Opts: ledgerstore.GetVolumesOptions{
UseInsertionDate: true,
},
OOT: &futurOOT,
@@ -261,8 +260,8 @@ func TestVolumesList(t *testing.T) {
t.Run("Get all volumes with balance for effective date with previous pit", func(t *testing.T) {
t.Parallel()
- volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
- Options: common.ResourceQuery[ledgercontroller.GetVolumesOptions]{
+ volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
+ Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{
PIT: &previousPIT,
},
})
@@ -282,8 +281,8 @@ func TestVolumesList(t *testing.T) {
t.Run("Get all volumes with balance for effective date with futur pit", func(t *testing.T) {
t.Parallel()
- volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
- Options: common.ResourceQuery[ledgercontroller.GetVolumesOptions]{
+ volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
+ Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{
PIT: &futurPIT,
},
})
@@ -293,8 +292,8 @@ func TestVolumesList(t *testing.T) {
t.Run("Get all volumes with balance for effective date with previous oot", func(t *testing.T) {
t.Parallel()
- volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
- Options: common.ResourceQuery[ledgercontroller.GetVolumesOptions]{
+ volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
+ Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{
OOT: &previousOOT,
},
})
@@ -304,8 +303,8 @@ func TestVolumesList(t *testing.T) {
t.Run("Get all volumes with balance for effective date with futur oot", func(t *testing.T) {
t.Parallel()
- volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
- Options: common.ResourceQuery[ledgercontroller.GetVolumesOptions]{
+ volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
+ Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{
OOT: &futurOOT,
},
})
@@ -325,9 +324,9 @@ func TestVolumesList(t *testing.T) {
t.Run("Get all volumes with balance for insertion date with future PIT and now OOT", func(t *testing.T) {
t.Parallel()
- volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
- Options: common.ResourceQuery[ledgercontroller.GetVolumesOptions]{
- Opts: ledgercontroller.GetVolumesOptions{
+ volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
+ Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{
+ Opts: ledgerstore.GetVolumesOptions{
UseInsertionDate: true,
},
PIT: &futurPIT,
@@ -350,9 +349,9 @@ func TestVolumesList(t *testing.T) {
t.Run("Get all volumes with balance for insertion date with previous OOT and now PIT", func(t *testing.T) {
t.Parallel()
- volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
- Options: common.ResourceQuery[ledgercontroller.GetVolumesOptions]{
- Opts: ledgercontroller.GetVolumesOptions{
+ volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
+ Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{
+ Opts: ledgerstore.GetVolumesOptions{
UseInsertionDate: true,
},
PIT: &now,
@@ -375,8 +374,8 @@ func TestVolumesList(t *testing.T) {
t.Run("Get all volumes with balance for effective date with future PIT and now OOT", func(t *testing.T) {
t.Parallel()
- volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
- Options: common.ResourceQuery[ledgercontroller.GetVolumesOptions]{
+ volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
+ Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{
PIT: &futurPIT,
OOT: &now,
},
@@ -397,8 +396,8 @@ func TestVolumesList(t *testing.T) {
t.Run("Get all volumes with balance for insertion date with previous OOT and now PIT", func(t *testing.T) {
t.Parallel()
- volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
- Options: common.ResourceQuery[ledgercontroller.GetVolumesOptions]{
+ volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
+ Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{
PIT: &now,
OOT: &previousOOT,
},
@@ -421,8 +420,8 @@ func TestVolumesList(t *testing.T) {
t.Parallel()
volumes, err := store.Volumes().Paginate(ctx,
- common.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
- Options: common.ResourceQuery[ledgercontroller.GetVolumesOptions]{
+ common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
+ Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{
PIT: &now,
OOT: &previousOOT,
Builder: query.Match("account", "account:1"),
@@ -447,8 +446,8 @@ func TestVolumesList(t *testing.T) {
t.Parallel()
volumes, err := store.Volumes().Paginate(ctx,
- common.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
- Options: common.ResourceQuery[ledgercontroller.GetVolumesOptions]{
+ common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
+ Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{
Builder: query.Match("metadata[foo]", "bar"),
},
},
@@ -461,8 +460,8 @@ func TestVolumesList(t *testing.T) {
t.Parallel()
volumes, err := store.Volumes().Paginate(ctx,
- common.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
- Options: common.ResourceQuery[ledgercontroller.GetVolumesOptions]{
+ common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
+ Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{
Builder: query.Exists("metadata", "category"),
},
},
@@ -475,8 +474,8 @@ func TestVolumesList(t *testing.T) {
t.Parallel()
volumes, err := store.Volumes().Paginate(ctx,
- common.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
- Options: common.ResourceQuery[ledgercontroller.GetVolumesOptions]{
+ common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
+ Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{
Builder: query.Exists("metadata", "foo"),
},
},
@@ -546,9 +545,9 @@ func TestVolumesAggregate(t *testing.T) {
t.Run("Aggregation Volumes with balance for GroupLvl 0", func(t *testing.T) {
t.Parallel()
- volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
- Options: common.ResourceQuery[ledgercontroller.GetVolumesOptions]{
- Opts: ledgercontroller.GetVolumesOptions{
+ volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
+ Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{
+ Opts: ledgerstore.GetVolumesOptions{
UseInsertionDate: true,
},
Builder: query.Match("account", "account::"),
@@ -560,9 +559,9 @@ func TestVolumesAggregate(t *testing.T) {
t.Run("Aggregation Volumes with balance for GroupLvl 1", func(t *testing.T) {
t.Parallel()
- volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
- Options: common.ResourceQuery[ledgercontroller.GetVolumesOptions]{
- Opts: ledgercontroller.GetVolumesOptions{
+ volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
+ Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{
+ Opts: ledgerstore.GetVolumesOptions{
UseInsertionDate: true,
GroupLvl: 1,
},
@@ -575,9 +574,9 @@ func TestVolumesAggregate(t *testing.T) {
t.Run("Aggregation Volumes with balance for GroupLvl 2", func(t *testing.T) {
t.Parallel()
- volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
- Options: common.ResourceQuery[ledgercontroller.GetVolumesOptions]{
- Opts: ledgercontroller.GetVolumesOptions{
+ volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
+ Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{
+ Opts: ledgerstore.GetVolumesOptions{
UseInsertionDate: true,
GroupLvl: 2,
},
@@ -590,9 +589,9 @@ func TestVolumesAggregate(t *testing.T) {
t.Run("Aggregation Volumes with balance for GroupLvl 3", func(t *testing.T) {
t.Parallel()
- volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
- Options: common.ResourceQuery[ledgercontroller.GetVolumesOptions]{
- Opts: ledgercontroller.GetVolumesOptions{
+ volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
+ Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{
+ Opts: ledgerstore.GetVolumesOptions{
UseInsertionDate: true,
GroupLvl: 3,
},
@@ -606,9 +605,9 @@ func TestVolumesAggregate(t *testing.T) {
t.Run("Aggregation Volumes with balance for GroupLvl 1 && PIT && OOT && effectiveDate", func(t *testing.T) {
t.Parallel()
- volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
- Options: common.ResourceQuery[ledgercontroller.GetVolumesOptions]{
- Opts: ledgercontroller.GetVolumesOptions{
+ volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
+ Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{
+ Opts: ledgerstore.GetVolumesOptions{
GroupLvl: 1,
},
PIT: &pit,
@@ -641,9 +640,9 @@ func TestVolumesAggregate(t *testing.T) {
t.Run("Aggregation Volumes with balance for GroupLvl 1 && PIT && OOT && effectiveDate && Balance Filter 1", func(t *testing.T) {
t.Parallel()
- volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
- Options: common.ResourceQuery[ledgercontroller.GetVolumesOptions]{
- Opts: ledgercontroller.GetVolumesOptions{
+ volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
+ Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{
+ Opts: ledgerstore.GetVolumesOptions{
GroupLvl: 1,
},
PIT: &pit,
@@ -666,9 +665,9 @@ func TestVolumesAggregate(t *testing.T) {
t.Run("Aggregation Volumes with balance for GroupLvl 1 && Balance Filter 2", func(t *testing.T) {
t.Parallel()
- volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
- Options: common.ResourceQuery[ledgercontroller.GetVolumesOptions]{
- Opts: ledgercontroller.GetVolumesOptions{
+ volumes, err := store.Volumes().Paginate(ctx, common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
+ Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{
+ Opts: ledgerstore.GetVolumesOptions{
GroupLvl: 2,
UseInsertionDate: true,
},
@@ -711,9 +710,9 @@ func TestVolumesAggregate(t *testing.T) {
t.Parallel()
volumes, err := store.Volumes().Paginate(ctx,
- common.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
- Options: common.ResourceQuery[ledgercontroller.GetVolumesOptions]{
- Opts: ledgercontroller.GetVolumesOptions{
+ common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
+ Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{
+ Opts: ledgerstore.GetVolumesOptions{
GroupLvl: 1,
},
Builder: query.And(
@@ -731,9 +730,9 @@ func TestVolumesAggregate(t *testing.T) {
t.Parallel()
volumes, err := store.Volumes().Paginate(ctx,
- common.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
- Options: common.ResourceQuery[ledgercontroller.GetVolumesOptions]{
- Opts: ledgercontroller.GetVolumesOptions{
+ common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
+ Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{
+ Opts: ledgerstore.GetVolumesOptions{
GroupLvl: 1,
},
PIT: pointer.For(now.Add(time.Minute)),
@@ -752,9 +751,9 @@ func TestVolumesAggregate(t *testing.T) {
t.Parallel()
volumes, err := store.Volumes().Paginate(ctx,
- common.OffsetPaginatedQuery[ledgercontroller.GetVolumesOptions]{
- Options: common.ResourceQuery[ledgercontroller.GetVolumesOptions]{
- Opts: ledgercontroller.GetVolumesOptions{
+ common.OffsetPaginatedQuery[ledgerstore.GetVolumesOptions]{
+ Options: common.ResourceQuery[ledgerstore.GetVolumesOptions]{
+ Opts: ledgerstore.GetVolumesOptions{
GroupLvl: 1,
},
Builder: query.Match("metadata[foo]", "bar"),
@@ -837,11 +836,11 @@ func TestUpdateVolumes(t *testing.T) {
// At this stage, the accounts_volumes table is empty.
// Take balance of the 'world' account should force a lock.
- volumes, err := storeTx1.GetBalances(ctx, ledgercontroller.BalanceQuery{
+ volumes, err := storeTx1.GetBalances(ctx, ledgerstore.BalanceQuery{
"world": {"USD"},
})
require.NoError(t, err)
- require.Equal(t, ledgercontroller.Balances{
+ require.Equal(t, ledger.Balances{
"world": {
"USD": big.NewInt(0),
},
@@ -854,7 +853,7 @@ func TestUpdateVolumes(t *testing.T) {
errChan := make(chan error, 2)
go func() {
// This call should block as the lock for the row holding 'world' balance is owned by tx1
- _, err := storeTx2.GetBalances(ctx, ledgercontroller.BalanceQuery{
+ _, err := storeTx2.GetBalances(ctx, ledgerstore.BalanceQuery{
"world": {"USD"},
})
errChan <- err
diff --git a/internal/storage/module.go b/internal/storage/module.go
index 9750e8a968..3798847739 100644
--- a/internal/storage/module.go
+++ b/internal/storage/module.go
@@ -6,6 +6,7 @@ import (
"github.com/formancehq/go-libs/v3/health"
"github.com/formancehq/go-libs/v3/logging"
"github.com/formancehq/ledger/internal/storage/driver"
+ systemstore "github.com/formancehq/ledger/internal/storage/system"
"github.com/formancehq/ledger/internal/tracing"
"go.opentelemetry.io/otel/trace"
"go.uber.org/fx"
@@ -19,6 +20,10 @@ type ModuleConfig struct {
func NewFXModule(config ModuleConfig) fx.Option {
ret := []fx.Option{
+ systemstore.NewFXModule(),
+ fx.Provide(func(store *systemstore.DefaultStore) driver.SystemStore {
+ return store
+ }),
driver.NewFXModule(),
health.ProvideHealthCheck(func(driver *driver.Driver, tracer trace.TracerProvider) health.NamedCheck {
hasReachedMinimalVersion := false
diff --git a/internal/storage/system/migrations.go b/internal/storage/system/migrations.go
index 5a2b50cc42..f5d88da446 100644
--- a/internal/storage/system/migrations.go
+++ b/internal/storage/system/migrations.go
@@ -248,6 +248,36 @@ func GetMigrator(db bun.IDB, options ...migrations.Option) *migrations.Migrator
})
},
},
+ migrations.Migration{
+ Name: "add pipelines",
+ Up: func(ctx context.Context, db bun.IDB) error {
+ _, err := db.ExecContext(ctx, `
+ create table _system.exporters (
+ id varchar,
+ driver varchar,
+ config varchar,
+ created_at timestamp,
+
+ primary key(id)
+ );
+
+ create table _system.pipelines (
+ id varchar,
+ ledger varchar,
+ exporter_id varchar references _system.exporters (id) on delete cascade,
+ created_at timestamp,
+ enabled bool,
+ last_log_id bigint,
+ error varchar,
+ version int,
+
+ primary key(id)
+ );
+ create unique index on _system.pipelines (ledger, exporter_id);
+ `)
+ return err
+ },
+ },
)
return migrator
diff --git a/internal/storage/system/module.go b/internal/storage/system/module.go
new file mode 100644
index 0000000000..392c810f0d
--- /dev/null
+++ b/internal/storage/system/module.go
@@ -0,0 +1,15 @@
+package system
+
+import (
+ "github.com/uptrace/bun"
+
+ "go.uber.org/fx"
+)
+
+func NewFXModule() fx.Option {
+ return fx.Options(
+ fx.Provide(func(db *bun.DB) *DefaultStore {
+ return New(db)
+ }),
+ )
+}
diff --git a/internal/storage/system/queries.go b/internal/storage/system/queries.go
new file mode 100644
index 0000000000..7264dfbdf4
--- /dev/null
+++ b/internal/storage/system/queries.go
@@ -0,0 +1,23 @@
+package system
+
+import (
+ "github.com/formancehq/go-libs/v3/bun/bunpaginate"
+ "github.com/formancehq/go-libs/v3/pointer"
+ "github.com/formancehq/ledger/internal/storage/common"
+)
+
+type ListLedgersQueryPayload struct {
+ Bucket string
+ Features map[string]string
+}
+
+func NewListLedgersQuery(pageSize uint64) common.ColumnPaginatedQuery[ListLedgersQueryPayload] {
+ return common.ColumnPaginatedQuery[ListLedgersQueryPayload]{
+ PageSize: pageSize,
+ Column: "id",
+ Order: (*bunpaginate.Order)(pointer.For(bunpaginate.OrderAsc)),
+ Options: common.ResourceQuery[ListLedgersQueryPayload]{
+ Expand: make([]string, 0),
+ },
+ }
+}
diff --git a/internal/storage/system/resource_ledgers.go b/internal/storage/system/resource_ledgers.go
index cecd700b8c..11b88b5368 100644
--- a/internal/storage/system/resource_ledgers.go
+++ b/internal/storage/system/resource_ledgers.go
@@ -45,13 +45,13 @@ func (h ledgersResourceHandler) Filters() []common.Filter {
}
}
-func (h ledgersResourceHandler) BuildDataset(opts common.RepositoryHandlerBuildContext[any]) (*bun.SelectQuery, error) {
+func (h ledgersResourceHandler) BuildDataset(opts common.RepositoryHandlerBuildContext[ListLedgersQueryPayload]) (*bun.SelectQuery, error) {
return h.store.db.NewSelect().
Model(&ledger.Ledger{}).
Column("*"), nil
}
-func (h ledgersResourceHandler) ResolveFilter(opts common.ResourceQuery[any], operator, property string, value any) (string, []any, error) {
+func (h ledgersResourceHandler) ResolveFilter(opts common.ResourceQuery[ListLedgersQueryPayload], operator, property string, value any) (string, []any, error) {
switch {
case property == "bucket":
return "bucket = ?", []any{value}, nil
@@ -77,12 +77,12 @@ func (h ledgersResourceHandler) ResolveFilter(opts common.ResourceQuery[any], op
}
}
-func (h ledgersResourceHandler) Project(query common.ResourceQuery[any], selectQuery *bun.SelectQuery) (*bun.SelectQuery, error) {
+func (h ledgersResourceHandler) Project(query common.ResourceQuery[ListLedgersQueryPayload], selectQuery *bun.SelectQuery) (*bun.SelectQuery, error) {
return selectQuery.ColumnExpr("*"), nil
}
-func (h ledgersResourceHandler) Expand(opts common.ResourceQuery[any], property string) (*bun.SelectQuery, *common.JoinCondition, error) {
+func (h ledgersResourceHandler) Expand(opts common.ResourceQuery[ListLedgersQueryPayload], property string) (*bun.SelectQuery, *common.JoinCondition, error) {
return nil, nil, errors.New("no expansion available")
}
-var _ common.RepositoryHandler[any] = ledgersResourceHandler{}
+var _ common.RepositoryHandler[ListLedgersQueryPayload] = ledgersResourceHandler{}
diff --git a/internal/storage/system/store.go b/internal/storage/system/store.go
index b293fb7f08..5cd15e3936 100644
--- a/internal/storage/system/store.go
+++ b/internal/storage/system/store.go
@@ -2,6 +2,7 @@ package system
import (
"context"
+ "database/sql"
"errors"
"fmt"
"github.com/formancehq/go-libs/v3/bun/bunpaginate"
@@ -9,9 +10,9 @@ import (
"github.com/formancehq/go-libs/v3/migrations"
"github.com/formancehq/go-libs/v3/platform/postgres"
ledger "github.com/formancehq/ledger/internal"
- systemcontroller "github.com/formancehq/ledger/internal/controller/system"
"github.com/formancehq/ledger/internal/storage/common"
"github.com/formancehq/ledger/internal/tracing"
+ "github.com/lib/pq"
"github.com/uptrace/bun"
"go.opentelemetry.io/otel/trace"
"go.opentelemetry.io/otel/trace/noop"
@@ -21,7 +22,7 @@ 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
- Ledgers() common.PaginatedResource[ledger.Ledger, any, common.ColumnPaginatedQuery[any]]
+ Ledgers() common.PaginatedResource[ledger.Ledger, ListLedgersQueryPayload, common.ColumnPaginatedQuery[ListLedgersQueryPayload]]
GetLedger(ctx context.Context, name string) (*ledger.Ledger, error)
GetDistinctBuckets(ctx context.Context) ([]string, error)
@@ -34,6 +35,10 @@ const (
SchemaSystem = "_system"
)
+var (
+ ErrLedgerAlreadyExists = errors.New("ledger already exists")
+)
+
type DefaultStore struct {
db bun.IDB
tracer trace.Tracer
@@ -69,7 +74,7 @@ func (d *DefaultStore) CreateLedger(ctx context.Context, l *ledger.Ledger) error
Exec(ctx)
if err != nil {
if errors.Is(postgres.ResolveError(err), postgres.ErrConstraintsFailed{}) {
- return systemcontroller.ErrLedgerAlreadyExists
+ return ErrLedgerAlreadyExists
}
return postgres.ResolveError(err)
}
@@ -97,9 +102,9 @@ func (d *DefaultStore) DeleteLedgerMetadata(ctx context.Context, name string, ke
func (d *DefaultStore) Ledgers() common.PaginatedResource[
ledger.Ledger,
- any,
- common.ColumnPaginatedQuery[any]] {
- return common.NewPaginatedResourceRepository(&ledgersResourceHandler{store: d}, common.ColumnPaginator[ledger.Ledger, any]{
+ ListLedgersQueryPayload,
+ common.ColumnPaginatedQuery[ListLedgersQueryPayload]] {
+ return common.NewPaginatedResourceRepository(&ledgersResourceHandler{store: d}, common.ColumnPaginator[ledger.Ledger, ListLedgersQueryPayload]{
DefaultPaginationColumn: "id",
DefaultOrder: bunpaginate.OrderAsc,
})
@@ -153,3 +158,168 @@ func WithTracer(tracer trace.Tracer) Option {
var defaultOptions = []Option{
WithTracer(noop.Tracer{}),
}
+
+func (d *DefaultStore) ListExporters(ctx context.Context) (*bunpaginate.Cursor[ledger.Exporter], error) {
+ return bunpaginate.UsingOffset[struct{}, ledger.Exporter](
+ ctx,
+ d.db.NewSelect(),
+ bunpaginate.OffsetPaginatedQuery[struct{}]{},
+ )
+}
+
+func (d *DefaultStore) CreateExporter(ctx context.Context, exporter ledger.Exporter) error {
+ _, err := d.db.NewInsert().
+ Model(&exporter).
+ Exec(ctx)
+ return err
+}
+
+func (d *DefaultStore) DeleteExporter(ctx context.Context, id string) error {
+ ret, err := d.db.NewDelete().
+ Model(&ledger.Exporter{}).
+ Where("id = ?", id).
+ Exec(ctx)
+ if err != nil {
+ switch err := err.(type) {
+ case *pq.Error:
+ if err.Constraint == "pipelines_exporter_id_fkey" {
+ return ledger.NewErrExporterUsed(id)
+ }
+ return err
+ default:
+ return err
+ }
+ }
+
+ rowsAffected, err := ret.RowsAffected()
+ if err != nil {
+ panic(err)
+ }
+ if rowsAffected == 0 {
+ return sql.ErrNoRows
+ }
+
+ return err
+}
+
+func (d *DefaultStore) GetExporter(ctx context.Context, id string) (*ledger.Exporter, error) {
+ ret := &ledger.Exporter{}
+ err := d.db.NewSelect().
+ Model(ret).
+ Where("id = ?", id).
+ Scan(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ return ret, nil
+}
+
+func (d *DefaultStore) ListPipelines(ctx context.Context) (*bunpaginate.Cursor[ledger.Pipeline], error) {
+ return bunpaginate.UsingOffset[struct{}, ledger.Pipeline](
+ ctx,
+ d.db.NewSelect(),
+ bunpaginate.OffsetPaginatedQuery[struct{}]{},
+ )
+}
+
+func (d *DefaultStore) CreatePipeline(ctx context.Context, pipeline ledger.Pipeline) error {
+ _, err := d.db.NewInsert().
+ Model(&pipeline).
+ Exec(ctx)
+ if err != nil {
+ // notes(gfyrag): it is not safe to check errors like that
+ // but *pq.Error does not implement standard go utils for errors
+ // so, we don't have choice
+ err := postgres.ResolveError(err)
+ if errors.Is(err, postgres.ErrConstraintsFailed{}) {
+ return ledger.NewErrPipelineAlreadyExists(pipeline.PipelineConfiguration)
+ }
+
+ return err
+ }
+ return nil
+}
+
+func (d *DefaultStore) UpdatePipeline(ctx context.Context, id string, o map[string]any) (*ledger.Pipeline, error) {
+ updateQuery := d.db.NewUpdate().
+ Table("_system.pipelines")
+ for k, v := range o {
+ updateQuery = updateQuery.Set(k+" = ?", v)
+ }
+ updateQuery = updateQuery.
+ Set("version = version + 1").
+ Where("id = ?", id).
+ Returning("*")
+
+ ret := &ledger.Pipeline{}
+ _, err := updateQuery.Exec(ctx, ret)
+ if err != nil {
+ return nil, postgres.ResolveError(err)
+ }
+ return ret, nil
+}
+
+func (d *DefaultStore) DeletePipeline(ctx context.Context, id string) error {
+ ret, err := d.db.NewDelete().
+ Model(&ledger.Pipeline{}).
+ Where("id = ?", id).
+ Exec(ctx)
+ if err != nil {
+ return err
+ }
+
+ rowsAffected, err := ret.RowsAffected()
+ if err != nil {
+ panic(err)
+ }
+ if rowsAffected == 0 {
+ return sql.ErrNoRows
+ }
+
+ return err
+}
+
+func (d *DefaultStore) GetPipeline(ctx context.Context, id string) (*ledger.Pipeline, error) {
+ ret := &ledger.Pipeline{}
+ err := d.db.NewSelect().
+ Model(ret).
+ Where("id = ?", id).
+ Scan(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ return ret, nil
+}
+
+func (d *DefaultStore) ListEnabledPipelines(ctx context.Context) ([]ledger.Pipeline, error) {
+ ret := make([]ledger.Pipeline, 0)
+ if err := d.db.NewSelect().
+ Model(&ret).
+ Where("enabled").
+ Scan(ctx); err != nil {
+ return nil, err
+ }
+ return ret, nil
+}
+
+func (d *DefaultStore) StorePipelineState(ctx context.Context, id string, lastLogID uint64) error {
+ ret, err := d.db.NewUpdate().
+ Model(&ledger.Pipeline{}).
+ Where("id = ?", id).
+ Set("last_log_id = ?", lastLogID).
+ Exec(ctx)
+ if err != nil {
+ return fmt.Errorf("updating state in database: %w", err)
+ }
+ rowsAffected, err := ret.RowsAffected()
+ if err != nil {
+ panic(err)
+ }
+ if rowsAffected == 0 {
+ return sql.ErrNoRows
+ }
+
+ return nil
+}
diff --git a/internal/storage/system/store_test.go b/internal/storage/system/store_test.go
index c3778abae0..3670c4e27e 100644
--- a/internal/storage/system/store_test.go
+++ b/internal/storage/system/store_test.go
@@ -4,6 +4,7 @@ package system
import (
"context"
+ "encoding/json"
"fmt"
"github.com/formancehq/go-libs/v3/bun/bunconnect"
"github.com/formancehq/go-libs/v3/bun/bundebug"
@@ -11,7 +12,6 @@ import (
"github.com/formancehq/go-libs/v3/metadata"
"github.com/formancehq/go-libs/v3/testing/docker"
ledger "github.com/formancehq/ledger/internal"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
"github.com/formancehq/ledger/internal/storage/common"
"github.com/google/uuid"
"github.com/uptrace/bun"
@@ -92,13 +92,13 @@ func TestLedgersList(t *testing.T) {
ledgers = append(ledgers, l)
}
- cursor, err := store.Ledgers().Paginate(ctx, ledgercontroller.NewListLedgersQuery(pageSize))
+ cursor, err := store.Ledgers().Paginate(ctx, 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 := common.ColumnPaginatedQuery[any]{}
+ query := common.ColumnPaginatedQuery[ListLedgersQueryPayload]{}
require.NoError(t, bunpaginate.UnmarshalCursor(cursor.Next, &query))
cursor, err = store.Ledgers().Paginate(ctx, query)
@@ -150,7 +150,157 @@ func TestLedgerDeleteMetadata(t *testing.T) {
require.Equal(t, metadata.Metadata{}, ledgerFromDB.Metadata)
}
-func newStore(t docker.T) Store {
+func TestListEnabledPipelines(t *testing.T) {
+ ctx := logging.TestingContext()
+
+ store := newStore(t)
+
+ // Create a exporter
+ exporter := ledger.NewExporter(
+ ledger.NewExporterConfiguration("exporter1", json.RawMessage("")),
+ )
+ require.NoError(t, store.CreateExporter(ctx, exporter))
+
+ // Creating a pair which will be marked as ready
+ alivePipeline := ledger.NewPipeline(
+ ledger.NewPipelineConfiguration("module1", exporter.ID),
+ )
+
+ // Save a state
+ require.NoError(t, store.CreatePipeline(ctx, alivePipeline))
+
+ // Creating a pair which will be marked as stopped
+ stoppedPipeline := ledger.NewPipeline(
+ ledger.NewPipelineConfiguration("module2", exporter.ID),
+ )
+ stoppedPipeline.Enabled = false
+
+ // Save a state
+ require.NoError(t, store.CreatePipeline(ctx, stoppedPipeline))
+
+ // Read all states
+ states, err := store.ListEnabledPipelines(ctx)
+ require.NoError(t, err)
+ require.Len(t, states, 1)
+ require.Equal(t, alivePipeline, states[0])
+}
+
+func TestCreatePipeline(t *testing.T) {
+
+ ctx := logging.TestingContext()
+
+ store := newStore(t)
+
+ // Create a exporter
+ exporter := ledger.NewExporter(
+ ledger.NewExporterConfiguration("exporter1", json.RawMessage("")),
+ )
+ require.NoError(t, store.CreateExporter(ctx, exporter))
+
+ // Creating a pipeline which will be marked as ready
+ alivePipeline := ledger.NewPipeline(
+ ledger.NewPipelineConfiguration("module1", exporter.ID),
+ )
+
+ // Save a state
+ require.NoError(t, store.CreatePipeline(ctx, alivePipeline))
+
+ // Try to create the same pipeline again
+ require.IsType(t, ledger.ErrPipelineAlreadyExists{}, store.CreatePipeline(ctx, alivePipeline))
+
+ // Try to create another pipeline with the same configuration
+ newPipeline := ledger.NewPipeline(
+ ledger.NewPipelineConfiguration("module1", exporter.ID),
+ )
+ require.IsType(t, ledger.ErrPipelineAlreadyExists{}, store.CreatePipeline(ctx, newPipeline))
+}
+
+func TestDeletePipeline(t *testing.T) {
+
+ ctx := logging.TestingContext()
+
+ // Create the store
+ store := newStore(t)
+
+ // Create a exporter
+ exporter := ledger.NewExporter(
+ ledger.NewExporterConfiguration("exporter1", json.RawMessage("")),
+ )
+ require.NoError(t, store.CreateExporter(ctx, exporter))
+
+ // Creating a pair which will be marked as ready
+ alivePipeline := ledger.NewPipeline(
+ ledger.NewPipelineConfiguration("module1", exporter.ID),
+ )
+
+ // Save a state
+ require.NoError(t, store.CreatePipeline(ctx, alivePipeline))
+
+ // Try to create the same pipeline again
+ require.NoError(t, store.DeletePipeline(ctx, alivePipeline.ID))
+}
+
+func TestUpdatePipeline(t *testing.T) {
+
+ ctx := logging.TestingContext()
+
+ // Create the store
+ store := newStore(t)
+
+ // Create a exporter
+ exporter := ledger.NewExporter(
+ ledger.NewExporterConfiguration("exporter1", json.RawMessage("")),
+ )
+ require.NoError(t, store.CreateExporter(ctx, exporter))
+
+ // Creating a pair which will be marked as ready
+ alivePipeline := ledger.NewPipeline(
+ ledger.NewPipelineConfiguration("module1", exporter.ID),
+ )
+
+ // Save a state
+ require.NoError(t, store.CreatePipeline(ctx, alivePipeline))
+
+ // Try to create the same pipeline again
+ _, err := store.UpdatePipeline(ctx, alivePipeline.ID, map[string]any{
+ "enabled": false,
+ })
+ require.NoError(t, err)
+
+ pipelineFromDB, err := store.GetPipeline(ctx, alivePipeline.ID)
+ require.NoError(t, err)
+ require.False(t, pipelineFromDB.Enabled)
+
+ pipelineFromDB.Enabled = true
+ require.Equal(t, alivePipeline, *pipelineFromDB)
+}
+
+func TestDeleteExporter(t *testing.T) {
+ ctx := logging.TestingContext()
+
+ // Create the store
+ store := newStore(t)
+
+ // Create a exporter
+ exporter := ledger.NewExporter(
+ ledger.NewExporterConfiguration("exporter1", json.RawMessage("")),
+ )
+ require.NoError(t, store.CreateExporter(ctx, exporter))
+
+ // Creating a pipeline which will be marked as ready
+ pipeline := ledger.NewPipeline(
+ ledger.NewPipelineConfiguration("module1", exporter.ID),
+ )
+
+ // Save a state
+ require.NoError(t, store.CreatePipeline(ctx, pipeline))
+
+ // Pipelines should be deleted in cascade
+ err := store.DeleteExporter(ctx, pipeline.ExporterID)
+ require.NoError(t, err)
+}
+
+func newStore(t docker.T) *DefaultStore {
t.Helper()
ctx := logging.TestingContext()
diff --git a/internal/worker/async_block.go b/internal/storage/worker_async_block.go
similarity index 77%
rename from internal/worker/async_block.go
rename to internal/storage/worker_async_block.go
index 450c17c84d..4261c9c46b 100644
--- a/internal/worker/async_block.go
+++ b/internal/storage/worker_async_block.go
@@ -1,4 +1,4 @@
-package worker
+package storage
import (
"context"
@@ -7,7 +7,6 @@ import (
"github.com/formancehq/go-libs/v3/logging"
"github.com/formancehq/go-libs/v3/query"
"github.com/formancehq/ledger/internal"
- ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
"github.com/formancehq/ledger/internal/storage/common"
systemstore "github.com/formancehq/ledger/internal/storage/system"
"github.com/formancehq/ledger/pkg/features"
@@ -16,6 +15,7 @@ import (
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"go.opentelemetry.io/otel/trace/noop"
+ "go.uber.org/fx"
"time"
)
@@ -27,8 +27,8 @@ type AsyncBlockRunnerConfig struct {
type AsyncBlockRunner struct {
stopChannel chan chan struct{}
logger logging.Logger
- db *bun.DB
- cfg AsyncBlockRunnerConfig
+ db *bun.DB
+ cfg AsyncBlockRunnerConfig
tracer trace.Tracer
}
@@ -77,13 +77,13 @@ func (r *AsyncBlockRunner) run(ctx context.Context) error {
ctx, span := r.tracer.Start(ctx, "Run")
defer span.End()
- initialQuery := ledgercontroller.NewListLedgersQuery(10)
+ initialQuery := systemstore.NewListLedgersQuery(10)
initialQuery.Options.Builder = query.Match(fmt.Sprintf("features[%s]", features.FeatureHashLogs), "ASYNC")
systemStore := systemstore.New(r.db)
return bunpaginate.Iterate(
ctx,
initialQuery,
- func(ctx context.Context, q common.ColumnPaginatedQuery[any]) (*bunpaginate.Cursor[ledger.Ledger], error) {
+ func(ctx context.Context, q common.ColumnPaginatedQuery[systemstore.ListLedgersQueryPayload]) (*bunpaginate.Cursor[ledger.Ledger], error) {
return systemStore.Ledgers().Paginate(ctx, q)
},
func(cursor *bunpaginate.Cursor[ledger.Ledger]) error {
@@ -137,3 +137,25 @@ func WithTracer(tracer trace.Tracer) Option {
var defaultOptions = []Option{
WithTracer(noop.Tracer{}),
}
+
+func NewAsyncBlockRunnerModule(cfg AsyncBlockRunnerConfig) fx.Option {
+ return fx.Options(
+ fx.Provide(func(logger logging.Logger, db *bun.DB) (*AsyncBlockRunner, error) {
+ return NewAsyncBlockRunner(logger, db, cfg), nil
+ }),
+ fx.Invoke(func(lc fx.Lifecycle, asyncBlockRunner *AsyncBlockRunner) {
+ lc.Append(fx.Hook{
+ OnStart: func(ctx context.Context) error {
+ go func() {
+ if err := asyncBlockRunner.Run(context.WithoutCancel(ctx)); err != nil {
+ panic(err)
+ }
+ }()
+
+ return nil
+ },
+ OnStop: asyncBlockRunner.Stop,
+ })
+ }),
+ )
+}
\ No newline at end of file
diff --git a/internal/volumes.go b/internal/volumes.go
index 49036a7ea1..d21b84385a 100644
--- a/internal/volumes.go
+++ b/internal/volumes.go
@@ -170,3 +170,5 @@ func (a PostCommitVolumes) Merge(volumes PostCommitVolumes) PostCommitVolumes {
type AggregatedVolumes struct {
Aggregated VolumesByAssets `bun:"aggregated,type:jsonb"`
}
+
+type Balances = map[string]map[string]*big.Int
diff --git a/internal/worker/fx.go b/internal/worker/fx.go
deleted file mode 100644
index 6e6e772a3c..0000000000
--- a/internal/worker/fx.go
+++ /dev/null
@@ -1,50 +0,0 @@
-package worker
-
-import (
- "context"
- "github.com/formancehq/go-libs/v3/logging"
- "github.com/robfig/cron/v3"
- "github.com/uptrace/bun"
- "go.opentelemetry.io/otel/trace"
- "go.uber.org/fx"
-)
-
-type ModuleConfig struct {
- Schedule string
- MaxBlockSize int
-}
-
-func NewFXModule(cfg ModuleConfig) fx.Option {
- return fx.Options(
- fx.Provide(func(
- logger logging.Logger,
- db *bun.DB,
- traceProvider trace.TracerProvider,
- ) (*AsyncBlockRunner, error) {
- parser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
- schedule, err := parser.Parse(cfg.Schedule)
- if err != nil {
- return nil, err
- }
-
- return NewAsyncBlockRunner(logger, db, AsyncBlockRunnerConfig{
- MaxBlockSize: cfg.MaxBlockSize,
- Schedule: schedule,
- }, WithTracer(traceProvider.Tracer("AsyncBlockRunner"))), nil
- }),
- fx.Invoke(fx.Annotate(func(lc fx.Lifecycle, asyncBlockRunner *AsyncBlockRunner) {
- lc.Append(fx.Hook{
- OnStart: func(ctx context.Context) error {
- go func() {
- if err := asyncBlockRunner.Run(context.WithoutCancel(ctx)); err != nil {
- panic(err)
- }
- }()
-
- return nil
- },
- OnStop: asyncBlockRunner.Stop,
- })
- }, fx.ParamTags(``, ``, ``, `group:"workerModules"`))),
- )
-}
diff --git a/internal/worker/module.go b/internal/worker/module.go
new file mode 100644
index 0000000000..e383271e50
--- /dev/null
+++ b/internal/worker/module.go
@@ -0,0 +1,70 @@
+package worker
+
+import (
+ "fmt"
+ "github.com/formancehq/go-libs/v3/grpcserver"
+ "github.com/formancehq/go-libs/v3/serverport"
+ "github.com/formancehq/ledger/internal/replication"
+ innergrpc "github.com/formancehq/ledger/internal/replication/grpc"
+ "github.com/formancehq/ledger/internal/storage"
+ "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
+ "go.opentelemetry.io/otel/trace"
+ "go.uber.org/fx"
+ "google.golang.org/grpc"
+)
+
+type GRPCServerModuleConfig struct {
+ Address string
+ ServerOptions []grpc.ServerOption
+}
+
+type ModuleConfig struct {
+ AsyncBlockRunnerConfig storage.AsyncBlockRunnerConfig
+ ReplicationConfig replication.WorkerModuleConfig
+}
+
+func NewFXModule(cfg ModuleConfig) fx.Option {
+ return fx.Options(
+ // todo: add auto discovery
+ storage.NewAsyncBlockRunnerModule(cfg.AsyncBlockRunnerConfig),
+ // todo: add auto discovery
+ replication.NewWorkerFXModule(cfg.ReplicationConfig),
+ )
+}
+
+func NewGRPCServerFXModule(cfg GRPCServerModuleConfig) fx.Option {
+ return fx.Options(
+ fx.Invoke(func(lc fx.Lifecycle, replicationServer innergrpc.ReplicationServer, traceProvider trace.TracerProvider) {
+ lc.Append(grpcserver.NewHook(
+ grpcserver.WithServerPortOptions(
+ serverport.WithAddress(cfg.Address),
+ ),
+ grpcserver.WithGRPCSetupOptions(func(server *grpc.Server) {
+ innergrpc.RegisterReplicationServer(server, replicationServer)
+ }),
+ grpcserver.WithGRPCServerOptions(
+ grpc.StatsHandler(otelgrpc.NewServerHandler(otelgrpc.WithTracerProvider(traceProvider))),
+ ),
+ ))
+ }),
+ )
+}
+
+func NewGRPCClientFxModule(
+ address string,
+ options ...grpc.DialOption,
+) fx.Option {
+ return fx.Options(
+ fx.Provide(func(tracerProvider trace.TracerProvider) (*grpc.ClientConn, error) {
+ client, err := grpc.NewClient(address, append(
+ options,
+ grpc.WithStatsHandler(otelgrpc.NewClientHandler(otelgrpc.WithTracerProvider(tracerProvider))),
+ )...)
+ if err != nil {
+ return nil, fmt.Errorf("failed to dial: %v", err)
+ }
+
+ return client, nil
+ }),
+ )
+}
diff --git a/openapi.yaml b/openapi.yaml
index d46a5701cc..39b29802aa 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -2593,6 +2593,260 @@ paths:
security:
- Authorization:
- ledger:write
+ /v2/_/exporters:
+ get:
+ summary: List exporters
+ operationId: v2ListExporters
+ x-speakeasy-name-override: ListExporters
+ tags:
+ - ledger.v2
+ responses:
+ "200":
+ $ref: '#/components/responses/V2ListExportersResponse'
+ default:
+ description: Error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/V2ErrorResponse"
+ post:
+ summary: Create exporter
+ operationId: v2CreateExporter
+ x-speakeasy-name-override: CreateExporter
+ tags:
+ - ledger.v2
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/V2CreateExporterRequest'
+ responses:
+ "201":
+ $ref: '#/components/responses/V2CreateExporterResponse'
+ default:
+ description: Error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/V2ErrorResponse"
+ /v2/_/exporters/{exporterID}:
+ parameters:
+ - name: exporterID
+ description: The exporter id
+ in: path
+ schema:
+ type: string
+ required: true
+ get:
+ summary: Get exporter state
+ operationId: v2GetExporterState
+ x-speakeasy-name-override: GetExporterState
+ tags:
+ - ledger.v2
+ responses:
+ "200":
+ $ref: '#/components/responses/V2GetExporterStateResponse'
+ default:
+ description: Error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/V2ErrorResponse"
+ delete:
+ summary: Delete exporter
+ operationId: v2DeleteExporter
+ x-speakeasy-name-override: DeleteExporter
+ tags:
+ - ledger.v2
+ responses:
+ "204":
+ description: Exporter deleted
+ default:
+ description: Error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/V2ErrorResponse"
+ /v2/{ledger}/pipelines:
+ parameters:
+ - name: ledger
+ in: path
+ description: Name of the ledger.
+ required: true
+ schema:
+ type: string
+ example: ledger001
+ get:
+ summary: List pipelines
+ operationId: v2ListPipelines
+ x-speakeasy-name-override: ListPipelines
+ tags:
+ - ledger.v2
+ responses:
+ "200":
+ $ref: '#/components/responses/V2ListPipelinesResponse'
+ default:
+ description: Error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/V2ErrorResponse"
+ post:
+ summary: Create pipeline
+ operationId: v2CreatePipeline
+ x-speakeasy-name-override: CreatePipeline
+ tags:
+ - ledger.v2
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/V2CreatePipelineRequest'
+ responses:
+ "201":
+ $ref: '#/components/responses/V2CreatePipelineResponse'
+ default:
+ description: Error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/V2ErrorResponse"
+ /v2/{ledger}/pipelines/{pipelineID}:
+ parameters:
+ - name: ledger
+ in: path
+ description: Name of the ledger.
+ required: true
+ schema:
+ type: string
+ example: ledger001
+ - name: pipelineID
+ description: The pipeline id
+ in: path
+ schema:
+ type: string
+ required: true
+ get:
+ summary: Get pipeline state
+ operationId: v2GetPipelineState
+ x-speakeasy-name-override: GetPipelineState
+ tags:
+ - ledger.v2
+ responses:
+ "200":
+ $ref: '#/components/responses/V2GetPipelineStateResponse'
+ default:
+ description: Error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/V2ErrorResponse"
+ delete:
+ summary: Delete pipeline
+ operationId: v2DeletePipeline
+ x-speakeasy-name-override: DeletePipeline
+ tags:
+ - ledger.v2
+ responses:
+ "204":
+ description: Pipeline deleted
+ default:
+ description: Error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/V2ErrorResponse"
+ /v2/{ledger}/pipelines/{pipelineID}/reset:
+ parameters:
+ - name: ledger
+ in: path
+ description: Name of the ledger.
+ required: true
+ schema:
+ type: string
+ example: ledger001
+ - name: pipelineID
+ description: The pipeline id
+ in: path
+ schema:
+ type: string
+ required: true
+ post:
+ summary: Reset pipeline
+ operationId: v2ResetPipeline
+ x-speakeasy-name-override: ResetPipeline
+ tags:
+ - ledger.v2
+ responses:
+ "202":
+ description: Pipeline reset
+ default:
+ description: Error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/V2ErrorResponse"
+ /v2/{ledger}/pipelines/{pipelineID}/start:
+ parameters:
+ - name: ledger
+ in: path
+ description: Name of the ledger.
+ required: true
+ schema:
+ type: string
+ example: ledger001
+ - name: pipelineID
+ description: The pipeline id
+ in: path
+ schema:
+ type: string
+ required: true
+ post:
+ summary: Start pipeline
+ operationId: v2StartPipeline
+ x-speakeasy-name-override: StartPipeline
+ tags:
+ - ledger.v2
+ responses:
+ "202":
+ description: Pipeline started
+ default:
+ description: Error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/V2ErrorResponse"
+ /v2/{ledger}/pipelines/{pipelineID}/stop:
+ parameters:
+ - name: ledger
+ in: path
+ description: Name of the ledger.
+ required: true
+ schema:
+ type: string
+ example: ledger001
+ - name: pipelineID
+ description: The pipeline id
+ in: path
+ schema:
+ type: string
+ required: true
+ post:
+ summary: Stop pipeline
+ operationId: v2StopPipeline
+ x-speakeasy-name-override: StopPipeline
+ tags:
+ - ledger.v2
+ responses:
+ "202":
+ description: Pipeline stopped
+ default:
+ description: Error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/V2ErrorResponse"
x-speakeasy-errors:
statusCodes:
- default
@@ -3156,6 +3410,7 @@ components:
- TIMEOUT
example: INSUFFICIENT_FUND
LedgerInfoResponse:
+ type: object
properties:
data:
$ref: '#/components/schemas/LedgerInfo'
@@ -3190,6 +3445,68 @@ components:
enum:
- TO DO
- DONE
+ V2ExportersCursorResponse:
+ type: object
+ required:
+ - cursor
+ properties:
+ cursor:
+ type: object
+ required:
+ - pageSize
+ - hasMore
+ - data
+ properties:
+ pageSize:
+ type: integer
+ format: int64
+ minimum: 1
+ maximum: 1000
+ example: 15
+ hasMore:
+ type: boolean
+ example: false
+ previous:
+ type: string
+ example: YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol=
+ next:
+ type: string
+ example: ""
+ data:
+ type: array
+ items:
+ $ref: "#/components/schemas/V2Exporter"
+ V2PipelinesCursorResponse:
+ type: object
+ required:
+ - cursor
+ properties:
+ cursor:
+ type: object
+ required:
+ - pageSize
+ - hasMore
+ - data
+ properties:
+ pageSize:
+ type: integer
+ format: int64
+ minimum: 1
+ maximum: 1000
+ example: 15
+ hasMore:
+ type: boolean
+ example: false
+ previous:
+ type: string
+ example: YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol=
+ next:
+ type: string
+ example: ""
+ data:
+ type: array
+ items:
+ $ref: "#/components/schemas/V2Pipeline"
V2AccountsCursorResponse:
type: object
required:
@@ -3952,5 +4269,141 @@ components:
file:
type: string
format: binary
+ V2CreatePipelineRequest:
+ type: object
+ properties:
+ exporterID:
+ type: string
+ required:
+ - exporterID
+ V2CreateExporterRequest:
+ $ref: '#/components/schemas/V2ExporterConfiguration'
+ V2PipelineConfiguration:
+ properties:
+ ledger:
+ type: string
+ exporterID:
+ type: string
+ required:
+ - ledger
+ - exporterID
+ V2ExporterConfiguration:
+ type: object
+ properties:
+ driver:
+ type: string
+ config:
+ type: object
+ additionalProperties: true
+ required:
+ - driver
+ - config
+ V2Exporter:
+ type: object
+ allOf:
+ - $ref: '#/components/schemas/V2ExporterConfiguration'
+ - type: object
+ properties:
+ id:
+ type: string
+ createdAt:
+ type: string
+ format: date-time
+ required:
+ - id
+ - createdAt
+ V2Pipeline:
+ allOf:
+ - $ref: '#/components/schemas/V2PipelineConfiguration'
+ - type: object
+ properties:
+ id:
+ type: string
+ createdAt:
+ type: string
+ format: date-time
+ lastLogID:
+ type: integer
+ enabled:
+ type: boolean
+ required:
+ - id
+ - createdAt
+ responses:
+ V2CreatePipelineResponse:
+ description: Created ipeline
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ $ref: '#/components/schemas/V2Pipeline'
+ required:
+ - data
+ V2ListPipelinesResponse:
+ description: Pipelines list
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ cursor:
+ allOf:
+ - $ref: '#/components/schemas/V2PipelinesCursorResponse'
+ - properties:
+ data:
+ type: array
+ items:
+ $ref: '#/components/schemas/V2Pipeline'
+ V2GetPipelineStateResponse:
+ description: Pipeline information
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ $ref: '#/components/schemas/V2Pipeline'
+ required:
+ - data
+ V2CreateExporterResponse:
+ description: Created exporter
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ $ref: '#/components/schemas/V2Exporter'
+ required:
+ - data
+ V2ListExportersResponse:
+ description: Exporters list
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ cursor:
+ allOf:
+ - $ref: '#/components/schemas/V2ExportersCursorResponse'
+ - type: object
+ properties:
+ data:
+ type: array
+ items:
+ $ref: '#/components/schemas/V2Exporter'
+ V2GetExporterStateResponse:
+ description: Exporter information
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ $ref: '#/components/schemas/V2Exporter'
+ required:
+ - data
servers:
- url: http://localhost:8080/
diff --git a/openapi/v1.yaml b/openapi/v1.yaml
index 41754204e7..e78b54e0dd 100644
--- a/openapi/v1.yaml
+++ b/openapi/v1.yaml
@@ -1849,6 +1849,7 @@ components:
- TIMEOUT
example: INSUFFICIENT_FUND
LedgerInfoResponse:
+ type: object
properties:
data:
$ref: '#/components/schemas/LedgerInfo'
diff --git a/openapi/v2.yaml b/openapi/v2.yaml
index 5ce569c812..ccda0ae19b 100644
--- a/openapi/v2.yaml
+++ b/openapi/v2.yaml
@@ -1336,6 +1336,260 @@ paths:
security:
- Authorization:
- ledger:write
+ /v2/_/exporters:
+ get:
+ summary: List exporters
+ operationId: v2ListExporters
+ x-speakeasy-name-override: ListExporters
+ tags:
+ - ledger.v2
+ responses:
+ "200":
+ $ref: '#/components/responses/V2ListExportersResponse'
+ default:
+ description: Error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/V2ErrorResponse"
+ post:
+ summary: Create exporter
+ operationId: v2CreateExporter
+ x-speakeasy-name-override: CreateExporter
+ tags:
+ - ledger.v2
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/V2CreateExporterRequest'
+ responses:
+ "201":
+ $ref: '#/components/responses/V2CreateExporterResponse'
+ default:
+ description: Error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/V2ErrorResponse"
+ /v2/_/exporters/{exporterID}:
+ parameters:
+ - name: exporterID
+ description: The exporter id
+ in: path
+ schema:
+ type: string
+ required: true
+ get:
+ summary: Get exporter state
+ operationId: v2GetExporterState
+ x-speakeasy-name-override: GetExporterState
+ tags:
+ - ledger.v2
+ responses:
+ "200":
+ $ref: '#/components/responses/V2GetExporterStateResponse'
+ default:
+ description: Error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/V2ErrorResponse"
+ delete:
+ summary: Delete exporter
+ operationId: v2DeleteExporter
+ x-speakeasy-name-override: DeleteExporter
+ tags:
+ - ledger.v2
+ responses:
+ "204":
+ description: Exporter deleted
+ default:
+ description: Error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/V2ErrorResponse"
+ /v2/{ledger}/pipelines:
+ parameters:
+ - name: ledger
+ in: path
+ description: Name of the ledger.
+ required: true
+ schema:
+ type: string
+ example: ledger001
+ get:
+ summary: List pipelines
+ operationId: v2ListPipelines
+ x-speakeasy-name-override: ListPipelines
+ tags:
+ - ledger.v2
+ responses:
+ "200":
+ $ref: '#/components/responses/V2ListPipelinesResponse'
+ default:
+ description: Error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/V2ErrorResponse"
+ post:
+ summary: Create pipeline
+ operationId: v2CreatePipeline
+ x-speakeasy-name-override: CreatePipeline
+ tags:
+ - ledger.v2
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/V2CreatePipelineRequest'
+ responses:
+ "201":
+ $ref: '#/components/responses/V2CreatePipelineResponse'
+ default:
+ description: Error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/V2ErrorResponse"
+ /v2/{ledger}/pipelines/{pipelineID}:
+ parameters:
+ - name: ledger
+ in: path
+ description: Name of the ledger.
+ required: true
+ schema:
+ type: string
+ example: ledger001
+ - name: pipelineID
+ description: The pipeline id
+ in: path
+ schema:
+ type: string
+ required: true
+ get:
+ summary: Get pipeline state
+ operationId: v2GetPipelineState
+ x-speakeasy-name-override: GetPipelineState
+ tags:
+ - ledger.v2
+ responses:
+ "200":
+ $ref: '#/components/responses/V2GetPipelineStateResponse'
+ default:
+ description: Error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/V2ErrorResponse"
+ delete:
+ summary: Delete pipeline
+ operationId: v2DeletePipeline
+ x-speakeasy-name-override: DeletePipeline
+ tags:
+ - ledger.v2
+ responses:
+ "204":
+ description: Pipeline deleted
+ default:
+ description: Error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/V2ErrorResponse"
+ /v2/{ledger}/pipelines/{pipelineID}/reset:
+ parameters:
+ - name: ledger
+ in: path
+ description: Name of the ledger.
+ required: true
+ schema:
+ type: string
+ example: ledger001
+ - name: pipelineID
+ description: The pipeline id
+ in: path
+ schema:
+ type: string
+ required: true
+ post:
+ summary: Reset pipeline
+ operationId: v2ResetPipeline
+ x-speakeasy-name-override: ResetPipeline
+ tags:
+ - ledger.v2
+ responses:
+ "202":
+ description: Pipeline reset
+ default:
+ description: Error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/V2ErrorResponse"
+ /v2/{ledger}/pipelines/{pipelineID}/start:
+ parameters:
+ - name: ledger
+ in: path
+ description: Name of the ledger.
+ required: true
+ schema:
+ type: string
+ example: ledger001
+ - name: pipelineID
+ description: The pipeline id
+ in: path
+ schema:
+ type: string
+ required: true
+ post:
+ summary: Start pipeline
+ operationId: v2StartPipeline
+ x-speakeasy-name-override: StartPipeline
+ tags:
+ - ledger.v2
+ responses:
+ "202":
+ description: Pipeline started
+ default:
+ description: Error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/V2ErrorResponse"
+ /v2/{ledger}/pipelines/{pipelineID}/stop:
+ parameters:
+ - name: ledger
+ in: path
+ description: Name of the ledger.
+ required: true
+ schema:
+ type: string
+ example: ledger001
+ - name: pipelineID
+ description: The pipeline id
+ in: path
+ schema:
+ type: string
+ required: true
+ post:
+ summary: Stop pipeline
+ operationId: v2StopPipeline
+ x-speakeasy-name-override: StopPipeline
+ tags:
+ - ledger.v2
+ responses:
+ "202":
+ description: Pipeline stopped
+ default:
+ description: Error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/V2ErrorResponse"
components:
securitySchemes:
Authorization:
@@ -1345,8 +1599,148 @@ components:
tokenUrl: "/oauth/token"
refreshUrl: "/oauth/token"
scopes: {}
+ responses:
+ V2CreatePipelineResponse:
+ description: Created ipeline
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ $ref: '#/components/schemas/V2Pipeline'
+ required:
+ - data
+ V2ListPipelinesResponse:
+ description: Pipelines list
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ cursor:
+ allOf:
+ - $ref: '#/components/schemas/V2PipelinesCursorResponse'
+ - properties:
+ data:
+ type: array
+ items:
+ $ref: '#/components/schemas/V2Pipeline'
+
+ V2GetPipelineStateResponse:
+ description: Pipeline information
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ $ref: '#/components/schemas/V2Pipeline'
+ required:
+ - data
+ V2CreateExporterResponse:
+ description: Created exporter
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ $ref: '#/components/schemas/V2Exporter'
+ required:
+ - data
+ V2ListExportersResponse:
+ description: Exporters list
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ cursor:
+ allOf:
+ - $ref: '#/components/schemas/V2ExportersCursorResponse'
+ - type: object
+ properties:
+ data:
+ type: array
+ items:
+ $ref: '#/components/schemas/V2Exporter'
+
+ V2GetExporterStateResponse:
+ description: Exporter information
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ $ref: '#/components/schemas/V2Exporter'
+ required:
+ - data
schemas:
+ V2ExportersCursorResponse:
+ type: object
+ required:
+ - cursor
+ properties:
+ cursor:
+ type: object
+ required:
+ - pageSize
+ - hasMore
+ - data
+ properties:
+ pageSize:
+ type: integer
+ format: int64
+ minimum: 1
+ maximum: 1000
+ example: 15
+ hasMore:
+ type: boolean
+ example: false
+ previous:
+ type: string
+ example: YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol=
+ next:
+ type: string
+ example: ""
+ data:
+ type: array
+ items:
+ $ref: "#/components/schemas/V2Exporter"
+ V2PipelinesCursorResponse:
+ type: object
+ required:
+ - cursor
+ properties:
+ cursor:
+ type: object
+ required:
+ - pageSize
+ - hasMore
+ - data
+ properties:
+ pageSize:
+ type: integer
+ format: int64
+ minimum: 1
+ maximum: 1000
+ example: 15
+ hasMore:
+ type: boolean
+ example: false
+ previous:
+ type: string
+ example: YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol=
+ next:
+ type: string
+ example: ""
+ data:
+ type: array
+ items:
+ $ref: "#/components/schemas/V2Pipeline"
V2AccountsCursorResponse:
type: object
required:
@@ -2109,3 +2503,63 @@ components:
file:
type: string
format: binary
+ V2CreatePipelineRequest:
+ type: object
+ properties:
+ exporterID:
+ type: string
+ required:
+ - exporterID
+ V2CreateExporterRequest:
+ $ref: '#/components/schemas/V2ExporterConfiguration'
+ V2PipelineConfiguration:
+ properties:
+ ledger:
+ type: string
+ exporterID:
+ type: string
+ required:
+ - ledger
+ - exporterID
+ V2ExporterConfiguration:
+ type: object
+ properties:
+ driver:
+ type: string
+ config:
+ type: object
+ additionalProperties: true
+ required:
+ - driver
+ - config
+ V2Exporter:
+ type: object
+ allOf:
+ - $ref: '#/components/schemas/V2ExporterConfiguration'
+ - type: object
+ properties:
+ id:
+ type: string
+ createdAt:
+ type: string
+ format: date-time
+ required:
+ - id
+ - createdAt
+ V2Pipeline:
+ allOf:
+ - $ref: '#/components/schemas/V2PipelineConfiguration'
+ - type: object
+ properties:
+ id:
+ type: string
+ createdAt:
+ type: string
+ format: date-time
+ lastLogID:
+ type: integer
+ enabled:
+ type: boolean
+ required:
+ - id
+ - createdAt
diff --git a/pkg/client/.speakeasy/gen.lock b/pkg/client/.speakeasy/gen.lock
index d043908ca9..5ef5144438 100644
--- a/pkg/client/.speakeasy/gen.lock
+++ b/pkg/client/.speakeasy/gen.lock
@@ -1,7 +1,7 @@
lockVersion: 2.0.0
id: a9ac79e1-e429-4ee3-96c4-ec973f19bec3
management:
- docChecksum: ce0c8826dc64a0dde9cb29984a1013d1
+ docChecksum: 37eed4d21b520c4cd11173bd59222513
docVersion: v2
speakeasyVersion: 1.517.3
generationVersion: 2.548.6
@@ -83,19 +83,29 @@ generatedFiles:
- /models/components/v2bulkelementreverttransaction.go
- /models/components/v2bulkresponse.go
- /models/components/v2configinforesponse.go
+ - /models/components/v2createexporterrequest.go
+ - /models/components/v2createexporterresponse.go
- /models/components/v2createledgerrequest.go
+ - /models/components/v2createpipelinerequest.go
+ - /models/components/v2createpipelineresponse.go
- /models/components/v2createtransactionresponse.go
- /models/components/v2errorresponse.go
- /models/components/v2errorsenum.go
+ - /models/components/v2exporter.go
+ - /models/components/v2getexporterstateresponse.go
- /models/components/v2getledgerresponse.go
+ - /models/components/v2getpipelinestateresponse.go
- /models/components/v2gettransactionresponse.go
- /models/components/v2ledger.go
- /models/components/v2ledgerinfo.go
- /models/components/v2ledgerinforesponse.go
- /models/components/v2ledgerlistresponse.go
+ - /models/components/v2listexportersresponse.go
+ - /models/components/v2listpipelinesresponse.go
- /models/components/v2log.go
- /models/components/v2logscursorresponse.go
- /models/components/v2migrationinfo.go
+ - /models/components/v2pipeline.go
- /models/components/v2posting.go
- /models/components/v2posttransaction.go
- /models/components/v2reverttransactionresponse.go
@@ -135,26 +145,37 @@ generatedFiles:
- /models/operations/v2countaccounts.go
- /models/operations/v2counttransactions.go
- /models/operations/v2createbulk.go
+ - /models/operations/v2createexporter.go
- /models/operations/v2createledger.go
+ - /models/operations/v2createpipeline.go
- /models/operations/v2createtransaction.go
- /models/operations/v2deleteaccountmetadata.go
+ - /models/operations/v2deleteexporter.go
- /models/operations/v2deleteledgermetadata.go
+ - /models/operations/v2deletepipeline.go
- /models/operations/v2deletetransactionmetadata.go
- /models/operations/v2exportlogs.go
- /models/operations/v2getaccount.go
- /models/operations/v2getbalancesaggregated.go
+ - /models/operations/v2getexporterstate.go
- /models/operations/v2getinfo.go
- /models/operations/v2getledger.go
- /models/operations/v2getledgerinfo.go
+ - /models/operations/v2getpipelinestate.go
- /models/operations/v2gettransaction.go
- /models/operations/v2getvolumeswithbalances.go
- /models/operations/v2importlogs.go
- /models/operations/v2listaccounts.go
+ - /models/operations/v2listexporters.go
- /models/operations/v2listledgers.go
- /models/operations/v2listlogs.go
+ - /models/operations/v2listpipelines.go
- /models/operations/v2listtransactions.go
- /models/operations/v2readstats.go
+ - /models/operations/v2resetpipeline.go
- /models/operations/v2reverttransaction.go
+ - /models/operations/v2startpipeline.go
+ - /models/operations/v2stoppipeline.go
- /models/operations/v2updateledgermetadata.go
- /models/sdkerrors/errorresponse.go
- /models/sdkerrors/v2errorresponse.go
@@ -224,11 +245,18 @@ generatedFiles:
- docs/models/components/v2bulkelementreverttransactiondata.md
- docs/models/components/v2bulkresponse.md
- docs/models/components/v2configinforesponse.md
+ - docs/models/components/v2createexporterrequest.md
+ - docs/models/components/v2createexporterresponse.md
- docs/models/components/v2createledgerrequest.md
+ - docs/models/components/v2createpipelinerequest.md
+ - docs/models/components/v2createpipelineresponse.md
- docs/models/components/v2createtransactionresponse.md
- docs/models/components/v2errorresponse.md
- docs/models/components/v2errorsenum.md
+ - docs/models/components/v2exporter.md
+ - docs/models/components/v2getexporterstateresponse.md
- docs/models/components/v2getledgerresponse.md
+ - docs/models/components/v2getpipelinestateresponse.md
- docs/models/components/v2gettransactionresponse.md
- docs/models/components/v2ledger.md
- docs/models/components/v2ledgerinfo.md
@@ -236,12 +264,19 @@ generatedFiles:
- docs/models/components/v2ledgerinfostorage.md
- docs/models/components/v2ledgerlistresponse.md
- docs/models/components/v2ledgerlistresponsecursor.md
+ - docs/models/components/v2listexportersresponse.md
+ - docs/models/components/v2listexportersresponsecursor.md
+ - docs/models/components/v2listexportersresponsecursorcursor.md
+ - docs/models/components/v2listpipelinesresponse.md
+ - docs/models/components/v2listpipelinesresponsecursor.md
+ - docs/models/components/v2listpipelinesresponsecursorcursor.md
- docs/models/components/v2log.md
- docs/models/components/v2logscursorresponse.md
- docs/models/components/v2logscursorresponsecursor.md
- docs/models/components/v2logtype.md
- docs/models/components/v2migrationinfo.md
- docs/models/components/v2migrationinfostate.md
+ - docs/models/components/v2pipeline.md
- docs/models/components/v2posting.md
- docs/models/components/v2posttransaction.md
- docs/models/components/v2posttransactionscript.md
@@ -311,14 +346,21 @@ generatedFiles:
- docs/models/operations/v2counttransactionsresponse.md
- docs/models/operations/v2createbulkrequest.md
- docs/models/operations/v2createbulkresponse.md
+ - docs/models/operations/v2createexporterresponse.md
- docs/models/operations/v2createledgerrequest.md
- docs/models/operations/v2createledgerresponse.md
+ - docs/models/operations/v2createpipelinerequest.md
+ - docs/models/operations/v2createpipelineresponse.md
- docs/models/operations/v2createtransactionrequest.md
- docs/models/operations/v2createtransactionresponse.md
- docs/models/operations/v2deleteaccountmetadatarequest.md
- docs/models/operations/v2deleteaccountmetadataresponse.md
+ - docs/models/operations/v2deleteexporterrequest.md
+ - docs/models/operations/v2deleteexporterresponse.md
- docs/models/operations/v2deleteledgermetadatarequest.md
- docs/models/operations/v2deleteledgermetadataresponse.md
+ - docs/models/operations/v2deletepipelinerequest.md
+ - docs/models/operations/v2deletepipelineresponse.md
- docs/models/operations/v2deletetransactionmetadatarequest.md
- docs/models/operations/v2deletetransactionmetadataresponse.md
- docs/models/operations/v2exportlogsrequest.md
@@ -327,11 +369,15 @@ generatedFiles:
- docs/models/operations/v2getaccountresponse.md
- docs/models/operations/v2getbalancesaggregatedrequest.md
- docs/models/operations/v2getbalancesaggregatedresponse.md
+ - docs/models/operations/v2getexporterstaterequest.md
+ - docs/models/operations/v2getexporterstateresponse.md
- docs/models/operations/v2getinforesponse.md
- docs/models/operations/v2getledgerinforequest.md
- docs/models/operations/v2getledgerinforesponse.md
- docs/models/operations/v2getledgerrequest.md
- docs/models/operations/v2getledgerresponse.md
+ - docs/models/operations/v2getpipelinestaterequest.md
+ - docs/models/operations/v2getpipelinestateresponse.md
- docs/models/operations/v2gettransactionrequest.md
- docs/models/operations/v2gettransactionresponse.md
- docs/models/operations/v2getvolumeswithbalancesrequest.md
@@ -340,16 +386,25 @@ generatedFiles:
- docs/models/operations/v2importlogsresponse.md
- docs/models/operations/v2listaccountsrequest.md
- docs/models/operations/v2listaccountsresponse.md
+ - docs/models/operations/v2listexportersresponse.md
- docs/models/operations/v2listledgersrequest.md
- docs/models/operations/v2listledgersresponse.md
- docs/models/operations/v2listlogsrequest.md
- docs/models/operations/v2listlogsresponse.md
+ - docs/models/operations/v2listpipelinesrequest.md
+ - docs/models/operations/v2listpipelinesresponse.md
- docs/models/operations/v2listtransactionsrequest.md
- docs/models/operations/v2listtransactionsresponse.md
- docs/models/operations/v2readstatsrequest.md
- docs/models/operations/v2readstatsresponse.md
+ - docs/models/operations/v2resetpipelinerequest.md
+ - docs/models/operations/v2resetpipelineresponse.md
- docs/models/operations/v2reverttransactionrequest.md
- docs/models/operations/v2reverttransactionresponse.md
+ - docs/models/operations/v2startpipelinerequest.md
+ - docs/models/operations/v2startpipelineresponse.md
+ - docs/models/operations/v2stoppipelinerequest.md
+ - docs/models/operations/v2stoppipelineresponse.md
- docs/models/operations/v2updateledgermetadatarequest.md
- docs/models/operations/v2updateledgermetadataresponse.md
- docs/models/sdkerrors/errorresponse.md
@@ -967,5 +1022,140 @@ examples:
responses:
default:
application/octet-stream: "x-file: example.file"
+ v2ListConnectors:
+ speakeasy-default-v2-list-connectors:
+ responses:
+ "200":
+ application/json: {"cursor": {"cursor": {"pageSize": 15, "hasMore": false, "previous": "YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol=", "next": "", "data": [{"driver": "", "config": {"key": "", "key1": "", "key2": ""}, "id": "", "createdAt": "2024-10-12T02:10:22.631Z"}]}}}
+ default:
+ application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] invalid 'cursor' query param", "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"}
+ v2CreateConnector:
+ speakeasy-default-v2-create-connector:
+ requestBody:
+ application/json: {"driver": "", "config": {"key": "", "key1": ""}}
+ responses:
+ "201":
+ application/json: {"data": {"driver": "", "config": {}, "id": "", "createdAt": "2023-09-20T16:38:56.879Z"}}
+ default:
+ application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] invalid 'cursor' query param", "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"}
+ v2GetConnectorState:
+ speakeasy-default-v2-get-connector-state:
+ parameters:
+ path:
+ connectorID: ""
+ responses:
+ "200":
+ application/json: {"data": {"driver": "", "config": {"key": "", "key1": "", "key2": ""}, "id": "", "createdAt": "2024-06-07T10:29:56.255Z"}}
+ default:
+ application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] invalid 'cursor' query param", "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"}
+ v2DeleteConnector:
+ speakeasy-default-v2-delete-connector:
+ parameters:
+ path:
+ connectorID: ""
+ responses:
+ default:
+ application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] invalid 'cursor' query param", "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"}
+ v2ListPipelines:
+ speakeasy-default-v2-list-pipelines:
+ parameters:
+ path:
+ ledger: "ledger001"
+ responses:
+ "200":
+ application/json: {"cursor": {"cursor": {"pageSize": 15, "hasMore": false, "previous": "YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol=", "next": "", "data": []}}}
+ default:
+ application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] invalid 'cursor' query param", "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"}
+ v2CreatePipeline:
+ speakeasy-default-v2-create-pipeline:
+ parameters:
+ path:
+ ledger: "ledger001"
+ responses:
+ "201":
+ application/json: {"data": {"ledger": "", "exporterID": "", "id": "", "createdAt": "2025-03-25T21:15:52.531Z"}}
+ default:
+ application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] invalid 'cursor' query param", "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"}
+ v2GetPipelineState:
+ speakeasy-default-v2-get-pipeline-state:
+ parameters:
+ path:
+ ledger: "ledger001"
+ pipelineID: ""
+ responses:
+ "200":
+ application/json: {"data": {"ledger": "", "exporterID": "", "id": "", "createdAt": "2023-11-10T12:13:58.484Z"}}
+ default:
+ application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] invalid 'cursor' query param", "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"}
+ v2DeletePipeline:
+ speakeasy-default-v2-delete-pipeline:
+ parameters:
+ path:
+ ledger: "ledger001"
+ pipelineID: ""
+ responses:
+ default:
+ application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] invalid 'cursor' query param", "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"}
+ v2ResetPipeline:
+ speakeasy-default-v2-reset-pipeline:
+ parameters:
+ path:
+ ledger: "ledger001"
+ pipelineID: ""
+ responses:
+ default:
+ application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] invalid 'cursor' query param", "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"}
+ v2StartPipeline:
+ speakeasy-default-v2-start-pipeline:
+ parameters:
+ path:
+ ledger: "ledger001"
+ pipelineID: ""
+ responses:
+ default:
+ application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] invalid 'cursor' query param", "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"}
+ v2StopPipeline:
+ speakeasy-default-v2-stop-pipeline:
+ parameters:
+ path:
+ ledger: "ledger001"
+ pipelineID: ""
+ responses:
+ default:
+ application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] invalid 'cursor' query param", "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"}
+ v2ListExporters:
+ speakeasy-default-v2-list-exporters:
+ responses:
+ "200":
+ application/json: {"cursor": {"cursor": {"pageSize": 15, "hasMore": false, "previous": "YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol=", "next": "", "data": [{"driver": "", "config": {}, "id": "", "createdAt": "2024-02-03T00:04:53.543Z"}]}}}
+ default:
+ application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] invalid 'cursor' query param", "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"}
+ v2CreateExporter:
+ speakeasy-default-v2-create-exporter:
+ requestBody:
+ application/json: {"driver": "", "config": {"key": "", "key1": ""}}
+ responses:
+ "201":
+ application/json: {"data": {"driver": "", "config": {"key": ""}, "id": "", "createdAt": "2023-06-14T15:07:35.927Z"}}
+ default:
+ application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] invalid 'cursor' query param", "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"}
+ v2GetExporterState:
+ speakeasy-default-v2-get-exporter-state:
+ parameters:
+ path:
+ exporterID: ""
+ responses:
+ "200":
+ application/json: {"data": {"driver": "", "config": {}, "id": "", "createdAt": "2024-09-04T23:13:06.746Z"}}
+ default:
+ application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] invalid 'cursor' query param", "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"}
+ v2DeleteExporter:
+ speakeasy-default-v2-delete-exporter:
+ parameters:
+ path:
+ exporterID: ""
+ responses:
+ default:
+ application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] invalid 'cursor' query param", "details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"}
examplesVersion: 1.0.0
generatedTests: {}
diff --git a/pkg/client/README.md b/pkg/client/README.md
index 4d79f6efa4..0c75097fd9 100644
--- a/pkg/client/README.md
+++ b/pkg/client/README.md
@@ -152,6 +152,17 @@ func main() {
* [ListLogs](docs/sdks/v2/README.md#listlogs) - List the logs from a ledger
* [ImportLogs](docs/sdks/v2/README.md#importlogs)
* [ExportLogs](docs/sdks/v2/README.md#exportlogs) - Export logs
+* [ListExporters](docs/sdks/v2/README.md#listexporters) - List exporters
+* [CreateExporter](docs/sdks/v2/README.md#createexporter) - Create exporter
+* [GetExporterState](docs/sdks/v2/README.md#getexporterstate) - Get exporter state
+* [DeleteExporter](docs/sdks/v2/README.md#deleteexporter) - Delete exporter
+* [ListPipelines](docs/sdks/v2/README.md#listpipelines) - List pipelines
+* [CreatePipeline](docs/sdks/v2/README.md#createpipeline) - Create pipeline
+* [GetPipelineState](docs/sdks/v2/README.md#getpipelinestate) - Get pipeline state
+* [DeletePipeline](docs/sdks/v2/README.md#deletepipeline) - Delete pipeline
+* [ResetPipeline](docs/sdks/v2/README.md#resetpipeline) - Reset pipeline
+* [StartPipeline](docs/sdks/v2/README.md#startpipeline) - Start pipeline
+* [StopPipeline](docs/sdks/v2/README.md#stoppipeline) - Stop pipeline
diff --git a/pkg/client/docs/models/components/v2createexporterrequest.md b/pkg/client/docs/models/components/v2createexporterrequest.md
new file mode 100644
index 0000000000..d628389a98
--- /dev/null
+++ b/pkg/client/docs/models/components/v2createexporterrequest.md
@@ -0,0 +1,9 @@
+# V2CreateExporterRequest
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| ------------------ | ------------------ | ------------------ | ------------------ |
+| `Driver` | *string* | :heavy_check_mark: | N/A |
+| `Config` | map[string]*any* | :heavy_check_mark: | N/A |
\ No newline at end of file
diff --git a/pkg/client/docs/models/components/v2createexporterresponse.md b/pkg/client/docs/models/components/v2createexporterresponse.md
new file mode 100644
index 0000000000..b7cbf94eee
--- /dev/null
+++ b/pkg/client/docs/models/components/v2createexporterresponse.md
@@ -0,0 +1,10 @@
+# V2CreateExporterResponse
+
+Created exporter
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| -------------------------------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------- |
+| `Data` | [components.V2Exporter](../../models/components/v2exporter.md) | :heavy_check_mark: | N/A |
\ No newline at end of file
diff --git a/pkg/client/docs/models/components/v2createpipelinerequest.md b/pkg/client/docs/models/components/v2createpipelinerequest.md
new file mode 100644
index 0000000000..9361f9f358
--- /dev/null
+++ b/pkg/client/docs/models/components/v2createpipelinerequest.md
@@ -0,0 +1,8 @@
+# V2CreatePipelineRequest
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| ------------------ | ------------------ | ------------------ | ------------------ |
+| `ExporterID` | *string* | :heavy_check_mark: | N/A |
\ No newline at end of file
diff --git a/pkg/client/docs/models/components/v2createpipelineresponse.md b/pkg/client/docs/models/components/v2createpipelineresponse.md
new file mode 100644
index 0000000000..4acd06d7fd
--- /dev/null
+++ b/pkg/client/docs/models/components/v2createpipelineresponse.md
@@ -0,0 +1,10 @@
+# V2CreatePipelineResponse
+
+Created ipeline
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| -------------------------------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------- |
+| `Data` | [components.V2Pipeline](../../models/components/v2pipeline.md) | :heavy_check_mark: | N/A |
\ No newline at end of file
diff --git a/pkg/client/docs/models/components/v2exporter.md b/pkg/client/docs/models/components/v2exporter.md
new file mode 100644
index 0000000000..339d620712
--- /dev/null
+++ b/pkg/client/docs/models/components/v2exporter.md
@@ -0,0 +1,11 @@
+# V2Exporter
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| ----------------------------------------- | ----------------------------------------- | ----------------------------------------- | ----------------------------------------- |
+| `Driver` | *string* | :heavy_check_mark: | N/A |
+| `Config` | map[string]*any* | :heavy_check_mark: | N/A |
+| `ID` | *string* | :heavy_check_mark: | N/A |
+| `CreatedAt` | [time.Time](https://pkg.go.dev/time#Time) | :heavy_check_mark: | N/A |
\ No newline at end of file
diff --git a/pkg/client/docs/models/components/v2getexporterstateresponse.md b/pkg/client/docs/models/components/v2getexporterstateresponse.md
new file mode 100644
index 0000000000..13c2c7bd66
--- /dev/null
+++ b/pkg/client/docs/models/components/v2getexporterstateresponse.md
@@ -0,0 +1,10 @@
+# V2GetExporterStateResponse
+
+Exporter information
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| -------------------------------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------- |
+| `Data` | [components.V2Exporter](../../models/components/v2exporter.md) | :heavy_check_mark: | N/A |
\ No newline at end of file
diff --git a/pkg/client/docs/models/components/v2getpipelinestateresponse.md b/pkg/client/docs/models/components/v2getpipelinestateresponse.md
new file mode 100644
index 0000000000..3145e60945
--- /dev/null
+++ b/pkg/client/docs/models/components/v2getpipelinestateresponse.md
@@ -0,0 +1,10 @@
+# V2GetPipelineStateResponse
+
+Pipeline information
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| -------------------------------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------- |
+| `Data` | [components.V2Pipeline](../../models/components/v2pipeline.md) | :heavy_check_mark: | N/A |
\ No newline at end of file
diff --git a/pkg/client/docs/models/components/v2listexportersresponse.md b/pkg/client/docs/models/components/v2listexportersresponse.md
new file mode 100644
index 0000000000..71304bacf3
--- /dev/null
+++ b/pkg/client/docs/models/components/v2listexportersresponse.md
@@ -0,0 +1,10 @@
+# V2ListExportersResponse
+
+Exporters list
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
+| `Cursor` | [*components.V2ListExportersResponseCursor](../../models/components/v2listexportersresponsecursor.md) | :heavy_minus_sign: | N/A |
\ No newline at end of file
diff --git a/pkg/client/docs/models/components/v2listexportersresponsecursor.md b/pkg/client/docs/models/components/v2listexportersresponsecursor.md
new file mode 100644
index 0000000000..82b64ecd65
--- /dev/null
+++ b/pkg/client/docs/models/components/v2listexportersresponsecursor.md
@@ -0,0 +1,9 @@
+# V2ListExportersResponseCursor
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
+| `Cursor` | [components.V2ListExportersResponseCursorCursor](../../models/components/v2listexportersresponsecursorcursor.md) | :heavy_check_mark: | N/A |
+| `Data` | [][components.V2Exporter](../../models/components/v2exporter.md) | :heavy_minus_sign: | N/A |
\ No newline at end of file
diff --git a/pkg/client/docs/models/components/v2listexportersresponsecursorcursor.md b/pkg/client/docs/models/components/v2listexportersresponsecursorcursor.md
new file mode 100644
index 0000000000..180cf4a10c
--- /dev/null
+++ b/pkg/client/docs/models/components/v2listexportersresponsecursorcursor.md
@@ -0,0 +1,12 @@
+# V2ListExportersResponseCursorCursor
+
+
+## Fields
+
+| Field | Type | Required | Description | Example |
+| ---------------------------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------- |
+| `PageSize` | *int64* | :heavy_check_mark: | N/A | 15 |
+| `HasMore` | *bool* | :heavy_check_mark: | N/A | false |
+| `Previous` | **string* | :heavy_minus_sign: | N/A | YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol= |
+| `Next` | **string* | :heavy_minus_sign: | N/A | |
+| `Data` | [][components.V2Exporter](../../models/components/v2exporter.md) | :heavy_check_mark: | N/A | |
\ No newline at end of file
diff --git a/pkg/client/docs/models/components/v2listpipelinesresponse.md b/pkg/client/docs/models/components/v2listpipelinesresponse.md
new file mode 100644
index 0000000000..9a80b81810
--- /dev/null
+++ b/pkg/client/docs/models/components/v2listpipelinesresponse.md
@@ -0,0 +1,10 @@
+# V2ListPipelinesResponse
+
+Pipelines list
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
+| `Cursor` | [*components.V2ListPipelinesResponseCursor](../../models/components/v2listpipelinesresponsecursor.md) | :heavy_minus_sign: | N/A |
\ No newline at end of file
diff --git a/pkg/client/docs/models/components/v2listpipelinesresponsecursor.md b/pkg/client/docs/models/components/v2listpipelinesresponsecursor.md
new file mode 100644
index 0000000000..9f78929c04
--- /dev/null
+++ b/pkg/client/docs/models/components/v2listpipelinesresponsecursor.md
@@ -0,0 +1,9 @@
+# V2ListPipelinesResponseCursor
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
+| `Cursor` | [components.V2ListPipelinesResponseCursorCursor](../../models/components/v2listpipelinesresponsecursorcursor.md) | :heavy_check_mark: | N/A |
+| `Data` | [][components.V2Pipeline](../../models/components/v2pipeline.md) | :heavy_minus_sign: | N/A |
\ No newline at end of file
diff --git a/pkg/client/docs/models/components/v2listpipelinesresponsecursorcursor.md b/pkg/client/docs/models/components/v2listpipelinesresponsecursorcursor.md
new file mode 100644
index 0000000000..098a29543d
--- /dev/null
+++ b/pkg/client/docs/models/components/v2listpipelinesresponsecursorcursor.md
@@ -0,0 +1,12 @@
+# V2ListPipelinesResponseCursorCursor
+
+
+## Fields
+
+| Field | Type | Required | Description | Example |
+| ---------------------------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------- |
+| `PageSize` | *int64* | :heavy_check_mark: | N/A | 15 |
+| `HasMore` | *bool* | :heavy_check_mark: | N/A | false |
+| `Previous` | **string* | :heavy_minus_sign: | N/A | YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol= |
+| `Next` | **string* | :heavy_minus_sign: | N/A | |
+| `Data` | [][components.V2Pipeline](../../models/components/v2pipeline.md) | :heavy_check_mark: | N/A | |
\ No newline at end of file
diff --git a/pkg/client/docs/models/components/v2pipeline.md b/pkg/client/docs/models/components/v2pipeline.md
new file mode 100644
index 0000000000..5efa802388
--- /dev/null
+++ b/pkg/client/docs/models/components/v2pipeline.md
@@ -0,0 +1,13 @@
+# V2Pipeline
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| ----------------------------------------- | ----------------------------------------- | ----------------------------------------- | ----------------------------------------- |
+| `Ledger` | *string* | :heavy_check_mark: | N/A |
+| `ExporterID` | *string* | :heavy_check_mark: | N/A |
+| `ID` | *string* | :heavy_check_mark: | N/A |
+| `CreatedAt` | [time.Time](https://pkg.go.dev/time#Time) | :heavy_check_mark: | N/A |
+| `LastLogID` | **int64* | :heavy_minus_sign: | N/A |
+| `Enabled` | **bool* | :heavy_minus_sign: | N/A |
\ No newline at end of file
diff --git a/pkg/client/docs/models/operations/v2createexporterresponse.md b/pkg/client/docs/models/operations/v2createexporterresponse.md
new file mode 100644
index 0000000000..ad0e10e7bd
--- /dev/null
+++ b/pkg/client/docs/models/operations/v2createexporterresponse.md
@@ -0,0 +1,9 @@
+# V2CreateExporterResponse
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
+| `HTTPMeta` | [components.HTTPMetadata](../../models/components/httpmetadata.md) | :heavy_check_mark: | N/A |
+| `V2CreateExporterResponse` | [*components.V2CreateExporterResponse](../../models/components/v2createexporterresponse.md) | :heavy_minus_sign: | Created exporter |
\ No newline at end of file
diff --git a/pkg/client/docs/models/operations/v2createpipelinerequest.md b/pkg/client/docs/models/operations/v2createpipelinerequest.md
new file mode 100644
index 0000000000..69b99a8c93
--- /dev/null
+++ b/pkg/client/docs/models/operations/v2createpipelinerequest.md
@@ -0,0 +1,9 @@
+# V2CreatePipelineRequest
+
+
+## Fields
+
+| Field | Type | Required | Description | Example |
+| ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
+| `Ledger` | *string* | :heavy_check_mark: | Name of the ledger. | ledger001 |
+| `V2CreatePipelineRequest` | [*components.V2CreatePipelineRequest](../../models/components/v2createpipelinerequest.md) | :heavy_minus_sign: | N/A | |
\ No newline at end of file
diff --git a/pkg/client/docs/models/operations/v2createpipelineresponse.md b/pkg/client/docs/models/operations/v2createpipelineresponse.md
new file mode 100644
index 0000000000..4b4a255ed7
--- /dev/null
+++ b/pkg/client/docs/models/operations/v2createpipelineresponse.md
@@ -0,0 +1,9 @@
+# V2CreatePipelineResponse
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
+| `HTTPMeta` | [components.HTTPMetadata](../../models/components/httpmetadata.md) | :heavy_check_mark: | N/A |
+| `V2CreatePipelineResponse` | [*components.V2CreatePipelineResponse](../../models/components/v2createpipelineresponse.md) | :heavy_minus_sign: | Created ipeline |
\ No newline at end of file
diff --git a/pkg/client/docs/models/operations/v2deleteexporterrequest.md b/pkg/client/docs/models/operations/v2deleteexporterrequest.md
new file mode 100644
index 0000000000..c0bfc92819
--- /dev/null
+++ b/pkg/client/docs/models/operations/v2deleteexporterrequest.md
@@ -0,0 +1,8 @@
+# V2DeleteExporterRequest
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| ------------------ | ------------------ | ------------------ | ------------------ |
+| `ExporterID` | *string* | :heavy_check_mark: | The exporter id |
\ No newline at end of file
diff --git a/pkg/client/docs/models/operations/v2deleteexporterresponse.md b/pkg/client/docs/models/operations/v2deleteexporterresponse.md
new file mode 100644
index 0000000000..c6b98598ea
--- /dev/null
+++ b/pkg/client/docs/models/operations/v2deleteexporterresponse.md
@@ -0,0 +1,8 @@
+# V2DeleteExporterResponse
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ |
+| `HTTPMeta` | [components.HTTPMetadata](../../models/components/httpmetadata.md) | :heavy_check_mark: | N/A |
\ No newline at end of file
diff --git a/pkg/client/docs/models/operations/v2deletepipelinerequest.md b/pkg/client/docs/models/operations/v2deletepipelinerequest.md
new file mode 100644
index 0000000000..a12ba74e4e
--- /dev/null
+++ b/pkg/client/docs/models/operations/v2deletepipelinerequest.md
@@ -0,0 +1,9 @@
+# V2DeletePipelineRequest
+
+
+## Fields
+
+| Field | Type | Required | Description | Example |
+| ------------------- | ------------------- | ------------------- | ------------------- | ------------------- |
+| `Ledger` | *string* | :heavy_check_mark: | Name of the ledger. | ledger001 |
+| `PipelineID` | *string* | :heavy_check_mark: | The pipeline id | |
\ No newline at end of file
diff --git a/pkg/client/docs/models/operations/v2deletepipelineresponse.md b/pkg/client/docs/models/operations/v2deletepipelineresponse.md
new file mode 100644
index 0000000000..886d785f47
--- /dev/null
+++ b/pkg/client/docs/models/operations/v2deletepipelineresponse.md
@@ -0,0 +1,8 @@
+# V2DeletePipelineResponse
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ |
+| `HTTPMeta` | [components.HTTPMetadata](../../models/components/httpmetadata.md) | :heavy_check_mark: | N/A |
\ No newline at end of file
diff --git a/pkg/client/docs/models/operations/v2getexporterstaterequest.md b/pkg/client/docs/models/operations/v2getexporterstaterequest.md
new file mode 100644
index 0000000000..807a48a7f4
--- /dev/null
+++ b/pkg/client/docs/models/operations/v2getexporterstaterequest.md
@@ -0,0 +1,8 @@
+# V2GetExporterStateRequest
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| ------------------ | ------------------ | ------------------ | ------------------ |
+| `ExporterID` | *string* | :heavy_check_mark: | The exporter id |
\ No newline at end of file
diff --git a/pkg/client/docs/models/operations/v2getexporterstateresponse.md b/pkg/client/docs/models/operations/v2getexporterstateresponse.md
new file mode 100644
index 0000000000..888487ccf0
--- /dev/null
+++ b/pkg/client/docs/models/operations/v2getexporterstateresponse.md
@@ -0,0 +1,9 @@
+# V2GetExporterStateResponse
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
+| `HTTPMeta` | [components.HTTPMetadata](../../models/components/httpmetadata.md) | :heavy_check_mark: | N/A |
+| `V2GetExporterStateResponse` | [*components.V2GetExporterStateResponse](../../models/components/v2getexporterstateresponse.md) | :heavy_minus_sign: | Exporter information |
\ No newline at end of file
diff --git a/pkg/client/docs/models/operations/v2getpipelinestaterequest.md b/pkg/client/docs/models/operations/v2getpipelinestaterequest.md
new file mode 100644
index 0000000000..1a0c1dfb69
--- /dev/null
+++ b/pkg/client/docs/models/operations/v2getpipelinestaterequest.md
@@ -0,0 +1,9 @@
+# V2GetPipelineStateRequest
+
+
+## Fields
+
+| Field | Type | Required | Description | Example |
+| ------------------- | ------------------- | ------------------- | ------------------- | ------------------- |
+| `Ledger` | *string* | :heavy_check_mark: | Name of the ledger. | ledger001 |
+| `PipelineID` | *string* | :heavy_check_mark: | The pipeline id | |
\ No newline at end of file
diff --git a/pkg/client/docs/models/operations/v2getpipelinestateresponse.md b/pkg/client/docs/models/operations/v2getpipelinestateresponse.md
new file mode 100644
index 0000000000..50d57cc7bb
--- /dev/null
+++ b/pkg/client/docs/models/operations/v2getpipelinestateresponse.md
@@ -0,0 +1,9 @@
+# V2GetPipelineStateResponse
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
+| `HTTPMeta` | [components.HTTPMetadata](../../models/components/httpmetadata.md) | :heavy_check_mark: | N/A |
+| `V2GetPipelineStateResponse` | [*components.V2GetPipelineStateResponse](../../models/components/v2getpipelinestateresponse.md) | :heavy_minus_sign: | Pipeline information |
\ No newline at end of file
diff --git a/pkg/client/docs/models/operations/v2listexportersresponse.md b/pkg/client/docs/models/operations/v2listexportersresponse.md
new file mode 100644
index 0000000000..647aa48d6d
--- /dev/null
+++ b/pkg/client/docs/models/operations/v2listexportersresponse.md
@@ -0,0 +1,9 @@
+# V2ListExportersResponse
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
+| `HTTPMeta` | [components.HTTPMetadata](../../models/components/httpmetadata.md) | :heavy_check_mark: | N/A |
+| `V2ListExportersResponse` | [*components.V2ListExportersResponse](../../models/components/v2listexportersresponse.md) | :heavy_minus_sign: | Exporters list |
\ No newline at end of file
diff --git a/pkg/client/docs/models/operations/v2listpipelinesrequest.md b/pkg/client/docs/models/operations/v2listpipelinesrequest.md
new file mode 100644
index 0000000000..baa6e9bbe6
--- /dev/null
+++ b/pkg/client/docs/models/operations/v2listpipelinesrequest.md
@@ -0,0 +1,8 @@
+# V2ListPipelinesRequest
+
+
+## Fields
+
+| Field | Type | Required | Description | Example |
+| ------------------- | ------------------- | ------------------- | ------------------- | ------------------- |
+| `Ledger` | *string* | :heavy_check_mark: | Name of the ledger. | ledger001 |
\ No newline at end of file
diff --git a/pkg/client/docs/models/operations/v2listpipelinesresponse.md b/pkg/client/docs/models/operations/v2listpipelinesresponse.md
new file mode 100644
index 0000000000..6588f8f0f7
--- /dev/null
+++ b/pkg/client/docs/models/operations/v2listpipelinesresponse.md
@@ -0,0 +1,9 @@
+# V2ListPipelinesResponse
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
+| `HTTPMeta` | [components.HTTPMetadata](../../models/components/httpmetadata.md) | :heavy_check_mark: | N/A |
+| `V2ListPipelinesResponse` | [*components.V2ListPipelinesResponse](../../models/components/v2listpipelinesresponse.md) | :heavy_minus_sign: | Pipelines list |
\ No newline at end of file
diff --git a/pkg/client/docs/models/operations/v2resetpipelinerequest.md b/pkg/client/docs/models/operations/v2resetpipelinerequest.md
new file mode 100644
index 0000000000..3a57f48d40
--- /dev/null
+++ b/pkg/client/docs/models/operations/v2resetpipelinerequest.md
@@ -0,0 +1,9 @@
+# V2ResetPipelineRequest
+
+
+## Fields
+
+| Field | Type | Required | Description | Example |
+| ------------------- | ------------------- | ------------------- | ------------------- | ------------------- |
+| `Ledger` | *string* | :heavy_check_mark: | Name of the ledger. | ledger001 |
+| `PipelineID` | *string* | :heavy_check_mark: | The pipeline id | |
\ No newline at end of file
diff --git a/pkg/client/docs/models/operations/v2resetpipelineresponse.md b/pkg/client/docs/models/operations/v2resetpipelineresponse.md
new file mode 100644
index 0000000000..39d09daed7
--- /dev/null
+++ b/pkg/client/docs/models/operations/v2resetpipelineresponse.md
@@ -0,0 +1,8 @@
+# V2ResetPipelineResponse
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ |
+| `HTTPMeta` | [components.HTTPMetadata](../../models/components/httpmetadata.md) | :heavy_check_mark: | N/A |
\ No newline at end of file
diff --git a/pkg/client/docs/models/operations/v2startpipelinerequest.md b/pkg/client/docs/models/operations/v2startpipelinerequest.md
new file mode 100644
index 0000000000..ac0fd9669e
--- /dev/null
+++ b/pkg/client/docs/models/operations/v2startpipelinerequest.md
@@ -0,0 +1,9 @@
+# V2StartPipelineRequest
+
+
+## Fields
+
+| Field | Type | Required | Description | Example |
+| ------------------- | ------------------- | ------------------- | ------------------- | ------------------- |
+| `Ledger` | *string* | :heavy_check_mark: | Name of the ledger. | ledger001 |
+| `PipelineID` | *string* | :heavy_check_mark: | The pipeline id | |
\ No newline at end of file
diff --git a/pkg/client/docs/models/operations/v2startpipelineresponse.md b/pkg/client/docs/models/operations/v2startpipelineresponse.md
new file mode 100644
index 0000000000..47b3c5353b
--- /dev/null
+++ b/pkg/client/docs/models/operations/v2startpipelineresponse.md
@@ -0,0 +1,8 @@
+# V2StartPipelineResponse
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ |
+| `HTTPMeta` | [components.HTTPMetadata](../../models/components/httpmetadata.md) | :heavy_check_mark: | N/A |
\ No newline at end of file
diff --git a/pkg/client/docs/models/operations/v2stoppipelinerequest.md b/pkg/client/docs/models/operations/v2stoppipelinerequest.md
new file mode 100644
index 0000000000..782586a26e
--- /dev/null
+++ b/pkg/client/docs/models/operations/v2stoppipelinerequest.md
@@ -0,0 +1,9 @@
+# V2StopPipelineRequest
+
+
+## Fields
+
+| Field | Type | Required | Description | Example |
+| ------------------- | ------------------- | ------------------- | ------------------- | ------------------- |
+| `Ledger` | *string* | :heavy_check_mark: | Name of the ledger. | ledger001 |
+| `PipelineID` | *string* | :heavy_check_mark: | The pipeline id | |
\ No newline at end of file
diff --git a/pkg/client/docs/models/operations/v2stoppipelineresponse.md b/pkg/client/docs/models/operations/v2stoppipelineresponse.md
new file mode 100644
index 0000000000..e6b91c6018
--- /dev/null
+++ b/pkg/client/docs/models/operations/v2stoppipelineresponse.md
@@ -0,0 +1,8 @@
+# V2StopPipelineResponse
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ |
+| `HTTPMeta` | [components.HTTPMetadata](../../models/components/httpmetadata.md) | :heavy_check_mark: | N/A |
\ No newline at end of file
diff --git a/pkg/client/docs/sdks/v2/README.md b/pkg/client/docs/sdks/v2/README.md
index ceb9601bdd..10bdab6675 100644
--- a/pkg/client/docs/sdks/v2/README.md
+++ b/pkg/client/docs/sdks/v2/README.md
@@ -30,6 +30,17 @@
* [ListLogs](#listlogs) - List the logs from a ledger
* [ImportLogs](#importlogs)
* [ExportLogs](#exportlogs) - Export logs
+* [ListExporters](#listexporters) - List exporters
+* [CreateExporter](#createexporter) - Create exporter
+* [GetExporterState](#getexporterstate) - Get exporter state
+* [DeleteExporter](#deleteexporter) - Delete exporter
+* [ListPipelines](#listpipelines) - List pipelines
+* [CreatePipeline](#createpipeline) - Create pipeline
+* [GetPipelineState](#getpipelinestate) - Get pipeline state
+* [DeletePipeline](#deletepipeline) - Delete pipeline
+* [ResetPipeline](#resetpipeline) - Reset pipeline
+* [StartPipeline](#startpipeline) - Start pipeline
+* [StopPipeline](#stoppipeline) - Stop pipeline
## ListLedgers
@@ -1630,4 +1641,657 @@ func main() {
| Error Type | Status Code | Content Type |
| ------------------ | ------------------ | ------------------ |
-| sdkerrors.SDKError | 4XX, 5XX | \*/\* |
\ No newline at end of file
+| sdkerrors.SDKError | 4XX, 5XX | \*/\* |
+
+## ListExporters
+
+List exporters
+
+### Example Usage
+
+```go
+package main
+
+import(
+ "context"
+ "os"
+ "github.com/formancehq/ledger/pkg/client/models/components"
+ "github.com/formancehq/ledger/pkg/client"
+ "log"
+)
+
+func main() {
+ ctx := context.Background()
+
+ s := client.New(
+ client.WithSecurity(components.Security{
+ ClientID: client.String(os.Getenv("FORMANCE_CLIENT_ID")),
+ ClientSecret: client.String(os.Getenv("FORMANCE_CLIENT_SECRET")),
+ }),
+ )
+
+ res, err := s.Ledger.V2.ListExporters(ctx)
+ if err != nil {
+ log.Fatal(err)
+ }
+ if res.V2ListExportersResponse != 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.V2ListExportersResponse](../../models/operations/v2listexportersresponse.md), error**
+
+### Errors
+
+| Error Type | Status Code | Content Type |
+| ------------------------- | ------------------------- | ------------------------- |
+| sdkerrors.V2ErrorResponse | default | application/json |
+| sdkerrors.SDKError | 4XX, 5XX | \*/\* |
+
+## CreateExporter
+
+Create exporter
+
+### Example Usage
+
+```go
+package main
+
+import(
+ "context"
+ "os"
+ "github.com/formancehq/ledger/pkg/client/models/components"
+ "github.com/formancehq/ledger/pkg/client"
+ "log"
+)
+
+func main() {
+ ctx := context.Background()
+
+ s := client.New(
+ client.WithSecurity(components.Security{
+ ClientID: client.String(os.Getenv("FORMANCE_CLIENT_ID")),
+ ClientSecret: client.String(os.Getenv("FORMANCE_CLIENT_SECRET")),
+ }),
+ )
+
+ res, err := s.Ledger.V2.CreateExporter(ctx, components.V2CreateExporterRequest{
+ Driver: "",
+ Config: map[string]any{
+ "key": "",
+ "key1": "",
+ },
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+ if res.V2CreateExporterResponse != nil {
+ // handle response
+ }
+}
+```
+
+### Parameters
+
+| Parameter | Type | Required | Description |
+| ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
+| `ctx` | [context.Context](https://pkg.go.dev/context#Context) | :heavy_check_mark: | The context to use for the request. |
+| `request` | [components.V2CreateExporterRequest](../../models/components/v2createexporterrequest.md) | :heavy_check_mark: | The request object to use for the request. |
+| `opts` | [][operations.Option](../../models/operations/option.md) | :heavy_minus_sign: | The options for this request. |
+
+### Response
+
+**[*operations.V2CreateExporterResponse](../../models/operations/v2createexporterresponse.md), error**
+
+### Errors
+
+| Error Type | Status Code | Content Type |
+| ------------------------- | ------------------------- | ------------------------- |
+| sdkerrors.V2ErrorResponse | default | application/json |
+| sdkerrors.SDKError | 4XX, 5XX | \*/\* |
+
+## GetExporterState
+
+Get exporter state
+
+### Example Usage
+
+```go
+package main
+
+import(
+ "context"
+ "os"
+ "github.com/formancehq/ledger/pkg/client/models/components"
+ "github.com/formancehq/ledger/pkg/client"
+ "github.com/formancehq/ledger/pkg/client/models/operations"
+ "log"
+)
+
+func main() {
+ ctx := context.Background()
+
+ s := client.New(
+ client.WithSecurity(components.Security{
+ ClientID: client.String(os.Getenv("FORMANCE_CLIENT_ID")),
+ ClientSecret: client.String(os.Getenv("FORMANCE_CLIENT_SECRET")),
+ }),
+ )
+
+ res, err := s.Ledger.V2.GetExporterState(ctx, operations.V2GetExporterStateRequest{
+ ExporterID: "",
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+ if res.V2GetExporterStateResponse != nil {
+ // handle response
+ }
+}
+```
+
+### Parameters
+
+| Parameter | Type | Required | Description |
+| -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
+| `ctx` | [context.Context](https://pkg.go.dev/context#Context) | :heavy_check_mark: | The context to use for the request. |
+| `request` | [operations.V2GetExporterStateRequest](../../models/operations/v2getexporterstaterequest.md) | :heavy_check_mark: | The request object to use for the request. |
+| `opts` | [][operations.Option](../../models/operations/option.md) | :heavy_minus_sign: | The options for this request. |
+
+### Response
+
+**[*operations.V2GetExporterStateResponse](../../models/operations/v2getexporterstateresponse.md), error**
+
+### Errors
+
+| Error Type | Status Code | Content Type |
+| ------------------------- | ------------------------- | ------------------------- |
+| sdkerrors.V2ErrorResponse | default | application/json |
+| sdkerrors.SDKError | 4XX, 5XX | \*/\* |
+
+## DeleteExporter
+
+Delete exporter
+
+### Example Usage
+
+```go
+package main
+
+import(
+ "context"
+ "os"
+ "github.com/formancehq/ledger/pkg/client/models/components"
+ "github.com/formancehq/ledger/pkg/client"
+ "github.com/formancehq/ledger/pkg/client/models/operations"
+ "log"
+)
+
+func main() {
+ ctx := context.Background()
+
+ s := client.New(
+ client.WithSecurity(components.Security{
+ ClientID: client.String(os.Getenv("FORMANCE_CLIENT_ID")),
+ ClientSecret: client.String(os.Getenv("FORMANCE_CLIENT_SECRET")),
+ }),
+ )
+
+ res, err := s.Ledger.V2.DeleteExporter(ctx, operations.V2DeleteExporterRequest{
+ ExporterID: "",
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+ if res != nil {
+ // handle response
+ }
+}
+```
+
+### Parameters
+
+| Parameter | Type | Required | Description |
+| ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
+| `ctx` | [context.Context](https://pkg.go.dev/context#Context) | :heavy_check_mark: | The context to use for the request. |
+| `request` | [operations.V2DeleteExporterRequest](../../models/operations/v2deleteexporterrequest.md) | :heavy_check_mark: | The request object to use for the request. |
+| `opts` | [][operations.Option](../../models/operations/option.md) | :heavy_minus_sign: | The options for this request. |
+
+### Response
+
+**[*operations.V2DeleteExporterResponse](../../models/operations/v2deleteexporterresponse.md), error**
+
+### Errors
+
+| Error Type | Status Code | Content Type |
+| ------------------------- | ------------------------- | ------------------------- |
+| sdkerrors.V2ErrorResponse | default | application/json |
+| sdkerrors.SDKError | 4XX, 5XX | \*/\* |
+
+## ListPipelines
+
+List pipelines
+
+### Example Usage
+
+```go
+package main
+
+import(
+ "context"
+ "os"
+ "github.com/formancehq/ledger/pkg/client/models/components"
+ "github.com/formancehq/ledger/pkg/client"
+ "github.com/formancehq/ledger/pkg/client/models/operations"
+ "log"
+)
+
+func main() {
+ ctx := context.Background()
+
+ s := client.New(
+ client.WithSecurity(components.Security{
+ ClientID: client.String(os.Getenv("FORMANCE_CLIENT_ID")),
+ ClientSecret: client.String(os.Getenv("FORMANCE_CLIENT_SECRET")),
+ }),
+ )
+
+ res, err := s.Ledger.V2.ListPipelines(ctx, operations.V2ListPipelinesRequest{
+ Ledger: "ledger001",
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+ if res.V2ListPipelinesResponse != nil {
+ // handle response
+ }
+}
+```
+
+### Parameters
+
+| Parameter | Type | Required | Description |
+| -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
+| `ctx` | [context.Context](https://pkg.go.dev/context#Context) | :heavy_check_mark: | The context to use for the request. |
+| `request` | [operations.V2ListPipelinesRequest](../../models/operations/v2listpipelinesrequest.md) | :heavy_check_mark: | The request object to use for the request. |
+| `opts` | [][operations.Option](../../models/operations/option.md) | :heavy_minus_sign: | The options for this request. |
+
+### Response
+
+**[*operations.V2ListPipelinesResponse](../../models/operations/v2listpipelinesresponse.md), error**
+
+### Errors
+
+| Error Type | Status Code | Content Type |
+| ------------------------- | ------------------------- | ------------------------- |
+| sdkerrors.V2ErrorResponse | default | application/json |
+| sdkerrors.SDKError | 4XX, 5XX | \*/\* |
+
+## CreatePipeline
+
+Create pipeline
+
+### Example Usage
+
+```go
+package main
+
+import(
+ "context"
+ "os"
+ "github.com/formancehq/ledger/pkg/client/models/components"
+ "github.com/formancehq/ledger/pkg/client"
+ "github.com/formancehq/ledger/pkg/client/models/operations"
+ "log"
+)
+
+func main() {
+ ctx := context.Background()
+
+ s := client.New(
+ client.WithSecurity(components.Security{
+ ClientID: client.String(os.Getenv("FORMANCE_CLIENT_ID")),
+ ClientSecret: client.String(os.Getenv("FORMANCE_CLIENT_SECRET")),
+ }),
+ )
+
+ res, err := s.Ledger.V2.CreatePipeline(ctx, operations.V2CreatePipelineRequest{
+ Ledger: "ledger001",
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+ if res.V2CreatePipelineResponse != nil {
+ // handle response
+ }
+}
+```
+
+### Parameters
+
+| Parameter | Type | Required | Description |
+| ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
+| `ctx` | [context.Context](https://pkg.go.dev/context#Context) | :heavy_check_mark: | The context to use for the request. |
+| `request` | [operations.V2CreatePipelineRequest](../../models/operations/v2createpipelinerequest.md) | :heavy_check_mark: | The request object to use for the request. |
+| `opts` | [][operations.Option](../../models/operations/option.md) | :heavy_minus_sign: | The options for this request. |
+
+### Response
+
+**[*operations.V2CreatePipelineResponse](../../models/operations/v2createpipelineresponse.md), error**
+
+### Errors
+
+| Error Type | Status Code | Content Type |
+| ------------------------- | ------------------------- | ------------------------- |
+| sdkerrors.V2ErrorResponse | default | application/json |
+| sdkerrors.SDKError | 4XX, 5XX | \*/\* |
+
+## GetPipelineState
+
+Get pipeline state
+
+### Example Usage
+
+```go
+package main
+
+import(
+ "context"
+ "os"
+ "github.com/formancehq/ledger/pkg/client/models/components"
+ "github.com/formancehq/ledger/pkg/client"
+ "github.com/formancehq/ledger/pkg/client/models/operations"
+ "log"
+)
+
+func main() {
+ ctx := context.Background()
+
+ s := client.New(
+ client.WithSecurity(components.Security{
+ ClientID: client.String(os.Getenv("FORMANCE_CLIENT_ID")),
+ ClientSecret: client.String(os.Getenv("FORMANCE_CLIENT_SECRET")),
+ }),
+ )
+
+ res, err := s.Ledger.V2.GetPipelineState(ctx, operations.V2GetPipelineStateRequest{
+ Ledger: "ledger001",
+ PipelineID: "",
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+ if res.V2GetPipelineStateResponse != nil {
+ // handle response
+ }
+}
+```
+
+### Parameters
+
+| Parameter | Type | Required | Description |
+| -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
+| `ctx` | [context.Context](https://pkg.go.dev/context#Context) | :heavy_check_mark: | The context to use for the request. |
+| `request` | [operations.V2GetPipelineStateRequest](../../models/operations/v2getpipelinestaterequest.md) | :heavy_check_mark: | The request object to use for the request. |
+| `opts` | [][operations.Option](../../models/operations/option.md) | :heavy_minus_sign: | The options for this request. |
+
+### Response
+
+**[*operations.V2GetPipelineStateResponse](../../models/operations/v2getpipelinestateresponse.md), error**
+
+### Errors
+
+| Error Type | Status Code | Content Type |
+| ------------------------- | ------------------------- | ------------------------- |
+| sdkerrors.V2ErrorResponse | default | application/json |
+| sdkerrors.SDKError | 4XX, 5XX | \*/\* |
+
+## DeletePipeline
+
+Delete pipeline
+
+### Example Usage
+
+```go
+package main
+
+import(
+ "context"
+ "os"
+ "github.com/formancehq/ledger/pkg/client/models/components"
+ "github.com/formancehq/ledger/pkg/client"
+ "github.com/formancehq/ledger/pkg/client/models/operations"
+ "log"
+)
+
+func main() {
+ ctx := context.Background()
+
+ s := client.New(
+ client.WithSecurity(components.Security{
+ ClientID: client.String(os.Getenv("FORMANCE_CLIENT_ID")),
+ ClientSecret: client.String(os.Getenv("FORMANCE_CLIENT_SECRET")),
+ }),
+ )
+
+ res, err := s.Ledger.V2.DeletePipeline(ctx, operations.V2DeletePipelineRequest{
+ Ledger: "ledger001",
+ PipelineID: "",
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+ if res != nil {
+ // handle response
+ }
+}
+```
+
+### Parameters
+
+| Parameter | Type | Required | Description |
+| ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
+| `ctx` | [context.Context](https://pkg.go.dev/context#Context) | :heavy_check_mark: | The context to use for the request. |
+| `request` | [operations.V2DeletePipelineRequest](../../models/operations/v2deletepipelinerequest.md) | :heavy_check_mark: | The request object to use for the request. |
+| `opts` | [][operations.Option](../../models/operations/option.md) | :heavy_minus_sign: | The options for this request. |
+
+### Response
+
+**[*operations.V2DeletePipelineResponse](../../models/operations/v2deletepipelineresponse.md), error**
+
+### Errors
+
+| Error Type | Status Code | Content Type |
+| ------------------------- | ------------------------- | ------------------------- |
+| sdkerrors.V2ErrorResponse | default | application/json |
+| sdkerrors.SDKError | 4XX, 5XX | \*/\* |
+
+## ResetPipeline
+
+Reset pipeline
+
+### Example Usage
+
+```go
+package main
+
+import(
+ "context"
+ "os"
+ "github.com/formancehq/ledger/pkg/client/models/components"
+ "github.com/formancehq/ledger/pkg/client"
+ "github.com/formancehq/ledger/pkg/client/models/operations"
+ "log"
+)
+
+func main() {
+ ctx := context.Background()
+
+ s := client.New(
+ client.WithSecurity(components.Security{
+ ClientID: client.String(os.Getenv("FORMANCE_CLIENT_ID")),
+ ClientSecret: client.String(os.Getenv("FORMANCE_CLIENT_SECRET")),
+ }),
+ )
+
+ res, err := s.Ledger.V2.ResetPipeline(ctx, operations.V2ResetPipelineRequest{
+ Ledger: "ledger001",
+ PipelineID: "",
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+ if res != nil {
+ // handle response
+ }
+}
+```
+
+### Parameters
+
+| Parameter | Type | Required | Description |
+| -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
+| `ctx` | [context.Context](https://pkg.go.dev/context#Context) | :heavy_check_mark: | The context to use for the request. |
+| `request` | [operations.V2ResetPipelineRequest](../../models/operations/v2resetpipelinerequest.md) | :heavy_check_mark: | The request object to use for the request. |
+| `opts` | [][operations.Option](../../models/operations/option.md) | :heavy_minus_sign: | The options for this request. |
+
+### Response
+
+**[*operations.V2ResetPipelineResponse](../../models/operations/v2resetpipelineresponse.md), error**
+
+### Errors
+
+| Error Type | Status Code | Content Type |
+| ------------------------- | ------------------------- | ------------------------- |
+| sdkerrors.V2ErrorResponse | default | application/json |
+| sdkerrors.SDKError | 4XX, 5XX | \*/\* |
+
+## StartPipeline
+
+Start pipeline
+
+### Example Usage
+
+```go
+package main
+
+import(
+ "context"
+ "os"
+ "github.com/formancehq/ledger/pkg/client/models/components"
+ "github.com/formancehq/ledger/pkg/client"
+ "github.com/formancehq/ledger/pkg/client/models/operations"
+ "log"
+)
+
+func main() {
+ ctx := context.Background()
+
+ s := client.New(
+ client.WithSecurity(components.Security{
+ ClientID: client.String(os.Getenv("FORMANCE_CLIENT_ID")),
+ ClientSecret: client.String(os.Getenv("FORMANCE_CLIENT_SECRET")),
+ }),
+ )
+
+ res, err := s.Ledger.V2.StartPipeline(ctx, operations.V2StartPipelineRequest{
+ Ledger: "ledger001",
+ PipelineID: "",
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+ if res != nil {
+ // handle response
+ }
+}
+```
+
+### Parameters
+
+| Parameter | Type | Required | Description |
+| -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
+| `ctx` | [context.Context](https://pkg.go.dev/context#Context) | :heavy_check_mark: | The context to use for the request. |
+| `request` | [operations.V2StartPipelineRequest](../../models/operations/v2startpipelinerequest.md) | :heavy_check_mark: | The request object to use for the request. |
+| `opts` | [][operations.Option](../../models/operations/option.md) | :heavy_minus_sign: | The options for this request. |
+
+### Response
+
+**[*operations.V2StartPipelineResponse](../../models/operations/v2startpipelineresponse.md), error**
+
+### Errors
+
+| Error Type | Status Code | Content Type |
+| ------------------------- | ------------------------- | ------------------------- |
+| sdkerrors.V2ErrorResponse | default | application/json |
+| sdkerrors.SDKError | 4XX, 5XX | \*/\* |
+
+## StopPipeline
+
+Stop pipeline
+
+### Example Usage
+
+```go
+package main
+
+import(
+ "context"
+ "os"
+ "github.com/formancehq/ledger/pkg/client/models/components"
+ "github.com/formancehq/ledger/pkg/client"
+ "github.com/formancehq/ledger/pkg/client/models/operations"
+ "log"
+)
+
+func main() {
+ ctx := context.Background()
+
+ s := client.New(
+ client.WithSecurity(components.Security{
+ ClientID: client.String(os.Getenv("FORMANCE_CLIENT_ID")),
+ ClientSecret: client.String(os.Getenv("FORMANCE_CLIENT_SECRET")),
+ }),
+ )
+
+ res, err := s.Ledger.V2.StopPipeline(ctx, operations.V2StopPipelineRequest{
+ Ledger: "ledger001",
+ PipelineID: "",
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+ if res != nil {
+ // handle response
+ }
+}
+```
+
+### Parameters
+
+| Parameter | Type | Required | Description |
+| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ |
+| `ctx` | [context.Context](https://pkg.go.dev/context#Context) | :heavy_check_mark: | The context to use for the request. |
+| `request` | [operations.V2StopPipelineRequest](../../models/operations/v2stoppipelinerequest.md) | :heavy_check_mark: | The request object to use for the request. |
+| `opts` | [][operations.Option](../../models/operations/option.md) | :heavy_minus_sign: | The options for this request. |
+
+### Response
+
+**[*operations.V2StopPipelineResponse](../../models/operations/v2stoppipelineresponse.md), error**
+
+### Errors
+
+| Error Type | Status Code | Content Type |
+| ------------------------- | ------------------------- | ------------------------- |
+| sdkerrors.V2ErrorResponse | default | application/json |
+| sdkerrors.SDKError | 4XX, 5XX | \*/\* |
\ No newline at end of file
diff --git a/pkg/client/models/components/v2createexporterrequest.go b/pkg/client/models/components/v2createexporterrequest.go
new file mode 100644
index 0000000000..875ef14dc4
--- /dev/null
+++ b/pkg/client/models/components/v2createexporterrequest.go
@@ -0,0 +1,22 @@
+// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
+
+package components
+
+type V2CreateExporterRequest struct {
+ Driver string `json:"driver"`
+ Config map[string]any `json:"config"`
+}
+
+func (o *V2CreateExporterRequest) GetDriver() string {
+ if o == nil {
+ return ""
+ }
+ return o.Driver
+}
+
+func (o *V2CreateExporterRequest) GetConfig() map[string]any {
+ if o == nil {
+ return map[string]any{}
+ }
+ return o.Config
+}
diff --git a/pkg/client/models/components/v2createexporterresponse.go b/pkg/client/models/components/v2createexporterresponse.go
new file mode 100644
index 0000000000..4157466bb3
--- /dev/null
+++ b/pkg/client/models/components/v2createexporterresponse.go
@@ -0,0 +1,15 @@
+// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
+
+package components
+
+// V2CreateExporterResponse - Created exporter
+type V2CreateExporterResponse struct {
+ Data V2Exporter `json:"data"`
+}
+
+func (o *V2CreateExporterResponse) GetData() V2Exporter {
+ if o == nil {
+ return V2Exporter{}
+ }
+ return o.Data
+}
diff --git a/pkg/client/models/components/v2createpipelinerequest.go b/pkg/client/models/components/v2createpipelinerequest.go
new file mode 100644
index 0000000000..409d5d7a28
--- /dev/null
+++ b/pkg/client/models/components/v2createpipelinerequest.go
@@ -0,0 +1,14 @@
+// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
+
+package components
+
+type V2CreatePipelineRequest struct {
+ ExporterID string `json:"exporterID"`
+}
+
+func (o *V2CreatePipelineRequest) GetExporterID() string {
+ if o == nil {
+ return ""
+ }
+ return o.ExporterID
+}
diff --git a/pkg/client/models/components/v2createpipelineresponse.go b/pkg/client/models/components/v2createpipelineresponse.go
new file mode 100644
index 0000000000..a07897d94b
--- /dev/null
+++ b/pkg/client/models/components/v2createpipelineresponse.go
@@ -0,0 +1,15 @@
+// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
+
+package components
+
+// V2CreatePipelineResponse - Created ipeline
+type V2CreatePipelineResponse struct {
+ Data V2Pipeline `json:"data"`
+}
+
+func (o *V2CreatePipelineResponse) GetData() V2Pipeline {
+ if o == nil {
+ return V2Pipeline{}
+ }
+ return o.Data
+}
diff --git a/pkg/client/models/components/v2exporter.go b/pkg/client/models/components/v2exporter.go
new file mode 100644
index 0000000000..bafc68653e
--- /dev/null
+++ b/pkg/client/models/components/v2exporter.go
@@ -0,0 +1,54 @@
+// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
+
+package components
+
+import (
+ "github.com/formancehq/ledger/pkg/client/internal/utils"
+ "time"
+)
+
+type V2Exporter struct {
+ Driver string `json:"driver"`
+ Config map[string]any `json:"config"`
+ ID string `json:"id"`
+ CreatedAt time.Time `json:"createdAt"`
+}
+
+func (v V2Exporter) MarshalJSON() ([]byte, error) {
+ return utils.MarshalJSON(v, "", false)
+}
+
+func (v *V2Exporter) UnmarshalJSON(data []byte) error {
+ if err := utils.UnmarshalJSON(data, &v, "", false, false); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (o *V2Exporter) GetDriver() string {
+ if o == nil {
+ return ""
+ }
+ return o.Driver
+}
+
+func (o *V2Exporter) GetConfig() map[string]any {
+ if o == nil {
+ return map[string]any{}
+ }
+ return o.Config
+}
+
+func (o *V2Exporter) GetID() string {
+ if o == nil {
+ return ""
+ }
+ return o.ID
+}
+
+func (o *V2Exporter) GetCreatedAt() time.Time {
+ if o == nil {
+ return time.Time{}
+ }
+ return o.CreatedAt
+}
diff --git a/pkg/client/models/components/v2getexporterstateresponse.go b/pkg/client/models/components/v2getexporterstateresponse.go
new file mode 100644
index 0000000000..6c333b8ac2
--- /dev/null
+++ b/pkg/client/models/components/v2getexporterstateresponse.go
@@ -0,0 +1,15 @@
+// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
+
+package components
+
+// V2GetExporterStateResponse - Exporter information
+type V2GetExporterStateResponse struct {
+ Data V2Exporter `json:"data"`
+}
+
+func (o *V2GetExporterStateResponse) GetData() V2Exporter {
+ if o == nil {
+ return V2Exporter{}
+ }
+ return o.Data
+}
diff --git a/pkg/client/models/components/v2getpipelinestateresponse.go b/pkg/client/models/components/v2getpipelinestateresponse.go
new file mode 100644
index 0000000000..709a89a15b
--- /dev/null
+++ b/pkg/client/models/components/v2getpipelinestateresponse.go
@@ -0,0 +1,15 @@
+// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
+
+package components
+
+// V2GetPipelineStateResponse - Pipeline information
+type V2GetPipelineStateResponse struct {
+ Data V2Pipeline `json:"data"`
+}
+
+func (o *V2GetPipelineStateResponse) GetData() V2Pipeline {
+ if o == nil {
+ return V2Pipeline{}
+ }
+ return o.Data
+}
diff --git a/pkg/client/models/components/v2listexportersresponse.go b/pkg/client/models/components/v2listexportersresponse.go
new file mode 100644
index 0000000000..91556b36a5
--- /dev/null
+++ b/pkg/client/models/components/v2listexportersresponse.go
@@ -0,0 +1,77 @@
+// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
+
+package components
+
+type V2ListExportersResponseCursorCursor struct {
+ PageSize int64 `json:"pageSize"`
+ HasMore bool `json:"hasMore"`
+ Previous *string `json:"previous,omitempty"`
+ Next *string `json:"next,omitempty"`
+ Data []V2Exporter `json:"data"`
+}
+
+func (o *V2ListExportersResponseCursorCursor) GetPageSize() int64 {
+ if o == nil {
+ return 0
+ }
+ return o.PageSize
+}
+
+func (o *V2ListExportersResponseCursorCursor) GetHasMore() bool {
+ if o == nil {
+ return false
+ }
+ return o.HasMore
+}
+
+func (o *V2ListExportersResponseCursorCursor) GetPrevious() *string {
+ if o == nil {
+ return nil
+ }
+ return o.Previous
+}
+
+func (o *V2ListExportersResponseCursorCursor) GetNext() *string {
+ if o == nil {
+ return nil
+ }
+ return o.Next
+}
+
+func (o *V2ListExportersResponseCursorCursor) GetData() []V2Exporter {
+ if o == nil {
+ return []V2Exporter{}
+ }
+ return o.Data
+}
+
+type V2ListExportersResponseCursor struct {
+ Cursor V2ListExportersResponseCursorCursor `json:"cursor"`
+ Data []V2Exporter `json:"data,omitempty"`
+}
+
+func (o *V2ListExportersResponseCursor) GetCursor() V2ListExportersResponseCursorCursor {
+ if o == nil {
+ return V2ListExportersResponseCursorCursor{}
+ }
+ return o.Cursor
+}
+
+func (o *V2ListExportersResponseCursor) GetData() []V2Exporter {
+ if o == nil {
+ return nil
+ }
+ return o.Data
+}
+
+// V2ListExportersResponse - Exporters list
+type V2ListExportersResponse struct {
+ Cursor *V2ListExportersResponseCursor `json:"cursor,omitempty"`
+}
+
+func (o *V2ListExportersResponse) GetCursor() *V2ListExportersResponseCursor {
+ if o == nil {
+ return nil
+ }
+ return o.Cursor
+}
diff --git a/pkg/client/models/components/v2listpipelinesresponse.go b/pkg/client/models/components/v2listpipelinesresponse.go
new file mode 100644
index 0000000000..b8ad8baf4f
--- /dev/null
+++ b/pkg/client/models/components/v2listpipelinesresponse.go
@@ -0,0 +1,77 @@
+// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
+
+package components
+
+type V2ListPipelinesResponseCursorCursor struct {
+ PageSize int64 `json:"pageSize"`
+ HasMore bool `json:"hasMore"`
+ Previous *string `json:"previous,omitempty"`
+ Next *string `json:"next,omitempty"`
+ Data []V2Pipeline `json:"data"`
+}
+
+func (o *V2ListPipelinesResponseCursorCursor) GetPageSize() int64 {
+ if o == nil {
+ return 0
+ }
+ return o.PageSize
+}
+
+func (o *V2ListPipelinesResponseCursorCursor) GetHasMore() bool {
+ if o == nil {
+ return false
+ }
+ return o.HasMore
+}
+
+func (o *V2ListPipelinesResponseCursorCursor) GetPrevious() *string {
+ if o == nil {
+ return nil
+ }
+ return o.Previous
+}
+
+func (o *V2ListPipelinesResponseCursorCursor) GetNext() *string {
+ if o == nil {
+ return nil
+ }
+ return o.Next
+}
+
+func (o *V2ListPipelinesResponseCursorCursor) GetData() []V2Pipeline {
+ if o == nil {
+ return []V2Pipeline{}
+ }
+ return o.Data
+}
+
+type V2ListPipelinesResponseCursor struct {
+ Cursor V2ListPipelinesResponseCursorCursor `json:"cursor"`
+ Data []V2Pipeline `json:"data,omitempty"`
+}
+
+func (o *V2ListPipelinesResponseCursor) GetCursor() V2ListPipelinesResponseCursorCursor {
+ if o == nil {
+ return V2ListPipelinesResponseCursorCursor{}
+ }
+ return o.Cursor
+}
+
+func (o *V2ListPipelinesResponseCursor) GetData() []V2Pipeline {
+ if o == nil {
+ return nil
+ }
+ return o.Data
+}
+
+// V2ListPipelinesResponse - Pipelines list
+type V2ListPipelinesResponse struct {
+ Cursor *V2ListPipelinesResponseCursor `json:"cursor,omitempty"`
+}
+
+func (o *V2ListPipelinesResponse) GetCursor() *V2ListPipelinesResponseCursor {
+ if o == nil {
+ return nil
+ }
+ return o.Cursor
+}
diff --git a/pkg/client/models/components/v2pipeline.go b/pkg/client/models/components/v2pipeline.go
new file mode 100644
index 0000000000..aec0679f76
--- /dev/null
+++ b/pkg/client/models/components/v2pipeline.go
@@ -0,0 +1,70 @@
+// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
+
+package components
+
+import (
+ "github.com/formancehq/ledger/pkg/client/internal/utils"
+ "time"
+)
+
+type V2Pipeline struct {
+ Ledger string `json:"ledger"`
+ ExporterID string `json:"exporterID"`
+ ID string `json:"id"`
+ CreatedAt time.Time `json:"createdAt"`
+ LastLogID *int64 `json:"lastLogID,omitempty"`
+ Enabled *bool `json:"enabled,omitempty"`
+}
+
+func (v V2Pipeline) MarshalJSON() ([]byte, error) {
+ return utils.MarshalJSON(v, "", false)
+}
+
+func (v *V2Pipeline) UnmarshalJSON(data []byte) error {
+ if err := utils.UnmarshalJSON(data, &v, "", false, false); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (o *V2Pipeline) GetLedger() string {
+ if o == nil {
+ return ""
+ }
+ return o.Ledger
+}
+
+func (o *V2Pipeline) GetExporterID() string {
+ if o == nil {
+ return ""
+ }
+ return o.ExporterID
+}
+
+func (o *V2Pipeline) GetID() string {
+ if o == nil {
+ return ""
+ }
+ return o.ID
+}
+
+func (o *V2Pipeline) GetCreatedAt() time.Time {
+ if o == nil {
+ return time.Time{}
+ }
+ return o.CreatedAt
+}
+
+func (o *V2Pipeline) GetLastLogID() *int64 {
+ if o == nil {
+ return nil
+ }
+ return o.LastLogID
+}
+
+func (o *V2Pipeline) GetEnabled() *bool {
+ if o == nil {
+ return nil
+ }
+ return o.Enabled
+}
diff --git a/pkg/client/models/operations/v2createexporter.go b/pkg/client/models/operations/v2createexporter.go
new file mode 100644
index 0000000000..dda385cf7b
--- /dev/null
+++ b/pkg/client/models/operations/v2createexporter.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 V2CreateExporterResponse struct {
+ HTTPMeta components.HTTPMetadata `json:"-"`
+ // Created exporter
+ V2CreateExporterResponse *components.V2CreateExporterResponse
+}
+
+func (o *V2CreateExporterResponse) GetHTTPMeta() components.HTTPMetadata {
+ if o == nil {
+ return components.HTTPMetadata{}
+ }
+ return o.HTTPMeta
+}
+
+func (o *V2CreateExporterResponse) GetV2CreateExporterResponse() *components.V2CreateExporterResponse {
+ if o == nil {
+ return nil
+ }
+ return o.V2CreateExporterResponse
+}
diff --git a/pkg/client/models/operations/v2createpipeline.go b/pkg/client/models/operations/v2createpipeline.go
new file mode 100644
index 0000000000..caf408236d
--- /dev/null
+++ b/pkg/client/models/operations/v2createpipeline.go
@@ -0,0 +1,47 @@
+// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
+
+package operations
+
+import (
+ "github.com/formancehq/ledger/pkg/client/models/components"
+)
+
+type V2CreatePipelineRequest struct {
+ // Name of the ledger.
+ Ledger string `pathParam:"style=simple,explode=false,name=ledger"`
+ V2CreatePipelineRequest *components.V2CreatePipelineRequest `request:"mediaType=application/json"`
+}
+
+func (o *V2CreatePipelineRequest) GetLedger() string {
+ if o == nil {
+ return ""
+ }
+ return o.Ledger
+}
+
+func (o *V2CreatePipelineRequest) GetV2CreatePipelineRequest() *components.V2CreatePipelineRequest {
+ if o == nil {
+ return nil
+ }
+ return o.V2CreatePipelineRequest
+}
+
+type V2CreatePipelineResponse struct {
+ HTTPMeta components.HTTPMetadata `json:"-"`
+ // Created ipeline
+ V2CreatePipelineResponse *components.V2CreatePipelineResponse
+}
+
+func (o *V2CreatePipelineResponse) GetHTTPMeta() components.HTTPMetadata {
+ if o == nil {
+ return components.HTTPMetadata{}
+ }
+ return o.HTTPMeta
+}
+
+func (o *V2CreatePipelineResponse) GetV2CreatePipelineResponse() *components.V2CreatePipelineResponse {
+ if o == nil {
+ return nil
+ }
+ return o.V2CreatePipelineResponse
+}
diff --git a/pkg/client/models/operations/v2deleteexporter.go b/pkg/client/models/operations/v2deleteexporter.go
new file mode 100644
index 0000000000..22fdd8a52f
--- /dev/null
+++ b/pkg/client/models/operations/v2deleteexporter.go
@@ -0,0 +1,30 @@
+// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
+
+package operations
+
+import (
+ "github.com/formancehq/ledger/pkg/client/models/components"
+)
+
+type V2DeleteExporterRequest struct {
+ // The exporter id
+ ExporterID string `pathParam:"style=simple,explode=false,name=exporterID"`
+}
+
+func (o *V2DeleteExporterRequest) GetExporterID() string {
+ if o == nil {
+ return ""
+ }
+ return o.ExporterID
+}
+
+type V2DeleteExporterResponse struct {
+ HTTPMeta components.HTTPMetadata `json:"-"`
+}
+
+func (o *V2DeleteExporterResponse) GetHTTPMeta() components.HTTPMetadata {
+ if o == nil {
+ return components.HTTPMetadata{}
+ }
+ return o.HTTPMeta
+}
diff --git a/pkg/client/models/operations/v2deletepipeline.go b/pkg/client/models/operations/v2deletepipeline.go
new file mode 100644
index 0000000000..6cf8aad843
--- /dev/null
+++ b/pkg/client/models/operations/v2deletepipeline.go
@@ -0,0 +1,39 @@
+// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
+
+package operations
+
+import (
+ "github.com/formancehq/ledger/pkg/client/models/components"
+)
+
+type V2DeletePipelineRequest struct {
+ // Name of the ledger.
+ Ledger string `pathParam:"style=simple,explode=false,name=ledger"`
+ // The pipeline id
+ PipelineID string `pathParam:"style=simple,explode=false,name=pipelineID"`
+}
+
+func (o *V2DeletePipelineRequest) GetLedger() string {
+ if o == nil {
+ return ""
+ }
+ return o.Ledger
+}
+
+func (o *V2DeletePipelineRequest) GetPipelineID() string {
+ if o == nil {
+ return ""
+ }
+ return o.PipelineID
+}
+
+type V2DeletePipelineResponse struct {
+ HTTPMeta components.HTTPMetadata `json:"-"`
+}
+
+func (o *V2DeletePipelineResponse) GetHTTPMeta() components.HTTPMetadata {
+ if o == nil {
+ return components.HTTPMetadata{}
+ }
+ return o.HTTPMeta
+}
diff --git a/pkg/client/models/operations/v2getexporterstate.go b/pkg/client/models/operations/v2getexporterstate.go
new file mode 100644
index 0000000000..bd18d9684b
--- /dev/null
+++ b/pkg/client/models/operations/v2getexporterstate.go
@@ -0,0 +1,39 @@
+// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
+
+package operations
+
+import (
+ "github.com/formancehq/ledger/pkg/client/models/components"
+)
+
+type V2GetExporterStateRequest struct {
+ // The exporter id
+ ExporterID string `pathParam:"style=simple,explode=false,name=exporterID"`
+}
+
+func (o *V2GetExporterStateRequest) GetExporterID() string {
+ if o == nil {
+ return ""
+ }
+ return o.ExporterID
+}
+
+type V2GetExporterStateResponse struct {
+ HTTPMeta components.HTTPMetadata `json:"-"`
+ // Exporter information
+ V2GetExporterStateResponse *components.V2GetExporterStateResponse
+}
+
+func (o *V2GetExporterStateResponse) GetHTTPMeta() components.HTTPMetadata {
+ if o == nil {
+ return components.HTTPMetadata{}
+ }
+ return o.HTTPMeta
+}
+
+func (o *V2GetExporterStateResponse) GetV2GetExporterStateResponse() *components.V2GetExporterStateResponse {
+ if o == nil {
+ return nil
+ }
+ return o.V2GetExporterStateResponse
+}
diff --git a/pkg/client/models/operations/v2getpipelinestate.go b/pkg/client/models/operations/v2getpipelinestate.go
new file mode 100644
index 0000000000..e2f3248b6e
--- /dev/null
+++ b/pkg/client/models/operations/v2getpipelinestate.go
@@ -0,0 +1,48 @@
+// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
+
+package operations
+
+import (
+ "github.com/formancehq/ledger/pkg/client/models/components"
+)
+
+type V2GetPipelineStateRequest struct {
+ // Name of the ledger.
+ Ledger string `pathParam:"style=simple,explode=false,name=ledger"`
+ // The pipeline id
+ PipelineID string `pathParam:"style=simple,explode=false,name=pipelineID"`
+}
+
+func (o *V2GetPipelineStateRequest) GetLedger() string {
+ if o == nil {
+ return ""
+ }
+ return o.Ledger
+}
+
+func (o *V2GetPipelineStateRequest) GetPipelineID() string {
+ if o == nil {
+ return ""
+ }
+ return o.PipelineID
+}
+
+type V2GetPipelineStateResponse struct {
+ HTTPMeta components.HTTPMetadata `json:"-"`
+ // Pipeline information
+ V2GetPipelineStateResponse *components.V2GetPipelineStateResponse
+}
+
+func (o *V2GetPipelineStateResponse) GetHTTPMeta() components.HTTPMetadata {
+ if o == nil {
+ return components.HTTPMetadata{}
+ }
+ return o.HTTPMeta
+}
+
+func (o *V2GetPipelineStateResponse) GetV2GetPipelineStateResponse() *components.V2GetPipelineStateResponse {
+ if o == nil {
+ return nil
+ }
+ return o.V2GetPipelineStateResponse
+}
diff --git a/pkg/client/models/operations/v2listexporters.go b/pkg/client/models/operations/v2listexporters.go
new file mode 100644
index 0000000000..abd1eba6a4
--- /dev/null
+++ b/pkg/client/models/operations/v2listexporters.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 V2ListExportersResponse struct {
+ HTTPMeta components.HTTPMetadata `json:"-"`
+ // Exporters list
+ V2ListExportersResponse *components.V2ListExportersResponse
+}
+
+func (o *V2ListExportersResponse) GetHTTPMeta() components.HTTPMetadata {
+ if o == nil {
+ return components.HTTPMetadata{}
+ }
+ return o.HTTPMeta
+}
+
+func (o *V2ListExportersResponse) GetV2ListExportersResponse() *components.V2ListExportersResponse {
+ if o == nil {
+ return nil
+ }
+ return o.V2ListExportersResponse
+}
diff --git a/pkg/client/models/operations/v2listpipelines.go b/pkg/client/models/operations/v2listpipelines.go
new file mode 100644
index 0000000000..66a546a09c
--- /dev/null
+++ b/pkg/client/models/operations/v2listpipelines.go
@@ -0,0 +1,39 @@
+// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
+
+package operations
+
+import (
+ "github.com/formancehq/ledger/pkg/client/models/components"
+)
+
+type V2ListPipelinesRequest struct {
+ // Name of the ledger.
+ Ledger string `pathParam:"style=simple,explode=false,name=ledger"`
+}
+
+func (o *V2ListPipelinesRequest) GetLedger() string {
+ if o == nil {
+ return ""
+ }
+ return o.Ledger
+}
+
+type V2ListPipelinesResponse struct {
+ HTTPMeta components.HTTPMetadata `json:"-"`
+ // Pipelines list
+ V2ListPipelinesResponse *components.V2ListPipelinesResponse
+}
+
+func (o *V2ListPipelinesResponse) GetHTTPMeta() components.HTTPMetadata {
+ if o == nil {
+ return components.HTTPMetadata{}
+ }
+ return o.HTTPMeta
+}
+
+func (o *V2ListPipelinesResponse) GetV2ListPipelinesResponse() *components.V2ListPipelinesResponse {
+ if o == nil {
+ return nil
+ }
+ return o.V2ListPipelinesResponse
+}
diff --git a/pkg/client/models/operations/v2resetpipeline.go b/pkg/client/models/operations/v2resetpipeline.go
new file mode 100644
index 0000000000..505cf17304
--- /dev/null
+++ b/pkg/client/models/operations/v2resetpipeline.go
@@ -0,0 +1,39 @@
+// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
+
+package operations
+
+import (
+ "github.com/formancehq/ledger/pkg/client/models/components"
+)
+
+type V2ResetPipelineRequest struct {
+ // Name of the ledger.
+ Ledger string `pathParam:"style=simple,explode=false,name=ledger"`
+ // The pipeline id
+ PipelineID string `pathParam:"style=simple,explode=false,name=pipelineID"`
+}
+
+func (o *V2ResetPipelineRequest) GetLedger() string {
+ if o == nil {
+ return ""
+ }
+ return o.Ledger
+}
+
+func (o *V2ResetPipelineRequest) GetPipelineID() string {
+ if o == nil {
+ return ""
+ }
+ return o.PipelineID
+}
+
+type V2ResetPipelineResponse struct {
+ HTTPMeta components.HTTPMetadata `json:"-"`
+}
+
+func (o *V2ResetPipelineResponse) GetHTTPMeta() components.HTTPMetadata {
+ if o == nil {
+ return components.HTTPMetadata{}
+ }
+ return o.HTTPMeta
+}
diff --git a/pkg/client/models/operations/v2startpipeline.go b/pkg/client/models/operations/v2startpipeline.go
new file mode 100644
index 0000000000..1b76332ff3
--- /dev/null
+++ b/pkg/client/models/operations/v2startpipeline.go
@@ -0,0 +1,39 @@
+// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
+
+package operations
+
+import (
+ "github.com/formancehq/ledger/pkg/client/models/components"
+)
+
+type V2StartPipelineRequest struct {
+ // Name of the ledger.
+ Ledger string `pathParam:"style=simple,explode=false,name=ledger"`
+ // The pipeline id
+ PipelineID string `pathParam:"style=simple,explode=false,name=pipelineID"`
+}
+
+func (o *V2StartPipelineRequest) GetLedger() string {
+ if o == nil {
+ return ""
+ }
+ return o.Ledger
+}
+
+func (o *V2StartPipelineRequest) GetPipelineID() string {
+ if o == nil {
+ return ""
+ }
+ return o.PipelineID
+}
+
+type V2StartPipelineResponse struct {
+ HTTPMeta components.HTTPMetadata `json:"-"`
+}
+
+func (o *V2StartPipelineResponse) GetHTTPMeta() components.HTTPMetadata {
+ if o == nil {
+ return components.HTTPMetadata{}
+ }
+ return o.HTTPMeta
+}
diff --git a/pkg/client/models/operations/v2stoppipeline.go b/pkg/client/models/operations/v2stoppipeline.go
new file mode 100644
index 0000000000..f4e8095332
--- /dev/null
+++ b/pkg/client/models/operations/v2stoppipeline.go
@@ -0,0 +1,39 @@
+// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
+
+package operations
+
+import (
+ "github.com/formancehq/ledger/pkg/client/models/components"
+)
+
+type V2StopPipelineRequest struct {
+ // Name of the ledger.
+ Ledger string `pathParam:"style=simple,explode=false,name=ledger"`
+ // The pipeline id
+ PipelineID string `pathParam:"style=simple,explode=false,name=pipelineID"`
+}
+
+func (o *V2StopPipelineRequest) GetLedger() string {
+ if o == nil {
+ return ""
+ }
+ return o.Ledger
+}
+
+func (o *V2StopPipelineRequest) GetPipelineID() string {
+ if o == nil {
+ return ""
+ }
+ return o.PipelineID
+}
+
+type V2StopPipelineResponse struct {
+ HTTPMeta components.HTTPMetadata `json:"-"`
+}
+
+func (o *V2StopPipelineResponse) GetHTTPMeta() components.HTTPMetadata {
+ if o == nil {
+ return components.HTTPMetadata{}
+ }
+ return o.HTTPMeta
+}
diff --git a/pkg/client/v2.go b/pkg/client/v2.go
index 35b1ae69fc..ada3c359fa 100644
--- a/pkg/client/v2.go
+++ b/pkg/client/v2.go
@@ -5116,3 +5116,2161 @@ func (s *V2) ExportLogs(ctx context.Context, request operations.V2ExportLogsRequ
return res, nil
}
+
+// ListExporters - List exporters
+func (s *V2) ListExporters(ctx context.Context, opts ...operations.Option) (*operations.V2ListExportersResponse, error) {
+ o := operations.Options{}
+ supportedOptions := []string{
+ operations.SupportedOptionRetries,
+ operations.SupportedOptionTimeout,
+ }
+
+ for _, opt := range opts {
+ if err := opt(&o, supportedOptions...); err != nil {
+ return nil, fmt.Errorf("error applying option: %w", err)
+ }
+ }
+
+ var baseURL string
+ if o.ServerURL == nil {
+ baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails())
+ } else {
+ baseURL = *o.ServerURL
+ }
+ opURL, err := url.JoinPath(baseURL, "/v2/_/exporters")
+ if err != nil {
+ return nil, fmt.Errorf("error generating URL: %w", err)
+ }
+
+ hookCtx := hooks.HookContext{
+ BaseURL: baseURL,
+ Context: ctx,
+ OperationID: "v2ListExporters",
+ OAuth2Scopes: []string{"ledger:read"},
+ SecuritySource: s.sdkConfiguration.Security,
+ }
+
+ timeout := o.Timeout
+ if timeout == nil {
+ timeout = s.sdkConfiguration.Timeout
+ }
+
+ if timeout != nil {
+ var cancel context.CancelFunc
+ ctx, cancel = context.WithTimeout(ctx, *timeout)
+ defer cancel()
+ }
+
+ req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error creating request: %w", err)
+ }
+ req.Header.Set("Accept", "application/json")
+ req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent)
+
+ if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil {
+ return nil, err
+ }
+
+ for k, v := range o.SetHeaders {
+ req.Header.Set(k, v)
+ }
+
+ globalRetryConfig := s.sdkConfiguration.RetryConfig
+ retryConfig := o.Retries
+ if retryConfig == nil {
+ if globalRetryConfig != nil {
+ retryConfig = globalRetryConfig
+ }
+ }
+
+ var httpRes *http.Response
+ if retryConfig != nil {
+ httpRes, err = utils.Retry(ctx, utils.Retries{
+ Config: retryConfig,
+ StatusCodes: []string{
+ "429",
+ "500",
+ "502",
+ "503",
+ "504",
+ },
+ }, func() (*http.Response, error) {
+ if req.Body != nil {
+ 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 {
+ if retry.IsPermanentError(err) || retry.IsTemporaryError(err) {
+ return nil, err
+ }
+
+ return nil, retry.Permanent(err)
+ }
+
+ httpRes, err := s.sdkConfiguration.Client.Do(req)
+ if err != nil || httpRes == nil {
+ if err != nil {
+ err = fmt.Errorf("error sending request: %w", err)
+ } else {
+ err = fmt.Errorf("error sending request: no response")
+ }
+
+ _, err = s.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.V2ListExportersResponse{
+ HTTPMeta: components.HTTPMetadata{
+ Request: req,
+ Response: httpRes,
+ },
+ }
+
+ switch {
+ case httpRes.StatusCode == 200:
+ switch {
+ case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`):
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+
+ var out components.V2ListExportersResponse
+ if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil {
+ return nil, err
+ }
+
+ res.V2ListExportersResponse = &out
+ default:
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+ return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes)
+ }
+ default:
+ switch {
+ case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`):
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+
+ var out sdkerrors.V2ErrorResponse
+ if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil {
+ return nil, err
+ }
+
+ return nil, &out
+ default:
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+ return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes)
+ }
+ }
+
+ return res, nil
+
+}
+
+// CreateExporter - Create exporter
+func (s *V2) CreateExporter(ctx context.Context, request components.V2CreateExporterRequest, opts ...operations.Option) (*operations.V2CreateExporterResponse, error) {
+ o := operations.Options{}
+ supportedOptions := []string{
+ operations.SupportedOptionRetries,
+ operations.SupportedOptionTimeout,
+ }
+
+ for _, opt := range opts {
+ if err := opt(&o, supportedOptions...); err != nil {
+ return nil, fmt.Errorf("error applying option: %w", err)
+ }
+ }
+
+ var baseURL string
+ if o.ServerURL == nil {
+ baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails())
+ } else {
+ baseURL = *o.ServerURL
+ }
+ opURL, err := url.JoinPath(baseURL, "/v2/_/exporters")
+ if err != nil {
+ return nil, fmt.Errorf("error generating URL: %w", err)
+ }
+
+ hookCtx := hooks.HookContext{
+ BaseURL: baseURL,
+ Context: ctx,
+ OperationID: "v2CreateExporter",
+ OAuth2Scopes: []string{"ledger:read"},
+ SecuritySource: s.sdkConfiguration.Security,
+ }
+ bodyReader, reqContentType, err := utils.SerializeRequestBody(ctx, request, false, false, "Request", "json", `request:"mediaType=application/json"`)
+ if err != nil {
+ return nil, err
+ }
+
+ timeout := o.Timeout
+ if timeout == nil {
+ timeout = s.sdkConfiguration.Timeout
+ }
+
+ if timeout != nil {
+ var cancel context.CancelFunc
+ ctx, cancel = context.WithTimeout(ctx, *timeout)
+ defer cancel()
+ }
+
+ req, err := http.NewRequestWithContext(ctx, "POST", opURL, bodyReader)
+ if err != nil {
+ return nil, fmt.Errorf("error creating request: %w", err)
+ }
+ req.Header.Set("Accept", "application/json")
+ req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent)
+ if reqContentType != "" {
+ req.Header.Set("Content-Type", reqContentType)
+ }
+
+ if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil {
+ return nil, err
+ }
+
+ for k, v := range o.SetHeaders {
+ req.Header.Set(k, v)
+ }
+
+ globalRetryConfig := s.sdkConfiguration.RetryConfig
+ retryConfig := o.Retries
+ if retryConfig == nil {
+ if globalRetryConfig != nil {
+ retryConfig = globalRetryConfig
+ }
+ }
+
+ var httpRes *http.Response
+ if retryConfig != nil {
+ httpRes, err = utils.Retry(ctx, utils.Retries{
+ Config: retryConfig,
+ StatusCodes: []string{
+ "429",
+ "500",
+ "502",
+ "503",
+ "504",
+ },
+ }, func() (*http.Response, error) {
+ if req.Body != nil {
+ 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 {
+ if retry.IsPermanentError(err) || retry.IsTemporaryError(err) {
+ return nil, err
+ }
+
+ return nil, retry.Permanent(err)
+ }
+
+ httpRes, err := s.sdkConfiguration.Client.Do(req)
+ if err != nil || httpRes == nil {
+ if err != nil {
+ err = fmt.Errorf("error sending request: %w", err)
+ } else {
+ err = fmt.Errorf("error sending request: no response")
+ }
+
+ _, err = s.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.V2CreateExporterResponse{
+ HTTPMeta: components.HTTPMetadata{
+ Request: req,
+ Response: httpRes,
+ },
+ }
+
+ switch {
+ case httpRes.StatusCode == 201:
+ switch {
+ case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`):
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+
+ var out components.V2CreateExporterResponse
+ if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil {
+ return nil, err
+ }
+
+ res.V2CreateExporterResponse = &out
+ default:
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+ return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes)
+ }
+ default:
+ switch {
+ case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`):
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+
+ var out sdkerrors.V2ErrorResponse
+ if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil {
+ return nil, err
+ }
+
+ return nil, &out
+ default:
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+ return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes)
+ }
+ }
+
+ return res, nil
+
+}
+
+// GetExporterState - Get exporter state
+func (s *V2) GetExporterState(ctx context.Context, request operations.V2GetExporterStateRequest, opts ...operations.Option) (*operations.V2GetExporterStateResponse, error) {
+ o := operations.Options{}
+ supportedOptions := []string{
+ operations.SupportedOptionRetries,
+ operations.SupportedOptionTimeout,
+ }
+
+ for _, opt := range opts {
+ if err := opt(&o, supportedOptions...); err != nil {
+ return nil, fmt.Errorf("error applying option: %w", err)
+ }
+ }
+
+ var baseURL string
+ if o.ServerURL == nil {
+ baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails())
+ } else {
+ baseURL = *o.ServerURL
+ }
+ opURL, err := utils.GenerateURL(ctx, baseURL, "/v2/_/exporters/{exporterID}", request, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error generating URL: %w", err)
+ }
+
+ hookCtx := hooks.HookContext{
+ BaseURL: baseURL,
+ Context: ctx,
+ OperationID: "v2GetExporterState",
+ OAuth2Scopes: []string{"ledger:read"},
+ SecuritySource: s.sdkConfiguration.Security,
+ }
+
+ timeout := o.Timeout
+ if timeout == nil {
+ timeout = s.sdkConfiguration.Timeout
+ }
+
+ if timeout != nil {
+ var cancel context.CancelFunc
+ ctx, cancel = context.WithTimeout(ctx, *timeout)
+ defer cancel()
+ }
+
+ req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error creating request: %w", err)
+ }
+ req.Header.Set("Accept", "application/json")
+ req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent)
+
+ if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil {
+ return nil, err
+ }
+
+ for k, v := range o.SetHeaders {
+ req.Header.Set(k, v)
+ }
+
+ globalRetryConfig := s.sdkConfiguration.RetryConfig
+ retryConfig := o.Retries
+ if retryConfig == nil {
+ if globalRetryConfig != nil {
+ retryConfig = globalRetryConfig
+ }
+ }
+
+ var httpRes *http.Response
+ if retryConfig != nil {
+ httpRes, err = utils.Retry(ctx, utils.Retries{
+ Config: retryConfig,
+ StatusCodes: []string{
+ "429",
+ "500",
+ "502",
+ "503",
+ "504",
+ },
+ }, func() (*http.Response, error) {
+ if req.Body != nil {
+ 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 {
+ if retry.IsPermanentError(err) || retry.IsTemporaryError(err) {
+ return nil, err
+ }
+
+ return nil, retry.Permanent(err)
+ }
+
+ httpRes, err := s.sdkConfiguration.Client.Do(req)
+ if err != nil || httpRes == nil {
+ if err != nil {
+ err = fmt.Errorf("error sending request: %w", err)
+ } else {
+ err = fmt.Errorf("error sending request: no response")
+ }
+
+ _, err = s.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.V2GetExporterStateResponse{
+ HTTPMeta: components.HTTPMetadata{
+ Request: req,
+ Response: httpRes,
+ },
+ }
+
+ switch {
+ case httpRes.StatusCode == 200:
+ switch {
+ case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`):
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+
+ var out components.V2GetExporterStateResponse
+ if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil {
+ return nil, err
+ }
+
+ res.V2GetExporterStateResponse = &out
+ default:
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+ return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes)
+ }
+ default:
+ switch {
+ case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`):
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+
+ var out sdkerrors.V2ErrorResponse
+ if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil {
+ return nil, err
+ }
+
+ return nil, &out
+ default:
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+ return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes)
+ }
+ }
+
+ return res, nil
+
+}
+
+// DeleteExporter - Delete exporter
+func (s *V2) DeleteExporter(ctx context.Context, request operations.V2DeleteExporterRequest, opts ...operations.Option) (*operations.V2DeleteExporterResponse, error) {
+ o := operations.Options{}
+ supportedOptions := []string{
+ operations.SupportedOptionRetries,
+ operations.SupportedOptionTimeout,
+ }
+
+ for _, opt := range opts {
+ if err := opt(&o, supportedOptions...); err != nil {
+ return nil, fmt.Errorf("error applying option: %w", err)
+ }
+ }
+
+ var baseURL string
+ if o.ServerURL == nil {
+ baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails())
+ } else {
+ baseURL = *o.ServerURL
+ }
+ opURL, err := utils.GenerateURL(ctx, baseURL, "/v2/_/exporters/{exporterID}", request, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error generating URL: %w", err)
+ }
+
+ hookCtx := hooks.HookContext{
+ BaseURL: baseURL,
+ Context: ctx,
+ OperationID: "v2DeleteExporter",
+ OAuth2Scopes: []string{"ledger:read"},
+ SecuritySource: s.sdkConfiguration.Security,
+ }
+
+ timeout := o.Timeout
+ if timeout == nil {
+ timeout = s.sdkConfiguration.Timeout
+ }
+
+ if timeout != nil {
+ var cancel context.CancelFunc
+ ctx, cancel = context.WithTimeout(ctx, *timeout)
+ defer cancel()
+ }
+
+ req, err := http.NewRequestWithContext(ctx, "DELETE", opURL, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error creating request: %w", err)
+ }
+ req.Header.Set("Accept", "application/json")
+ req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent)
+
+ if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil {
+ return nil, err
+ }
+
+ for k, v := range o.SetHeaders {
+ req.Header.Set(k, v)
+ }
+
+ globalRetryConfig := s.sdkConfiguration.RetryConfig
+ retryConfig := o.Retries
+ if retryConfig == nil {
+ if globalRetryConfig != nil {
+ retryConfig = globalRetryConfig
+ }
+ }
+
+ var httpRes *http.Response
+ if retryConfig != nil {
+ httpRes, err = utils.Retry(ctx, utils.Retries{
+ Config: retryConfig,
+ StatusCodes: []string{
+ "429",
+ "500",
+ "502",
+ "503",
+ "504",
+ },
+ }, func() (*http.Response, error) {
+ if req.Body != nil {
+ 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 {
+ if retry.IsPermanentError(err) || retry.IsTemporaryError(err) {
+ return nil, err
+ }
+
+ return nil, retry.Permanent(err)
+ }
+
+ httpRes, err := s.sdkConfiguration.Client.Do(req)
+ if err != nil || httpRes == nil {
+ if err != nil {
+ err = fmt.Errorf("error sending request: %w", err)
+ } else {
+ err = fmt.Errorf("error sending request: no response")
+ }
+
+ _, err = s.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.V2DeleteExporterResponse{
+ HTTPMeta: components.HTTPMetadata{
+ Request: req,
+ Response: httpRes,
+ },
+ }
+
+ switch {
+ case httpRes.StatusCode == 204:
+ default:
+ switch {
+ case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`):
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+
+ var out sdkerrors.V2ErrorResponse
+ if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil {
+ return nil, err
+ }
+
+ return nil, &out
+ default:
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+ return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes)
+ }
+ }
+
+ return res, nil
+
+}
+
+// ListPipelines - List pipelines
+func (s *V2) ListPipelines(ctx context.Context, request operations.V2ListPipelinesRequest, opts ...operations.Option) (*operations.V2ListPipelinesResponse, error) {
+ o := operations.Options{}
+ supportedOptions := []string{
+ operations.SupportedOptionRetries,
+ operations.SupportedOptionTimeout,
+ }
+
+ for _, opt := range opts {
+ if err := opt(&o, supportedOptions...); err != nil {
+ return nil, fmt.Errorf("error applying option: %w", err)
+ }
+ }
+
+ var baseURL string
+ if o.ServerURL == nil {
+ baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails())
+ } else {
+ baseURL = *o.ServerURL
+ }
+ opURL, err := utils.GenerateURL(ctx, baseURL, "/v2/{ledger}/pipelines", request, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error generating URL: %w", err)
+ }
+
+ hookCtx := hooks.HookContext{
+ BaseURL: baseURL,
+ Context: ctx,
+ OperationID: "v2ListPipelines",
+ OAuth2Scopes: []string{"ledger:read"},
+ SecuritySource: s.sdkConfiguration.Security,
+ }
+
+ timeout := o.Timeout
+ if timeout == nil {
+ timeout = s.sdkConfiguration.Timeout
+ }
+
+ if timeout != nil {
+ var cancel context.CancelFunc
+ ctx, cancel = context.WithTimeout(ctx, *timeout)
+ defer cancel()
+ }
+
+ req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error creating request: %w", err)
+ }
+ req.Header.Set("Accept", "application/json")
+ req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent)
+
+ if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil {
+ return nil, err
+ }
+
+ for k, v := range o.SetHeaders {
+ req.Header.Set(k, v)
+ }
+
+ globalRetryConfig := s.sdkConfiguration.RetryConfig
+ retryConfig := o.Retries
+ if retryConfig == nil {
+ if globalRetryConfig != nil {
+ retryConfig = globalRetryConfig
+ }
+ }
+
+ var httpRes *http.Response
+ if retryConfig != nil {
+ httpRes, err = utils.Retry(ctx, utils.Retries{
+ Config: retryConfig,
+ StatusCodes: []string{
+ "429",
+ "500",
+ "502",
+ "503",
+ "504",
+ },
+ }, func() (*http.Response, error) {
+ if req.Body != nil {
+ 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 {
+ if retry.IsPermanentError(err) || retry.IsTemporaryError(err) {
+ return nil, err
+ }
+
+ return nil, retry.Permanent(err)
+ }
+
+ httpRes, err := s.sdkConfiguration.Client.Do(req)
+ if err != nil || httpRes == nil {
+ if err != nil {
+ err = fmt.Errorf("error sending request: %w", err)
+ } else {
+ err = fmt.Errorf("error sending request: no response")
+ }
+
+ _, err = s.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.V2ListPipelinesResponse{
+ HTTPMeta: components.HTTPMetadata{
+ Request: req,
+ Response: httpRes,
+ },
+ }
+
+ switch {
+ case httpRes.StatusCode == 200:
+ switch {
+ case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`):
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+
+ var out components.V2ListPipelinesResponse
+ if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil {
+ return nil, err
+ }
+
+ res.V2ListPipelinesResponse = &out
+ default:
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+ return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes)
+ }
+ default:
+ switch {
+ case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`):
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+
+ var out sdkerrors.V2ErrorResponse
+ if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil {
+ return nil, err
+ }
+
+ return nil, &out
+ default:
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+ return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes)
+ }
+ }
+
+ return res, nil
+
+}
+
+// CreatePipeline - Create pipeline
+func (s *V2) CreatePipeline(ctx context.Context, request operations.V2CreatePipelineRequest, opts ...operations.Option) (*operations.V2CreatePipelineResponse, error) {
+ o := operations.Options{}
+ supportedOptions := []string{
+ operations.SupportedOptionRetries,
+ operations.SupportedOptionTimeout,
+ }
+
+ for _, opt := range opts {
+ if err := opt(&o, supportedOptions...); err != nil {
+ return nil, fmt.Errorf("error applying option: %w", err)
+ }
+ }
+
+ var baseURL string
+ if o.ServerURL == nil {
+ baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails())
+ } else {
+ baseURL = *o.ServerURL
+ }
+ opURL, err := utils.GenerateURL(ctx, baseURL, "/v2/{ledger}/pipelines", request, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error generating URL: %w", err)
+ }
+
+ hookCtx := hooks.HookContext{
+ BaseURL: baseURL,
+ Context: ctx,
+ OperationID: "v2CreatePipeline",
+ OAuth2Scopes: []string{"ledger:read"},
+ SecuritySource: s.sdkConfiguration.Security,
+ }
+ bodyReader, reqContentType, err := utils.SerializeRequestBody(ctx, request, false, true, "V2CreatePipelineRequest", "json", `request:"mediaType=application/json"`)
+ if err != nil {
+ return nil, err
+ }
+
+ timeout := o.Timeout
+ if timeout == nil {
+ timeout = s.sdkConfiguration.Timeout
+ }
+
+ if timeout != nil {
+ var cancel context.CancelFunc
+ ctx, cancel = context.WithTimeout(ctx, *timeout)
+ defer cancel()
+ }
+
+ req, err := http.NewRequestWithContext(ctx, "POST", opURL, bodyReader)
+ if err != nil {
+ return nil, fmt.Errorf("error creating request: %w", err)
+ }
+ req.Header.Set("Accept", "application/json")
+ req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent)
+ if reqContentType != "" {
+ req.Header.Set("Content-Type", reqContentType)
+ }
+
+ if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil {
+ return nil, err
+ }
+
+ for k, v := range o.SetHeaders {
+ req.Header.Set(k, v)
+ }
+
+ globalRetryConfig := s.sdkConfiguration.RetryConfig
+ retryConfig := o.Retries
+ if retryConfig == nil {
+ if globalRetryConfig != nil {
+ retryConfig = globalRetryConfig
+ }
+ }
+
+ var httpRes *http.Response
+ if retryConfig != nil {
+ httpRes, err = utils.Retry(ctx, utils.Retries{
+ Config: retryConfig,
+ StatusCodes: []string{
+ "429",
+ "500",
+ "502",
+ "503",
+ "504",
+ },
+ }, func() (*http.Response, error) {
+ if req.Body != nil {
+ 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 {
+ if retry.IsPermanentError(err) || retry.IsTemporaryError(err) {
+ return nil, err
+ }
+
+ return nil, retry.Permanent(err)
+ }
+
+ httpRes, err := s.sdkConfiguration.Client.Do(req)
+ if err != nil || httpRes == nil {
+ if err != nil {
+ err = fmt.Errorf("error sending request: %w", err)
+ } else {
+ err = fmt.Errorf("error sending request: no response")
+ }
+
+ _, err = s.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.V2CreatePipelineResponse{
+ HTTPMeta: components.HTTPMetadata{
+ Request: req,
+ Response: httpRes,
+ },
+ }
+
+ switch {
+ case httpRes.StatusCode == 201:
+ switch {
+ case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`):
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+
+ var out components.V2CreatePipelineResponse
+ if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil {
+ return nil, err
+ }
+
+ res.V2CreatePipelineResponse = &out
+ default:
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+ return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes)
+ }
+ default:
+ switch {
+ case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`):
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+
+ var out sdkerrors.V2ErrorResponse
+ if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil {
+ return nil, err
+ }
+
+ return nil, &out
+ default:
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+ return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes)
+ }
+ }
+
+ return res, nil
+
+}
+
+// GetPipelineState - Get pipeline state
+func (s *V2) GetPipelineState(ctx context.Context, request operations.V2GetPipelineStateRequest, opts ...operations.Option) (*operations.V2GetPipelineStateResponse, error) {
+ o := operations.Options{}
+ supportedOptions := []string{
+ operations.SupportedOptionRetries,
+ operations.SupportedOptionTimeout,
+ }
+
+ for _, opt := range opts {
+ if err := opt(&o, supportedOptions...); err != nil {
+ return nil, fmt.Errorf("error applying option: %w", err)
+ }
+ }
+
+ var baseURL string
+ if o.ServerURL == nil {
+ baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails())
+ } else {
+ baseURL = *o.ServerURL
+ }
+ opURL, err := utils.GenerateURL(ctx, baseURL, "/v2/{ledger}/pipelines/{pipelineID}", request, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error generating URL: %w", err)
+ }
+
+ hookCtx := hooks.HookContext{
+ BaseURL: baseURL,
+ Context: ctx,
+ OperationID: "v2GetPipelineState",
+ OAuth2Scopes: []string{"ledger:read"},
+ SecuritySource: s.sdkConfiguration.Security,
+ }
+
+ timeout := o.Timeout
+ if timeout == nil {
+ timeout = s.sdkConfiguration.Timeout
+ }
+
+ if timeout != nil {
+ var cancel context.CancelFunc
+ ctx, cancel = context.WithTimeout(ctx, *timeout)
+ defer cancel()
+ }
+
+ req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error creating request: %w", err)
+ }
+ req.Header.Set("Accept", "application/json")
+ req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent)
+
+ if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil {
+ return nil, err
+ }
+
+ for k, v := range o.SetHeaders {
+ req.Header.Set(k, v)
+ }
+
+ globalRetryConfig := s.sdkConfiguration.RetryConfig
+ retryConfig := o.Retries
+ if retryConfig == nil {
+ if globalRetryConfig != nil {
+ retryConfig = globalRetryConfig
+ }
+ }
+
+ var httpRes *http.Response
+ if retryConfig != nil {
+ httpRes, err = utils.Retry(ctx, utils.Retries{
+ Config: retryConfig,
+ StatusCodes: []string{
+ "429",
+ "500",
+ "502",
+ "503",
+ "504",
+ },
+ }, func() (*http.Response, error) {
+ if req.Body != nil {
+ 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 {
+ if retry.IsPermanentError(err) || retry.IsTemporaryError(err) {
+ return nil, err
+ }
+
+ return nil, retry.Permanent(err)
+ }
+
+ httpRes, err := s.sdkConfiguration.Client.Do(req)
+ if err != nil || httpRes == nil {
+ if err != nil {
+ err = fmt.Errorf("error sending request: %w", err)
+ } else {
+ err = fmt.Errorf("error sending request: no response")
+ }
+
+ _, err = s.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.V2GetPipelineStateResponse{
+ HTTPMeta: components.HTTPMetadata{
+ Request: req,
+ Response: httpRes,
+ },
+ }
+
+ switch {
+ case httpRes.StatusCode == 200:
+ switch {
+ case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`):
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+
+ var out components.V2GetPipelineStateResponse
+ if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil {
+ return nil, err
+ }
+
+ res.V2GetPipelineStateResponse = &out
+ default:
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+ return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes)
+ }
+ default:
+ switch {
+ case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`):
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+
+ var out sdkerrors.V2ErrorResponse
+ if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil {
+ return nil, err
+ }
+
+ return nil, &out
+ default:
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+ return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes)
+ }
+ }
+
+ return res, nil
+
+}
+
+// DeletePipeline - Delete pipeline
+func (s *V2) DeletePipeline(ctx context.Context, request operations.V2DeletePipelineRequest, opts ...operations.Option) (*operations.V2DeletePipelineResponse, error) {
+ o := operations.Options{}
+ supportedOptions := []string{
+ operations.SupportedOptionRetries,
+ operations.SupportedOptionTimeout,
+ }
+
+ for _, opt := range opts {
+ if err := opt(&o, supportedOptions...); err != nil {
+ return nil, fmt.Errorf("error applying option: %w", err)
+ }
+ }
+
+ var baseURL string
+ if o.ServerURL == nil {
+ baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails())
+ } else {
+ baseURL = *o.ServerURL
+ }
+ opURL, err := utils.GenerateURL(ctx, baseURL, "/v2/{ledger}/pipelines/{pipelineID}", request, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error generating URL: %w", err)
+ }
+
+ hookCtx := hooks.HookContext{
+ BaseURL: baseURL,
+ Context: ctx,
+ OperationID: "v2DeletePipeline",
+ OAuth2Scopes: []string{"ledger:read"},
+ SecuritySource: s.sdkConfiguration.Security,
+ }
+
+ timeout := o.Timeout
+ if timeout == nil {
+ timeout = s.sdkConfiguration.Timeout
+ }
+
+ if timeout != nil {
+ var cancel context.CancelFunc
+ ctx, cancel = context.WithTimeout(ctx, *timeout)
+ defer cancel()
+ }
+
+ req, err := http.NewRequestWithContext(ctx, "DELETE", opURL, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error creating request: %w", err)
+ }
+ req.Header.Set("Accept", "application/json")
+ req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent)
+
+ if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil {
+ return nil, err
+ }
+
+ for k, v := range o.SetHeaders {
+ req.Header.Set(k, v)
+ }
+
+ globalRetryConfig := s.sdkConfiguration.RetryConfig
+ retryConfig := o.Retries
+ if retryConfig == nil {
+ if globalRetryConfig != nil {
+ retryConfig = globalRetryConfig
+ }
+ }
+
+ var httpRes *http.Response
+ if retryConfig != nil {
+ httpRes, err = utils.Retry(ctx, utils.Retries{
+ Config: retryConfig,
+ StatusCodes: []string{
+ "429",
+ "500",
+ "502",
+ "503",
+ "504",
+ },
+ }, func() (*http.Response, error) {
+ if req.Body != nil {
+ 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 {
+ if retry.IsPermanentError(err) || retry.IsTemporaryError(err) {
+ return nil, err
+ }
+
+ return nil, retry.Permanent(err)
+ }
+
+ httpRes, err := s.sdkConfiguration.Client.Do(req)
+ if err != nil || httpRes == nil {
+ if err != nil {
+ err = fmt.Errorf("error sending request: %w", err)
+ } else {
+ err = fmt.Errorf("error sending request: no response")
+ }
+
+ _, err = s.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.V2DeletePipelineResponse{
+ HTTPMeta: components.HTTPMetadata{
+ Request: req,
+ Response: httpRes,
+ },
+ }
+
+ switch {
+ case httpRes.StatusCode == 204:
+ default:
+ switch {
+ case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`):
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+
+ var out sdkerrors.V2ErrorResponse
+ if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil {
+ return nil, err
+ }
+
+ return nil, &out
+ default:
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+ return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes)
+ }
+ }
+
+ return res, nil
+
+}
+
+// ResetPipeline - Reset pipeline
+func (s *V2) ResetPipeline(ctx context.Context, request operations.V2ResetPipelineRequest, opts ...operations.Option) (*operations.V2ResetPipelineResponse, error) {
+ o := operations.Options{}
+ supportedOptions := []string{
+ operations.SupportedOptionRetries,
+ operations.SupportedOptionTimeout,
+ }
+
+ for _, opt := range opts {
+ if err := opt(&o, supportedOptions...); err != nil {
+ return nil, fmt.Errorf("error applying option: %w", err)
+ }
+ }
+
+ var baseURL string
+ if o.ServerURL == nil {
+ baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails())
+ } else {
+ baseURL = *o.ServerURL
+ }
+ opURL, err := utils.GenerateURL(ctx, baseURL, "/v2/{ledger}/pipelines/{pipelineID}/reset", request, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error generating URL: %w", err)
+ }
+
+ hookCtx := hooks.HookContext{
+ BaseURL: baseURL,
+ Context: ctx,
+ OperationID: "v2ResetPipeline",
+ OAuth2Scopes: []string{"ledger:read"},
+ SecuritySource: s.sdkConfiguration.Security,
+ }
+
+ timeout := o.Timeout
+ if timeout == nil {
+ timeout = s.sdkConfiguration.Timeout
+ }
+
+ if timeout != nil {
+ var cancel context.CancelFunc
+ ctx, cancel = context.WithTimeout(ctx, *timeout)
+ defer cancel()
+ }
+
+ req, err := http.NewRequestWithContext(ctx, "POST", opURL, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error creating request: %w", err)
+ }
+ req.Header.Set("Accept", "application/json")
+ req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent)
+
+ if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil {
+ return nil, err
+ }
+
+ for k, v := range o.SetHeaders {
+ req.Header.Set(k, v)
+ }
+
+ globalRetryConfig := s.sdkConfiguration.RetryConfig
+ retryConfig := o.Retries
+ if retryConfig == nil {
+ if globalRetryConfig != nil {
+ retryConfig = globalRetryConfig
+ }
+ }
+
+ var httpRes *http.Response
+ if retryConfig != nil {
+ httpRes, err = utils.Retry(ctx, utils.Retries{
+ Config: retryConfig,
+ StatusCodes: []string{
+ "429",
+ "500",
+ "502",
+ "503",
+ "504",
+ },
+ }, func() (*http.Response, error) {
+ if req.Body != nil {
+ 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 {
+ if retry.IsPermanentError(err) || retry.IsTemporaryError(err) {
+ return nil, err
+ }
+
+ return nil, retry.Permanent(err)
+ }
+
+ httpRes, err := s.sdkConfiguration.Client.Do(req)
+ if err != nil || httpRes == nil {
+ if err != nil {
+ err = fmt.Errorf("error sending request: %w", err)
+ } else {
+ err = fmt.Errorf("error sending request: no response")
+ }
+
+ _, err = s.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.V2ResetPipelineResponse{
+ HTTPMeta: components.HTTPMetadata{
+ Request: req,
+ Response: httpRes,
+ },
+ }
+
+ switch {
+ case httpRes.StatusCode == 202:
+ default:
+ switch {
+ case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`):
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+
+ var out sdkerrors.V2ErrorResponse
+ if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil {
+ return nil, err
+ }
+
+ return nil, &out
+ default:
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+ return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes)
+ }
+ }
+
+ return res, nil
+
+}
+
+// StartPipeline - Start pipeline
+func (s *V2) StartPipeline(ctx context.Context, request operations.V2StartPipelineRequest, opts ...operations.Option) (*operations.V2StartPipelineResponse, error) {
+ o := operations.Options{}
+ supportedOptions := []string{
+ operations.SupportedOptionRetries,
+ operations.SupportedOptionTimeout,
+ }
+
+ for _, opt := range opts {
+ if err := opt(&o, supportedOptions...); err != nil {
+ return nil, fmt.Errorf("error applying option: %w", err)
+ }
+ }
+
+ var baseURL string
+ if o.ServerURL == nil {
+ baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails())
+ } else {
+ baseURL = *o.ServerURL
+ }
+ opURL, err := utils.GenerateURL(ctx, baseURL, "/v2/{ledger}/pipelines/{pipelineID}/start", request, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error generating URL: %w", err)
+ }
+
+ hookCtx := hooks.HookContext{
+ BaseURL: baseURL,
+ Context: ctx,
+ OperationID: "v2StartPipeline",
+ OAuth2Scopes: []string{"ledger:read"},
+ SecuritySource: s.sdkConfiguration.Security,
+ }
+
+ timeout := o.Timeout
+ if timeout == nil {
+ timeout = s.sdkConfiguration.Timeout
+ }
+
+ if timeout != nil {
+ var cancel context.CancelFunc
+ ctx, cancel = context.WithTimeout(ctx, *timeout)
+ defer cancel()
+ }
+
+ req, err := http.NewRequestWithContext(ctx, "POST", opURL, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error creating request: %w", err)
+ }
+ req.Header.Set("Accept", "application/json")
+ req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent)
+
+ if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil {
+ return nil, err
+ }
+
+ for k, v := range o.SetHeaders {
+ req.Header.Set(k, v)
+ }
+
+ globalRetryConfig := s.sdkConfiguration.RetryConfig
+ retryConfig := o.Retries
+ if retryConfig == nil {
+ if globalRetryConfig != nil {
+ retryConfig = globalRetryConfig
+ }
+ }
+
+ var httpRes *http.Response
+ if retryConfig != nil {
+ httpRes, err = utils.Retry(ctx, utils.Retries{
+ Config: retryConfig,
+ StatusCodes: []string{
+ "429",
+ "500",
+ "502",
+ "503",
+ "504",
+ },
+ }, func() (*http.Response, error) {
+ if req.Body != nil {
+ 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 {
+ if retry.IsPermanentError(err) || retry.IsTemporaryError(err) {
+ return nil, err
+ }
+
+ return nil, retry.Permanent(err)
+ }
+
+ httpRes, err := s.sdkConfiguration.Client.Do(req)
+ if err != nil || httpRes == nil {
+ if err != nil {
+ err = fmt.Errorf("error sending request: %w", err)
+ } else {
+ err = fmt.Errorf("error sending request: no response")
+ }
+
+ _, err = s.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.V2StartPipelineResponse{
+ HTTPMeta: components.HTTPMetadata{
+ Request: req,
+ Response: httpRes,
+ },
+ }
+
+ switch {
+ case httpRes.StatusCode == 202:
+ default:
+ switch {
+ case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`):
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+
+ var out sdkerrors.V2ErrorResponse
+ if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil {
+ return nil, err
+ }
+
+ return nil, &out
+ default:
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+ return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes)
+ }
+ }
+
+ return res, nil
+
+}
+
+// StopPipeline - Stop pipeline
+func (s *V2) StopPipeline(ctx context.Context, request operations.V2StopPipelineRequest, opts ...operations.Option) (*operations.V2StopPipelineResponse, error) {
+ o := operations.Options{}
+ supportedOptions := []string{
+ operations.SupportedOptionRetries,
+ operations.SupportedOptionTimeout,
+ }
+
+ for _, opt := range opts {
+ if err := opt(&o, supportedOptions...); err != nil {
+ return nil, fmt.Errorf("error applying option: %w", err)
+ }
+ }
+
+ var baseURL string
+ if o.ServerURL == nil {
+ baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails())
+ } else {
+ baseURL = *o.ServerURL
+ }
+ opURL, err := utils.GenerateURL(ctx, baseURL, "/v2/{ledger}/pipelines/{pipelineID}/stop", request, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error generating URL: %w", err)
+ }
+
+ hookCtx := hooks.HookContext{
+ BaseURL: baseURL,
+ Context: ctx,
+ OperationID: "v2StopPipeline",
+ OAuth2Scopes: []string{"ledger:read"},
+ SecuritySource: s.sdkConfiguration.Security,
+ }
+
+ timeout := o.Timeout
+ if timeout == nil {
+ timeout = s.sdkConfiguration.Timeout
+ }
+
+ if timeout != nil {
+ var cancel context.CancelFunc
+ ctx, cancel = context.WithTimeout(ctx, *timeout)
+ defer cancel()
+ }
+
+ req, err := http.NewRequestWithContext(ctx, "POST", opURL, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error creating request: %w", err)
+ }
+ req.Header.Set("Accept", "application/json")
+ req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent)
+
+ if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil {
+ return nil, err
+ }
+
+ for k, v := range o.SetHeaders {
+ req.Header.Set(k, v)
+ }
+
+ globalRetryConfig := s.sdkConfiguration.RetryConfig
+ retryConfig := o.Retries
+ if retryConfig == nil {
+ if globalRetryConfig != nil {
+ retryConfig = globalRetryConfig
+ }
+ }
+
+ var httpRes *http.Response
+ if retryConfig != nil {
+ httpRes, err = utils.Retry(ctx, utils.Retries{
+ Config: retryConfig,
+ StatusCodes: []string{
+ "429",
+ "500",
+ "502",
+ "503",
+ "504",
+ },
+ }, func() (*http.Response, error) {
+ if req.Body != nil {
+ 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 {
+ if retry.IsPermanentError(err) || retry.IsTemporaryError(err) {
+ return nil, err
+ }
+
+ return nil, retry.Permanent(err)
+ }
+
+ httpRes, err := s.sdkConfiguration.Client.Do(req)
+ if err != nil || httpRes == nil {
+ if err != nil {
+ err = fmt.Errorf("error sending request: %w", err)
+ } else {
+ err = fmt.Errorf("error sending request: no response")
+ }
+
+ _, err = s.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.V2StopPipelineResponse{
+ HTTPMeta: components.HTTPMetadata{
+ Request: req,
+ Response: httpRes,
+ },
+ }
+
+ switch {
+ case httpRes.StatusCode == 202:
+ default:
+ switch {
+ case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`):
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+
+ var out sdkerrors.V2ErrorResponse
+ if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil {
+ return nil, err
+ }
+
+ return nil, &out
+ default:
+ rawBody, err := utils.ConsumeRawBody(httpRes)
+ if err != nil {
+ return nil, err
+ }
+ return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes)
+ }
+ }
+
+ return res, nil
+
+}
diff --git a/internal/bus/message.go b/pkg/events/message.go
similarity index 67%
rename from internal/bus/message.go
rename to pkg/events/message.go
index ff678856ff..9c47d81fcd 100644
--- a/internal/bus/message.go
+++ b/pkg/events/message.go
@@ -1,11 +1,10 @@
-package bus
+package events
import (
"github.com/formancehq/go-libs/v3/metadata"
"github.com/formancehq/go-libs/v3/publish"
"github.com/formancehq/go-libs/v3/time"
ledger "github.com/formancehq/ledger/internal"
- "github.com/formancehq/ledger/pkg/events"
)
type CommittedTransactions struct {
@@ -14,12 +13,12 @@ type CommittedTransactions struct {
AccountMetadata map[string]metadata.Metadata `json:"accountMetadata"`
}
-func newEventCommittedTransactions(txs CommittedTransactions) publish.EventMessage {
+func NewEventCommittedTransactions(txs CommittedTransactions) publish.EventMessage {
return publish.EventMessage{
Date: time.Now().Time,
- App: events.EventApp,
- Version: events.EventVersion,
- Type: events.EventTypeCommittedTransactions,
+ App: EventApp,
+ Version: EventVersion,
+ Type: EventTypeCommittedTransactions,
Payload: txs,
}
}
@@ -31,12 +30,12 @@ type SavedMetadata struct {
Metadata metadata.Metadata `json:"metadata"`
}
-func newEventSavedMetadata(savedMetadata SavedMetadata) publish.EventMessage {
+func NewEventSavedMetadata(savedMetadata SavedMetadata) publish.EventMessage {
return publish.EventMessage{
Date: time.Now().Time,
- App: events.EventApp,
- Version: events.EventVersion,
- Type: events.EventTypeSavedMetadata,
+ App: EventApp,
+ Version: EventVersion,
+ Type: EventTypeSavedMetadata,
Payload: savedMetadata,
}
}
@@ -47,12 +46,12 @@ type RevertedTransaction struct {
RevertTransaction ledger.Transaction `json:"revertTransaction"`
}
-func newEventRevertedTransaction(revertedTransaction RevertedTransaction) publish.EventMessage {
+func NewEventRevertedTransaction(revertedTransaction RevertedTransaction) publish.EventMessage {
return publish.EventMessage{
Date: time.Now().Time,
- App: events.EventApp,
- Version: events.EventVersion,
- Type: events.EventTypeRevertedTransaction,
+ App: EventApp,
+ Version: EventVersion,
+ Type: EventTypeRevertedTransaction,
Payload: revertedTransaction,
}
}
@@ -64,12 +63,12 @@ type DeletedMetadata struct {
Key string `json:"key"`
}
-func newEventDeletedMetadata(deletedMetadata DeletedMetadata) publish.EventMessage {
+func NewEventDeletedMetadata(deletedMetadata DeletedMetadata) publish.EventMessage {
return publish.EventMessage{
Date: time.Now().Time,
- App: events.EventApp,
- Version: events.EventVersion,
- Type: events.EventTypeDeletedMetadata,
+ App: EventApp,
+ Version: EventVersion,
+ Type: EventTypeDeletedMetadata,
Payload: deletedMetadata,
}
}
diff --git a/pkg/generate/generator.go b/pkg/generate/generator.go
index 1ccc96ce05..26e72a5206 100644
--- a/pkg/generate/generator.go
+++ b/pkg/generate/generator.go
@@ -149,7 +149,7 @@ func (r Action) Apply(ctx context.Context, client *client.V2, l string) ([]compo
return nil, fmt.Errorf("creating transaction: %w", err)
}
- if response.HTTPMeta.Response.StatusCode == http.StatusBadRequest {
+ if response.HTTPMeta.Response.StatusCode == http.StatusBadRequest && response.V2BulkResponse.ErrorCode != nil {
return nil, fmt.Errorf(
"unexpected error: %s [%s]",
*response.V2BulkResponse.ErrorMessage,
diff --git a/pkg/generate/set.go b/pkg/generate/set.go
index 619fb45d82..e2230d60cb 100644
--- a/pkg/generate/set.go
+++ b/pkg/generate/set.go
@@ -44,7 +44,7 @@ func (s *GeneratorSet) Run(ctx context.Context) error {
for {
logging.FromContext(ctx).Debugf("Run iteration %d/%d", vu, iteration)
- action, err := generator.Next(vu)
+ action, err := generator.Next(iteration)
if err != nil {
return fmt.Errorf("iteration %d/%d failed: %w", vu, iteration, err)
}
diff --git a/pkg/testserver/ginkgo/helpers.go b/pkg/testserver/ginkgo/helpers.go
index 9a0453f05a..2f0ce34f47 100644
--- a/pkg/testserver/ginkgo/helpers.go
+++ b/pkg/testserver/ginkgo/helpers.go
@@ -23,8 +23,10 @@ func DeferTestWorker(postgresConnectionOptions *deferred.Deferred[bunconnect.Con
cmd.NewRootCommand,
append([]testservice.Option{
testservice.WithInstruments(
+ testservice.GRPCServerInstrumentation(),
testservice.AppendArgsInstrumentation("worker"),
testservice.PostgresInstrumentation(postgresConnectionOptions),
+ testserver.GRPCAddressInstrumentation(":0"),
),
}, options...)...,
)
diff --git a/pkg/testserver/replication_driver.go b/pkg/testserver/replication_driver.go
new file mode 100644
index 0000000000..24b7a36d00
--- /dev/null
+++ b/pkg/testserver/replication_driver.go
@@ -0,0 +1,13 @@
+package testserver
+
+import (
+ "context"
+ "github.com/formancehq/ledger/internal/replication/drivers"
+)
+
+type Driver interface {
+ Config() map[string]any
+ Name() string
+ ReadMessages(ctx context.Context) ([]drivers.LogWithLedger, error)
+ Clear(ctx context.Context) error
+}
diff --git a/pkg/testserver/replication_driver_clickhouse.go b/pkg/testserver/replication_driver_clickhouse.go
new file mode 100644
index 0000000000..f626f82376
--- /dev/null
+++ b/pkg/testserver/replication_driver_clickhouse.go
@@ -0,0 +1,95 @@
+package testserver
+
+import (
+ "context"
+ "github.com/ClickHouse/clickhouse-go/v2/lib/driver"
+ "github.com/formancehq/go-libs/v3/logging"
+ "github.com/formancehq/go-libs/v3/pointer"
+ ledger "github.com/formancehq/ledger/internal"
+ "github.com/formancehq/ledger/internal/replication/drivers"
+ clickhousedriver "github.com/formancehq/ledger/internal/replication/drivers/clickhouse"
+ "github.com/pkg/errors"
+ "sync"
+ "testing"
+)
+
+type ClickhouseDriver struct {
+ client driver.Conn
+ dsn string
+ mu sync.Mutex
+ logger logging.Logger
+}
+
+func (h *ClickhouseDriver) initClient() error {
+ h.mu.Lock()
+ defer h.mu.Unlock()
+
+ if h.client == nil {
+ var err error
+ h.client, err = clickhousedriver.OpenDB(h.logger, h.dsn, testing.Verbose())
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (h *ClickhouseDriver) Clear(ctx context.Context) error {
+ if err := h.initClient(); err != nil {
+ return err
+ }
+ return h.client.Exec(ctx, "delete from logs where true")
+}
+
+func (h *ClickhouseDriver) ReadMessages(ctx context.Context) ([]drivers.LogWithLedger, error) {
+ if err := h.initClient(); err != nil {
+ return nil, err
+ }
+
+ rows, err := h.client.Query(ctx, "select ledger, id, type, date, toJSONString(data) from logs final")
+ if err != nil {
+ return nil, err
+ }
+
+ ret := make([]drivers.LogWithLedger, 0)
+ for rows.Next() {
+ var (
+ payload string
+ id int64
+ )
+ newLog := drivers.LogWithLedger{}
+ if err := rows.Scan(&newLog.Ledger, &id, &newLog.Type, &newLog.Date, &payload); err != nil {
+ return nil, errors.Wrap(err, "scanning data from database")
+ }
+ newLog.ID = pointer.For(uint64(id))
+
+ newLog.Data, err = ledger.HydrateLog(newLog.Type, []byte(payload))
+ if err != nil {
+ return nil, errors.Wrap(err, "hydrating log data")
+ }
+
+ ret = append(ret, newLog)
+ }
+
+ return ret, nil
+}
+
+func (h *ClickhouseDriver) Config() map[string]any {
+ return map[string]any{
+ "dsn": h.dsn,
+ }
+}
+
+func (h *ClickhouseDriver) Name() string {
+ return "clickhouse"
+}
+
+var _ Driver = &ClickhouseDriver{}
+
+func NewClickhouseDriver(logger logging.Logger, dsn string) Driver {
+ return &ClickhouseDriver{
+ dsn: dsn,
+ logger: logger,
+ }
+}
diff --git a/pkg/testserver/replication_driver_elastic.go b/pkg/testserver/replication_driver_elastic.go
new file mode 100644
index 0000000000..e3e9a355f8
--- /dev/null
+++ b/pkg/testserver/replication_driver_elastic.go
@@ -0,0 +1,71 @@
+package testserver
+
+import (
+ "context"
+ "encoding/json"
+ "github.com/formancehq/go-libs/v3/collectionutils"
+ "github.com/formancehq/ledger/internal/replication/drivers"
+ "github.com/formancehq/ledger/internal/replication/drivers/elasticsearch"
+ "github.com/olivere/elastic/v7"
+ "sync"
+)
+
+type ElasticDriver struct {
+ mu sync.Mutex
+ endpoint string
+ client *elastic.Client
+}
+
+func (h *ElasticDriver) Clear(ctx context.Context) error {
+ _, err := h.client.Delete().Index(elasticsearch.DefaultIndex).Do(ctx)
+ return err
+}
+
+func (h *ElasticDriver) ReadMessages(ctx context.Context) ([]drivers.LogWithLedger, error) {
+
+ h.mu.Lock()
+ defer h.mu.Unlock()
+
+ if h.client == nil {
+ var err error
+ h.client, err = elastic.NewClient(elastic.SetURL(h.endpoint))
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ response, err := h.client.
+ Search(elasticsearch.DefaultIndex).
+ Size(1000).
+ Do(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ return collectionutils.Map(response.Hits.Hits, func(from *elastic.SearchHit) drivers.LogWithLedger {
+ ret := drivers.LogWithLedger{}
+ if err := json.Unmarshal(from.Source, &ret); err != nil {
+ panic(err)
+ }
+
+ return ret
+ }), nil
+}
+
+func (h *ElasticDriver) Config() map[string]any {
+ return map[string]any{
+ "endpoint": h.endpoint,
+ }
+}
+
+func (h *ElasticDriver) Name() string {
+ return "elasticsearch"
+}
+
+var _ Driver = &ElasticDriver{}
+
+func NewElasticDriver(endpoint string) Driver {
+ return &ElasticDriver{
+ endpoint: endpoint,
+ }
+}
diff --git a/pkg/testserver/replication_driver_http.go b/pkg/testserver/replication_driver_http.go
new file mode 100644
index 0000000000..f9da56a14b
--- /dev/null
+++ b/pkg/testserver/replication_driver_http.go
@@ -0,0 +1,83 @@
+package testserver
+
+import (
+ "context"
+ "encoding/json"
+ "github.com/formancehq/ledger/internal/replication/drivers"
+ "github.com/stretchr/testify/require"
+ "net/http"
+ "net/http/httptest"
+ "sync"
+)
+
+type HTTPDriver struct {
+ srv *httptest.Server
+ collector *Collector
+}
+
+func (h *HTTPDriver) Clear(_ context.Context) error {
+ h.collector.messages = nil
+ return nil
+}
+
+func (h *HTTPDriver) Config() map[string]any {
+ return map[string]any{
+ "url": h.srv.URL,
+ }
+}
+
+func (h *HTTPDriver) Name() string {
+ return "http"
+}
+
+func (h *HTTPDriver) ReadMessages(_ context.Context) ([]drivers.LogWithLedger, error) {
+ h.collector.mu.Lock()
+ defer h.collector.mu.Unlock()
+
+ return h.collector.messages[:], nil
+}
+
+var _ Driver = &HTTPDriver{}
+
+func NewHTTPDriver(t interface {
+ require.TestingT
+ Cleanup(func())
+}, collector *Collector) Driver {
+ ret := &HTTPDriver{
+ collector: collector,
+ }
+
+ ret.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ newMessages := make([]drivers.LogWithLedger, 0)
+ require.NoError(t, json.NewDecoder(r.Body).Decode(&newMessages))
+
+ ret.collector.mu.Lock()
+ defer ret.collector.mu.Unlock()
+
+ for _, message := range newMessages {
+ exists := false
+ for _, existingMessage := range ret.collector.messages {
+ if existingMessage.ID == message.ID {
+ exists = true
+ break
+ }
+ }
+ if !exists {
+ ret.collector.messages = append(ret.collector.messages, message)
+ }
+ }
+
+ }))
+ t.Cleanup(ret.srv.Close)
+
+ return ret
+}
+
+type Collector struct {
+ mu sync.Mutex
+ messages []drivers.LogWithLedger
+}
+
+func NewCollector() *Collector {
+ return &Collector{}
+}
diff --git a/pkg/testserver/server.go b/pkg/testserver/server.go
index b46b1d1d3b..16ddddbc36 100644
--- a/pkg/testserver/server.go
+++ b/pkg/testserver/server.go
@@ -7,6 +7,7 @@ import (
"github.com/formancehq/go-libs/v3/testing/deferred"
"github.com/formancehq/go-libs/v3/testing/testservice"
"github.com/formancehq/ledger/cmd"
+ "time"
)
func GetTestServerOptions(postgresConnectionOptions *deferred.Deferred[bunconnect.ConnectionOptions]) testservice.Option {
@@ -33,6 +34,20 @@ func ExperimentalFeaturesInstrumentation() testservice.InstrumentationFunc {
}
}
+func ExperimentalExportersInstrumentation() testservice.InstrumentationFunc {
+ return func(ctx context.Context, runConfiguration *testservice.RunConfiguration) error {
+ runConfiguration.AppendArgs("--" + cmd.ExperimentalExporters)
+ return nil
+ }
+}
+
+func ExperimentalEnableWorker() testservice.InstrumentationFunc {
+ return func(ctx context.Context, runConfiguration *testservice.RunConfiguration) error {
+ runConfiguration.AppendArgs("--" + cmd.WorkerEnabledFlag)
+ return nil
+ }
+}
+
func ExperimentalNumscriptRewriteInstrumentation() testservice.InstrumentationFunc {
return func(ctx context.Context, runConfiguration *testservice.RunConfiguration) error {
runConfiguration.AppendArgs("--" + cmd.NumscriptInterpreterFlag)
@@ -53,3 +68,35 @@ func DefaultPageSizeInstrumentation(size uint64) testservice.InstrumentationFunc
return nil
}
}
+
+func ExperimentalPipelinesPushRetryPeriodInstrumentation(duration time.Duration) testservice.InstrumentationFunc {
+ return func(ctx context.Context, runConfiguration *testservice.RunConfiguration) error {
+ runConfiguration.AppendArgs("--"+cmd.WorkerPipelinesPushRetryPeriodFlag, fmt.Sprint(duration))
+ return nil
+ }
+}
+
+func ExperimentalPipelinesPullIntervalInstrumentation(duration time.Duration) testservice.InstrumentationFunc {
+ return func(ctx context.Context, runConfiguration *testservice.RunConfiguration) error {
+ runConfiguration.AppendArgs("--"+cmd.WorkerPipelinesPullIntervalFlag, fmt.Sprint(duration))
+ return nil
+ }
+}
+
+func GRPCAddressInstrumentation(addr string) testservice.InstrumentationFunc {
+ return func(ctx context.Context, runConfiguration *testservice.RunConfiguration) error {
+ runConfiguration.AppendArgs("--"+cmd.WorkerGRPCAddressFlag, addr)
+ return nil
+ }
+}
+
+func WorkerAddressInstrumentation(addr *deferred.Deferred[string]) testservice.InstrumentationFunc {
+ return func(ctx context.Context, runConfiguration *testservice.RunConfiguration) error {
+ address, err := addr.Wait(ctx)
+ if err != nil {
+ return fmt.Errorf("waiting for worker address: %w", err)
+ }
+ runConfiguration.AppendArgs("--"+cmd.WorkerGRPCAddressFlag, address)
+ return nil
+ }
+}
diff --git a/test/e2e/api_accounts_list_test.go b/test/e2e/api_accounts_list_test.go
index ff99003b7e..6c7582c373 100644
--- a/test/e2e/api_accounts_list_test.go
+++ b/test/e2e/api_accounts_list_test.go
@@ -13,7 +13,7 @@ import (
"github.com/formancehq/ledger/pkg/client/models/components"
"github.com/formancehq/ledger/pkg/client/models/operations"
. "github.com/formancehq/ledger/pkg/testserver"
- "github.com/formancehq/ledger/pkg/testserver/ginkgo"
+ . "github.com/formancehq/ledger/pkg/testserver/ginkgo"
"math/big"
"sort"
"time"
@@ -31,7 +31,7 @@ var _ = Context("Ledger accounts list API tests", func() {
ctx = logging.TestingContext()
)
- testServer := ginkgo.DeferTestServer(
+ testServer := DeferTestServer(
DeferMap(db, (*pgtesting.Database).ConnectionOptions),
testservice.WithInstruments(
testservice.NatsInstrumentation(DeferMap(natsServer, (*natstesting.NatsServer).ClientURL)),
diff --git a/test/e2e/api_bulk_test.go b/test/e2e/api_bulk_test.go
index acb4633899..91a18ca8c0 100644
--- a/test/e2e/api_bulk_test.go
+++ b/test/e2e/api_bulk_test.go
@@ -15,7 +15,6 @@ import (
"github.com/formancehq/go-libs/v3/testing/platform/pgtesting"
"github.com/formancehq/go-libs/v3/testing/testservice"
ledger "github.com/formancehq/ledger/internal"
- "github.com/formancehq/ledger/internal/bus"
ledgerevents "github.com/formancehq/ledger/pkg/events"
. "github.com/formancehq/ledger/pkg/testserver/ginkgo"
"github.com/nats-io/nats.go"
@@ -358,7 +357,7 @@ var _ = Context("Ledger engine tests", func() {
})
By("Should have sent one event", func() {
- Eventually(events).Should(Receive(Event(ledgerevents.EventTypeCommittedTransactions, WithPayload(bus.CommittedTransactions{
+ Eventually(events).Should(Receive(Event(ledgerevents.EventTypeCommittedTransactions, WithPayload(ledgerevents.CommittedTransactions{
Ledger: "default",
Transactions: []ledger.Transaction{ConvertSDKTxToCoreTX(&bulkResponse.V2BulkResponse.Data[0].V2BulkElementResultCreateTransaction.Data)},
AccountMetadata: ledger.AccountMetadata{},
diff --git a/test/e2e/api_connectors_create_test.go b/test/e2e/api_connectors_create_test.go
new file mode 100644
index 0000000000..0c3aa761e8
--- /dev/null
+++ b/test/e2e/api_connectors_create_test.go
@@ -0,0 +1,70 @@
+//go:build it
+
+package test_suite
+
+import (
+ "github.com/formancehq/go-libs/v3/logging"
+ . "github.com/formancehq/go-libs/v3/testing/deferred/ginkgo"
+ "github.com/formancehq/go-libs/v3/testing/platform/natstesting"
+ "github.com/formancehq/go-libs/v3/testing/platform/pgtesting"
+ "github.com/formancehq/go-libs/v3/testing/testservice"
+ "github.com/formancehq/ledger/pkg/client/models/components"
+ "github.com/formancehq/ledger/pkg/client/models/operations"
+ . "github.com/formancehq/ledger/pkg/testserver"
+ . "github.com/formancehq/ledger/pkg/testserver/ginkgo"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Context("Exporters API tests", func() {
+ var (
+ db = UseTemplatedDatabase()
+ ctx = logging.TestingContext()
+ )
+
+ testServer := DeferTestServer(
+ DeferMap(db, (*pgtesting.Database).ConnectionOptions),
+ testservice.WithInstruments(
+ testservice.NatsInstrumentation(DeferMap(natsServer, (*natstesting.NatsServer).ClientURL)),
+ testservice.DebugInstrumentation(debug),
+ testservice.OutputInstrumentation(GinkgoWriter),
+ ExperimentalFeaturesInstrumentation(),
+ ExperimentalExportersInstrumentation(),
+ ExperimentalEnableWorker(),
+ ),
+ testservice.WithLogger(GinkgoT()),
+ )
+
+ When("creating a new exporter", func() {
+ var (
+ createExporterRequest components.V2CreateExporterRequest
+ createExporterResponse *operations.V2CreateExporterResponse
+ err error
+ )
+ BeforeEach(func() {
+ createExporterRequest = components.V2CreateExporterRequest{
+ Driver: "clickhouse",
+ Config: map[string]any{
+ "dsn": "clickhouse://localhost:9000",
+ },
+ }
+ })
+ BeforeEach(func(specContext SpecContext) {
+ createExporterResponse, err = Wait(specContext, DeferClient(testServer)).Ledger.V2.CreateExporter(ctx, createExporterRequest)
+ })
+ It("should be ok", func() {
+ Expect(err).To(BeNil())
+ })
+ Context("then deleting it", func() {
+ BeforeEach(func(specContext SpecContext) {
+ Expect(err).To(BeNil())
+ _, err = Wait(specContext, DeferClient(testServer)).Ledger.V2.DeleteExporter(ctx, operations.V2DeleteExporterRequest{
+ ExporterID: createExporterResponse.V2CreateExporterResponse.Data.ID,
+ })
+ })
+ It("Should be ok", func() {
+ Expect(err).To(BeNil())
+ })
+ })
+ })
+})
diff --git a/test/e2e/api_pipelines_create_test.go b/test/e2e/api_pipelines_create_test.go
new file mode 100644
index 0000000000..216b8bd96b
--- /dev/null
+++ b/test/e2e/api_pipelines_create_test.go
@@ -0,0 +1,313 @@
+//go:build it
+
+package test_suite
+
+import (
+ "context"
+ "fmt"
+ "github.com/formancehq/go-libs/v3/grpcserver"
+ "github.com/formancehq/go-libs/v3/logging"
+ "github.com/formancehq/go-libs/v3/testing/deferred"
+ . "github.com/formancehq/go-libs/v3/testing/deferred/ginkgo"
+ "github.com/formancehq/go-libs/v3/testing/platform/clickhousetesting"
+ "github.com/formancehq/go-libs/v3/testing/platform/natstesting"
+ "github.com/formancehq/go-libs/v3/testing/platform/pgtesting"
+ "github.com/formancehq/go-libs/v3/testing/testservice"
+ "github.com/formancehq/ledger/internal/replication/drivers"
+ "github.com/formancehq/ledger/pkg/client/models/components"
+ "github.com/formancehq/ledger/pkg/client/models/operations"
+ . "github.com/formancehq/ledger/pkg/testserver"
+ . "github.com/formancehq/ledger/pkg/testserver/ginkgo"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ "math/big"
+ "os"
+ "strings"
+ "time"
+)
+
+var (
+ defaultEnabledReplicationDrivers = []string{"http", "clickhouse"}
+)
+
+func enabledReplicationDrivers() []string {
+ fromEnv := os.Getenv("REPLICATION_DRIVERS")
+ if fromEnv == "" {
+ return defaultEnabledReplicationDrivers
+ }
+ return strings.Split(fromEnv, ",")
+}
+
+func withDriver(name string, exporterFactory func() Driver, fn func(p *deferred.Deferred[Driver])) {
+ Context(fmt.Sprintf("with driver '%s'", name), func() {
+ ret := deferred.New[Driver]()
+ BeforeEach(func() {
+ ret.Reset()
+ ret.SetValue(exporterFactory())
+ })
+ fn(ret)
+ })
+}
+
+// driversSetup allow to define a ginkgo node factory function for each exporter
+// This allows to configure the environment for the exporter
+var driversSetup = map[string]func(func(d *deferred.Deferred[Driver])){
+ "http": func(fn func(d *deferred.Deferred[Driver])) {
+ withDriver("http", func() Driver {
+ return NewHTTPDriver(GinkgoT(), NewCollector())
+ }, fn)
+ },
+ "clickhouse": func(fn func(d *deferred.Deferred[Driver])) {
+ clickhousetesting.WithNewDatabase(clickhouseServer, func(db *deferred.Deferred[*clickhousetesting.Database]) {
+ withDriver("clickhouse", func() Driver {
+ return NewClickhouseDriver(logger, db.GetValue().ConnString())
+ }, fn)
+ })
+ },
+}
+
+var _ = Context("Pipelines API tests", func() {
+ var (
+ db = UseTemplatedDatabase()
+ ctx = logging.TestingContext()
+ )
+
+ Context("With single instance and worker enabled", func() {
+ testServer := DeferTestServer(
+ DeferMap(db, (*pgtesting.Database).ConnectionOptions),
+ testservice.WithInstruments(
+ testservice.NatsInstrumentation(DeferMap(natsServer, (*natstesting.NatsServer).ClientURL)),
+ testservice.DebugInstrumentation(debug),
+ testservice.OutputInstrumentation(GinkgoWriter),
+ ExperimentalFeaturesInstrumentation(),
+ ExperimentalExportersInstrumentation(),
+ ExperimentalEnableWorker(),
+ ExperimentalPipelinesPullIntervalInstrumentation(100*time.Millisecond),
+ ExperimentalPipelinesPushRetryPeriodInstrumentation(100*time.Millisecond),
+ ),
+ testservice.WithLogger(GinkgoT()),
+ )
+ runPipelinesTests(ctx, testServer)
+ })
+
+ Context("With a single instance, and worker on a separate process", func() {
+ connectionOptions := DeferMap(db, (*pgtesting.Database).ConnectionOptions)
+
+ worker := DeferTestWorker(
+ connectionOptions,
+ testservice.WithInstruments(
+ testservice.DebugInstrumentation(debug),
+ testservice.OutputInstrumentation(GinkgoWriter),
+ ExperimentalFeaturesInstrumentation(),
+ ExperimentalExportersInstrumentation(),
+ ExperimentalPipelinesPullIntervalInstrumentation(100*time.Millisecond),
+ ExperimentalPipelinesPushRetryPeriodInstrumentation(100*time.Millisecond),
+ ),
+ testservice.WithLogger(GinkgoT()),
+ )
+
+ testServer := DeferTestServer(
+ connectionOptions,
+ testservice.WithInstruments(
+ testservice.NatsInstrumentation(DeferMap(natsServer, (*natstesting.NatsServer).ClientURL)),
+ testservice.DebugInstrumentation(debug),
+ testservice.OutputInstrumentation(GinkgoWriter),
+ ExperimentalFeaturesInstrumentation(),
+ ExperimentalExportersInstrumentation(),
+ WorkerAddressInstrumentation(DeferMap(worker, func(from *testservice.Service) string {
+ return grpcserver.Address(from.GetContext())
+ })),
+ ),
+ testservice.WithLogger(GinkgoT()),
+ )
+
+ runPipelinesTests(ctx, testServer)
+ })
+})
+
+func runPipelinesTests(ctx context.Context, testServer *deferred.Deferred[*testservice.Service]) {
+ for _, driverName := range enabledReplicationDrivers() {
+ setup, ok := driversSetup[driverName]
+ if !ok {
+ Fail(fmt.Sprintf("Driver '%s' not exists", driverName))
+ }
+ setup(func(driver *deferred.Deferred[Driver]) {
+ When("creating a new exporter", func() {
+ var (
+ createExporterRequest components.V2CreateExporterRequest
+ createExporterResponse *operations.V2CreateExporterResponse
+ err error
+ )
+ BeforeEach(func() {
+ config := driver.GetValue().Config()
+ // Set batching to 1 to make testing easier
+ config["batching"] = map[string]any{
+ "maxItems": 1,
+ }
+ createExporterRequest = components.V2CreateExporterRequest{
+ Driver: driverName,
+ Config: config,
+ }
+ })
+ BeforeEach(func(specContext SpecContext) {
+ createExporterResponse, err = Wait(specContext, DeferClient(testServer)).Ledger.V2.CreateExporter(ctx, createExporterRequest)
+ })
+ It("should be ok", func() {
+ Expect(err).To(BeNil())
+ })
+ Context("then creating a ledger and a pipeline", func() {
+ var (
+ createPipelineResponse *operations.V2CreatePipelineResponse
+ )
+ BeforeEach(func(specContext SpecContext) {
+ Expect(err).To(BeNil())
+ _, err = Wait(specContext, DeferClient(testServer)).Ledger.V2.CreateLedger(ctx, operations.V2CreateLedgerRequest{
+ Ledger: "default",
+ })
+ Expect(err).To(BeNil())
+
+ createPipelineResponse, err = Wait(specContext, DeferClient(testServer)).Ledger.V2.CreatePipeline(ctx, operations.V2CreatePipelineRequest{
+ Ledger: "default",
+ V2CreatePipelineRequest: &components.V2CreatePipelineRequest{
+ ExporterID: createExporterResponse.V2CreateExporterResponse.Data.ID,
+ },
+ })
+ })
+ It("Should be ok", func() {
+ Expect(err).To(BeNil())
+ })
+ Context("then deleting it", func() {
+ BeforeEach(func(specContext SpecContext) {
+ Expect(err).To(Succeed())
+ _, err = Wait(specContext, DeferClient(testServer)).Ledger.V2.DeletePipeline(ctx, operations.V2DeletePipelineRequest{
+ Ledger: "default",
+ PipelineID: createPipelineResponse.V2CreatePipelineResponse.Data.ID,
+ })
+ })
+ It("Should be ok", func() {
+ Expect(err).To(BeNil())
+ })
+ })
+ Context("then creating a transaction", func() {
+ BeforeEach(func(specContext SpecContext) {
+ _, err := Wait(specContext, DeferClient(testServer)).Ledger.V2.CreateTransaction(ctx, operations.V2CreateTransactionRequest{
+ Ledger: "default",
+ V2PostTransaction: components.V2PostTransaction{
+ Postings: []components.V2Posting{{
+ Amount: big.NewInt(100),
+ Asset: "USD",
+ Destination: "bank",
+ Source: "world",
+ }},
+ },
+ })
+ Expect(err).To(Succeed())
+ })
+ shouldHaveMessage := func(i int) {
+ GinkgoHelper()
+
+ Eventually(func(g Gomega) []drivers.LogWithLedger {
+ messages, err := driver.GetValue().ReadMessages(ctx)
+ g.Expect(err).To(BeNil())
+
+ return messages
+ }).
+ WithTimeout(10 * time.Second).
+ WithPolling(100 * time.Millisecond).
+ Should(HaveLen(i))
+ }
+ It("should be forwarded to the driver", func() {
+ shouldHaveMessage(1)
+ })
+ Context("then resetting the pipeline", func() {
+ BeforeEach(func(specContext SpecContext) {
+ shouldHaveMessage(1)
+ Expect(driver.GetValue().Clear(ctx)).To(BeNil())
+ shouldHaveMessage(0)
+
+ _, err = Wait(specContext, DeferClient(testServer)).Ledger.V2.ResetPipeline(ctx, operations.V2ResetPipelineRequest{
+ Ledger: "default",
+ PipelineID: createPipelineResponse.V2CreatePipelineResponse.Data.ID,
+ })
+ Expect(err).To(BeNil())
+ })
+ It("should be forwarded again to the driver", func() {
+ shouldHaveMessage(1)
+ })
+ })
+ Context("then stopping the pipeline", func() {
+ BeforeEach(func(specContext SpecContext) {
+ shouldHaveMessage(1)
+
+ _, err = Wait(specContext, DeferClient(testServer)).Ledger.V2.StopPipeline(ctx, operations.V2StopPipelineRequest{
+ Ledger: "default",
+ PipelineID: createPipelineResponse.V2CreatePipelineResponse.Data.ID,
+ })
+
+ // As we don't actually have a way to ensure the pipeline is stopped
+ // todo: find a better way to check for the pipeline termination
+ <-time.After(500 * time.Millisecond)
+ })
+ It("should be ok", func() {
+ Expect(err).To(BeNil())
+ })
+ Context("then creating a new tx", func() {
+ BeforeEach(func(specContext SpecContext) {
+ Expect(err).To(BeNil())
+
+ _, err := Wait(specContext, DeferClient(testServer)).Ledger.V2.CreateTransaction(ctx, operations.V2CreateTransactionRequest{
+ Ledger: "default",
+ V2PostTransaction: components.V2PostTransaction{
+ Postings: []components.V2Posting{{
+ Amount: big.NewInt(100),
+ Asset: "USD",
+ Destination: "bank",
+ Source: "world",
+ }},
+ },
+ })
+ Expect(err).To(Succeed())
+ })
+ It("should not be exported", func() {
+ Consistently(func(g Gomega) []drivers.LogWithLedger {
+ messages, err := driver.GetValue().ReadMessages(ctx)
+ if err != nil {
+ return nil
+ }
+
+ return messages
+ }).
+ WithTimeout(time.Second).
+ WithPolling(100 * time.Millisecond).
+ Should(HaveLen(1))
+ })
+ Context("then restarting the pipeline", func() {
+ BeforeEach(func(specContext SpecContext) {
+ _, err = Wait(specContext, DeferClient(testServer)).Ledger.V2.StartPipeline(ctx, operations.V2StartPipelineRequest{
+ Ledger: "default",
+ PipelineID: createPipelineResponse.V2CreatePipelineResponse.Data.ID,
+ })
+ })
+ It("should be exported", func() {
+ Expect(err).To(BeNil())
+ Eventually(func(g Gomega) []drivers.LogWithLedger {
+ messages, err := driver.GetValue().ReadMessages(ctx)
+ if err != nil {
+ return nil
+ }
+
+ return messages
+ }).
+ WithTimeout(10 * time.Second).
+ WithPolling(100 * time.Millisecond).
+ Should(HaveLen(2))
+ })
+ })
+ })
+ })
+ })
+ })
+ })
+ })
+ }
+}
diff --git a/test/e2e/api_transactions_create_test.go b/test/e2e/api_transactions_create_test.go
index 5be4018c85..f7b1e3576a 100644
--- a/test/e2e/api_transactions_create_test.go
+++ b/test/e2e/api_transactions_create_test.go
@@ -15,7 +15,6 @@ import (
"github.com/formancehq/go-libs/v3/logging"
. "github.com/formancehq/go-libs/v3/testing/api"
ledger "github.com/formancehq/ledger/internal"
- "github.com/formancehq/ledger/internal/bus"
"github.com/formancehq/ledger/pkg/client/models/components"
"github.com/formancehq/ledger/pkg/client/models/operations"
. "github.com/formancehq/ledger/pkg/testserver"
@@ -279,7 +278,7 @@ var _ = Context("Ledger transactions create API tests", func() {
},
}))
By("should trigger a new event", func() {
- Eventually(events).Should(Receive(Event(ledgerevents.EventTypeCommittedTransactions, WithPayload(bus.CommittedTransactions{
+ Eventually(events).Should(Receive(Event(ledgerevents.EventTypeCommittedTransactions, WithPayload(ledgerevents.CommittedTransactions{
Ledger: "default",
Transactions: []ledger.Transaction{ConvertSDKTxToCoreTX(&rsp.V2CreateTransactionResponse.Data)},
AccountMetadata: ledger.AccountMetadata{},
diff --git a/test/e2e/api_transactions_revert_test.go b/test/e2e/api_transactions_revert_test.go
index e225822834..ca36ff21ac 100644
--- a/test/e2e/api_transactions_revert_test.go
+++ b/test/e2e/api_transactions_revert_test.go
@@ -9,7 +9,6 @@ import (
"github.com/formancehq/go-libs/v3/testing/testservice"
libtime "github.com/formancehq/go-libs/v3/time"
ledger "github.com/formancehq/ledger/internal"
- "github.com/formancehq/ledger/internal/bus"
. "github.com/formancehq/ledger/pkg/testserver/ginkgo"
"math/big"
"time"
@@ -145,7 +144,7 @@ var _ = Context("Ledger revert transactions API tests", func() {
Expect(err).To(Succeed())
})
It("should trigger a new event", func() {
- Eventually(events).Should(Receive(Event(ledgerevents.EventTypeRevertedTransaction, WithPayload(bus.RevertedTransaction{
+ Eventually(events).Should(Receive(Event(ledgerevents.EventTypeRevertedTransaction, WithPayload(ledgerevents.RevertedTransaction{
Ledger: "default",
RevertTransaction: ledger.Transaction{
ID: pointer.For(newTransaction.V2RevertTransactionResponse.Data.ID.Uint64()),
diff --git a/test/e2e/suite_test.go b/test/e2e/suite_test.go
index f8393957f9..fb8e038124 100644
--- a/test/e2e/suite_test.go
+++ b/test/e2e/suite_test.go
@@ -7,6 +7,7 @@ import (
"encoding/json"
"github.com/formancehq/go-libs/v3/bun/bunconnect"
"github.com/formancehq/go-libs/v3/testing/deferred"
+ "github.com/formancehq/go-libs/v3/testing/platform/clickhousetesting"
"github.com/formancehq/go-libs/v3/testing/platform/natstesting"
"github.com/formancehq/go-libs/v3/testing/testservice"
ledger "github.com/formancehq/ledger/internal"
@@ -15,6 +16,7 @@ import (
"github.com/nats-io/nats.go"
"github.com/uptrace/bun"
"os"
+ "slices"
"testing"
"github.com/formancehq/go-libs/v3/logging"
@@ -30,18 +32,20 @@ func Test(t *testing.T) {
}
var (
- dockerPool = deferred.New[*docker.Pool]()
- pgServer = deferred.New[*PostgresServer]()
- natsServer = deferred.New[*natstesting.NatsServer]()
- debug = os.Getenv("DEBUG") == "true"
- logger = logging.NewDefaultLogger(GinkgoWriter, debug, false, false)
+ dockerPool = deferred.New[*docker.Pool]()
+ pgServer = deferred.New[*PostgresServer]()
+ natsServer = deferred.New[*natstesting.NatsServer]()
+ clickhouseServer = deferred.New[*clickhousetesting.Server]()
+ debug = os.Getenv("DEBUG") == "true"
+ logger = logging.NewDefaultLogger(GinkgoWriter, debug, false, false)
DBTemplate = "dbtemplate"
)
type ParallelExecutionContext struct {
- PostgresServer *PostgresServer
- NatsServer *natstesting.NatsServer
+ PostgresServer *PostgresServer
+ NatsServer *natstesting.NatsServer
+ ClickhouseServer *clickhousetesting.Server
}
var _ = SynchronizedBeforeSuite(func(specContext SpecContext) []byte {
@@ -62,19 +66,24 @@ var _ = SynchronizedBeforeSuite(func(specContext SpecContext) []byte {
templateDatabase := ret.NewDatabase(GinkgoT(), WithName(DBTemplate))
+ By("Connecting to database...")
bunDB, err := bunconnect.OpenSQLDB(context.Background(), templateDatabase.ConnectionOptions())
Expect(err).To(BeNil())
+ By("Creating system schema")
err = system.Migrate(context.Background(), bunDB)
Expect(err).To(BeNil())
+ By("Creating default bucket")
// Initialize the _default bucket on the default database
// This way, we will be able to clone this database to speed up the tests
err = bucket.GetMigrator(bunDB, ledger.DefaultBucket).Up(context.Background())
Expect(err).To(BeNil())
+ By("Closing connection")
Expect(bunDB.Close()).To(BeNil())
+ By("Loaded")
return ret, nil
})
@@ -85,13 +94,32 @@ var _ = SynchronizedBeforeSuite(func(specContext SpecContext) []byte {
return ret, nil
})
+ if slices.Contains(enabledReplicationDrivers(), "clickhouse") {
+ clickhouseServer.LoadAsync(func() (*clickhousetesting.Server, error) {
+ By("Initializing clickhouse server")
+ return clickhousetesting.CreateServer(dockerPool.GetValue()), nil
+ })
+ } else {
+ clickhouseServer.SetValue(&clickhousetesting.Server{})
+ }
+
By("Waiting services alive")
- Expect(deferred.WaitContext(specContext, pgServer, natsServer)).To(BeNil())
+ By("Waiting PG")
+ _, err := pgServer.Wait(specContext)
+ Expect(err).To(BeNil())
+ By("Waiting nats")
+ _, err = natsServer.Wait(specContext)
+ Expect(err).To(BeNil())
+ By("Waiting clickhouse")
+ _, err = clickhouseServer.Wait(specContext)
+ Expect(err).To(BeNil())
+ //Expect(deferred.WaitContext(specContext, pgServer, natsServer, clickhouseServer)).To(BeNil())
By("All services ready.")
data, err := json.Marshal(ParallelExecutionContext{
- PostgresServer: pgServer.GetValue(),
- NatsServer: natsServer.GetValue(),
+ PostgresServer: pgServer.GetValue(),
+ NatsServer: natsServer.GetValue(),
+ ClickhouseServer: clickhouseServer.GetValue(),
})
Expect(err).To(BeNil())
@@ -109,6 +137,7 @@ var _ = SynchronizedBeforeSuite(func(specContext SpecContext) []byte {
pgServer.SetValue(pec.PostgresServer)
natsServer.SetValue(pec.NatsServer)
+ clickhouseServer.SetValue(pec.ClickhouseServer)
})
func UseTemplatedDatabase() *deferred.Deferred[*Database] {
diff --git a/tools/generator/Dockerfile b/tools/generator/Dockerfile
new file mode 100644
index 0000000000..f2fd80c73a
--- /dev/null
+++ b/tools/generator/Dockerfile
@@ -0,0 +1,20 @@
+FROM golang:1.24-alpine AS compiler
+WORKDIR /src
+COPY --from=root pkg pkg
+COPY --from=root internal internal
+COPY --from=root cmd cmd
+COPY --from=root go.* .
+COPY --from=root *.go .
+
+WORKDIR /src/tools/generator
+COPY go.* .
+RUN --mount=type=cache,target=$GOPATH go mod download
+COPY main.go .
+COPY cmd /src/tools/generator/cmd
+RUN --mount=type=cache,target=$GOPATH go build -o generator
+
+FROM alpine:3.21
+LABEL org.opencontainers.image.source=https://github.com/formancehq/ledger
+COPY --from=compiler /src/tools/generator/generator /bin/generator
+ENTRYPOINT ["/bin/generator"]
+CMD ["--help"]
\ No newline at end of file
diff --git a/tools/generator/Earthfile b/tools/generator/Earthfile
deleted file mode 100644
index ef4fff51e3..0000000000
--- a/tools/generator/Earthfile
+++ /dev/null
@@ -1,33 +0,0 @@
-VERSION 0.8
-PROJECT FormanceHQ/ledger
-
-IMPORT github.com/formancehq/earthly:tags/v0.19.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 (../..+sources/*) /src
- WORKDIR /src/tools/generator
- COPY --dir cmd examples .
- COPY go.* *.go .
- SAVE ARTIFACT /src
-
-compile:
- FROM +sources
- 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
\ No newline at end of file
diff --git a/tools/generator/cmd/root.go b/tools/generator/cmd/root.go
index 3825417726..30ae7b86df 100644
--- a/tools/generator/cmd/root.go
+++ b/tools/generator/cmd/root.go
@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"github.com/formancehq/go-libs/v3/logging"
+ "github.com/formancehq/go-libs/v3/service"
ledgerclient "github.com/formancehq/ledger/pkg/client"
"github.com/formancehq/ledger/pkg/client/models/components"
"github.com/formancehq/ledger/pkg/client/models/operations"
@@ -62,10 +63,7 @@ func init() {
}
func Execute() {
- err := rootCmd.Execute()
- if err != nil {
- os.Exit(1)
- }
+ service.Execute(rootCmd)
}
func run(cmd *cobra.Command, args []string) error {
diff --git a/tools/generator/go.mod b/tools/generator/go.mod
index 9a7e274a41..7726b230fc 100644
--- a/tools/generator/go.mod
+++ b/tools/generator/go.mod
@@ -9,9 +9,9 @@ replace github.com/formancehq/ledger => ../..
replace github.com/formancehq/ledger/pkg/client => ../../pkg/client
require (
- github.com/formancehq/go-libs/v3 v3.0.0-20250422113236-ec98813a1539
- github.com/formancehq/ledger v0.0.0-00010101000000-000000000000
- github.com/formancehq/ledger/pkg/client v0.0.0-00010101000000-000000000000
+ github.com/formancehq/go-libs/v3 v3.0.0-20250422121250-0689c5e8027f
+ github.com/formancehq/ledger v1.12.0
+ github.com/formancehq/ledger/pkg/client v0.0.0-20250522125118-07406c497fbe
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0
golang.org/x/oauth2 v0.30.0
@@ -26,9 +26,10 @@ require (
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/v5 v5.0.2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
- github.com/dop251/goja v0.0.0-20241009100908-5f46f2705ca3 // indirect
+ github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c // 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
@@ -37,8 +38,9 @@ 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.4+incompatible // indirect
- github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
+ github.com/google/pprof v0.0.0-20250501235452-c0086092b71a // indirect
github.com/google/uuid v1.6.0 // indirect
+ github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
@@ -48,6 +50,7 @@ require (
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/lib/pq v1.10.9 // indirect
github.com/lithammer/shortuuid/v3 v3.0.7 // indirect
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
github.com/mailru/easyjson v0.9.0 // indirect
@@ -57,8 +60,12 @@ require (
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.5.1 // indirect
+ github.com/riandyrn/otelchi v0.12.1 // 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.6 // indirect
+ github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
github.com/uptrace/bun v1.2.11 // indirect
github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 // indirect
@@ -68,19 +75,30 @@ require (
github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240816141633-0a40785b4f41 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
+ go.opentelemetry.io/contrib/propagators/b3 v1.35.0 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 // indirect
+ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 // indirect
go.opentelemetry.io/otel/log v0.12.2 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/otel/sdk v1.36.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect
+ go.opentelemetry.io/proto/otlp v1.6.0 // indirect
go.uber.org/dig v1.19.0 // indirect
go.uber.org/fx v1.24.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
- golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
+ golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
+ golang.org/x/net v0.40.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect
+ google.golang.org/grpc v1.72.1 // indirect
+ google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/tools/generator/go.sum b/tools/generator/go.sum
index 903aac1a5e..9ee1f90b88 100644
--- a/tools/generator/go.sum
+++ b/tools/generator/go.sum
@@ -4,6 +4,10 @@ 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-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/ClickHouse/ch-go v0.66.0 h1:hLslxxAVb2PHpbHr4n0d6aP8CEIpUYGMVT1Yj/Q5Img=
+github.com/ClickHouse/ch-go v0.66.0/go.mod h1:noiHWyLMJAZ5wYuq3R/K0TcRhrNA8h7o1AqHX0klEhM=
+github.com/ClickHouse/clickhouse-go/v2 v2.35.0 h1:ZMLZqxu+NiW55f4JS32kzyEbMb7CthGn3ziCcULOvSE=
+github.com/ClickHouse/clickhouse-go/v2 v2.35.0/go.mod h1:O2FFT/rugdpGEW2VKyEGyMUWyQU0ahmenY9/emxLPxs=
github.com/IBM/sarama v1.45.1 h1:nY30XqYpqyXOXSNoe2XCgjj9jklGM1Ye94ierUb1jQ0=
github.com/IBM/sarama v1.45.1/go.mod h1:qifDhA3VWSrQ1TjSMyxDl3nYL3oX2C83u+G6L79sq4w=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
@@ -24,6 +28,8 @@ 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/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
+github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
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=
@@ -87,8 +93,8 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj
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/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c h1:mxWGS0YyquJ/ikZOjSrRjjFIbUqIP9ojyYQ+QZTU3Rg=
+github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c/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=
@@ -104,8 +110,8 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
-github.com/formancehq/go-libs/v3 v3.0.0-20250422113236-ec98813a1539 h1:6kUkmD2GiZGB7TDpGaPas2ipaAKqP/os3PVk4XFVrpI=
-github.com/formancehq/go-libs/v3 v3.0.0-20250422113236-ec98813a1539/go.mod h1:mRr5/y0I64iJ4I+BXNkUy49izwrh3SA5L+MTWD1d/7Q=
+github.com/formancehq/go-libs/v3 v3.0.0-20250422121250-0689c5e8027f h1:/t3fKq/iXwo1KtFLE+2jtK3Ktm82OHqf6ZhuzHZWOos=
+github.com/formancehq/go-libs/v3 v3.0.0-20250422121250-0689c5e8027f/go.mod h1:T8GpmWRmRrS7Iy6tiz1gHsWMBUEOkCAIVhoXdJFM6Ns=
github.com/formancehq/numscript v0.0.16 h1:kNNpPTmTvhRUrMXonZPMwUXUpJ06Io1WwC56Yf3nr1E=
github.com/formancehq/numscript v0.0.16/go.mod h1:8WhBIqcK6zu27njxy7ZG7CaDX0MHtI9qF9Ggfj07wfU=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
@@ -124,6 +130,10 @@ 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-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
+github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
+github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
+github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
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=
@@ -141,12 +151,14 @@ 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/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
-github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
-github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
+github.com/google/pprof v0.0.0-20250501235452-c0086092b71a h1:rDA3FfmxwXR+BVKKdz55WwMJ1pD2hJQNW31d+l3mPk4=
+github.com/google/pprof v0.0.0-20250501235452-c0086092b71a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
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=
@@ -198,12 +210,16 @@ 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 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
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/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
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=
@@ -222,10 +238,12 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
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/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
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/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
-github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
+github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
+github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM=
@@ -240,6 +258,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/olivere/elastic/v7 v7.0.32 h1:R7CXvbu8Eq+WlsLgxmKVKPox0oOwAE/2T9Si5BnvK6E=
+github.com/olivere/elastic/v7 v7.0.32/go.mod h1:c7PVmLe3Fxq77PIfY/bZmxY/TAamBhCzZ8xDOE09a9k=
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -250,6 +270,8 @@ github.com/opencontainers/runc v1.2.3 h1:fxE7amCzfZflJO2lHXf4y/y8M1BoAqp+FVmG19o
github.com/opencontainers/runc v1.2.3/go.mod h1:nSxcWUydXrsBZVYNSkTjoQ/N6rcyTtn+1SD5D4+kRIM=
github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw=
github.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE=
+github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU=
+github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
@@ -277,12 +299,18 @@ 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/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
+github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
+github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/shirou/gopsutil/v4 v4.25.4 h1:cdtFO363VEOOFrUCjZRh4XVJkb548lyF0q0uTeMqYPw=
github.com/shirou/gopsutil/v4 v4.25.4/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA=
+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/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
+github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
@@ -300,9 +328,15 @@ github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqj
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=
+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.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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
@@ -361,6 +395,8 @@ github.com/zitadel/oidc/v2 v2.12.2 h1:3kpckg4rurgw7w7aLJrq7yvRxb2pkNOtD08RH42vPE
github.com/zitadel/oidc/v2 v2.12.2/go.mod h1:vhP26g1g4YVntcTi0amMYW3tJuid70nxqxf+kb6XKgg=
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/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
go.opentelemetry.io/contrib/instrumentation/host v0.60.0 h1:LD6TMRg2hfNzkMD36Pq0jeYBcSP9W0aJt41Zmje43Ig=
go.opentelemetry.io/contrib/instrumentation/host v0.60.0/go.mod h1:GN4xnih1u2OQeRs8rNJ13XR8XsTqFopc57e/3Kf0h6c=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
@@ -411,10 +447,12 @@ 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=
+go.vallahaye.net/batcher v0.6.0 h1:aNqUGJyptsAiLYfS1qTPQO5Kh3wf4z57A3w+cpV4o/w=
+go.vallahaye.net/batcher v0.6.0/go.mod h1:7OX9A85hYVWrNgXKWkLjfKRoL6l04wLV0w4a8tNuDsI=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
-golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
-golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
+golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
+golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
diff --git a/tools/generator/justfile b/tools/generator/justfile
new file mode 100755
index 0000000000..5ee6a4aede
--- /dev/null
+++ b/tools/generator/justfile
@@ -0,0 +1,10 @@
+#!/usr/bin/env just --justfile
+
+set positional-arguments
+
+push-image version='latest':
+ docker buildx build . \
+ --build-context root=../.. \
+ -t ghcr.io/formancehq/ledger-generator:{{ version }} \
+ --push \
+ --platform linux/amd64,linux/arm64
diff --git a/tools/provisioner/Dockerfile b/tools/provisioner/Dockerfile
index e7e444faf7..c8fa51285a 100644
--- a/tools/provisioner/Dockerfile
+++ b/tools/provisioner/Dockerfile
@@ -1,14 +1,21 @@
FROM golang:1.24-alpine AS compiler
WORKDIR /src
+COPY --from=root pkg pkg
+COPY --from=root internal internal
+COPY --from=root cmd cmd
+COPY --from=root go.* .
+COPY --from=root *.go .
+
+WORKDIR /src/tools/provisioner
COPY go.* .
RUN --mount=type=cache,target=$GOPATH go mod download
COPY main.go .
-COPY cmd /src/cmd
-COPY pkg /src/pkg
+COPY cmd /src/tools/provisioner/cmd
+COPY pkg /src/tools/provisioner/pkg
RUN --mount=type=cache,target=$GOPATH go build -o provisioner
FROM alpine:3.21
LABEL org.opencontainers.image.source=https://github.com/formancehq/ledger
-COPY --from=compiler /src/provisioner /bin/provisioner
+COPY --from=compiler /src/tools/provisioner/provisioner /bin/provisioner
ENTRYPOINT ["/bin/provisioner"]
CMD ["--help"]
\ No newline at end of file
diff --git a/tools/provisioner/go.mod b/tools/provisioner/go.mod
index ffd4c88203..8e18e98a89 100644
--- a/tools/provisioner/go.mod
+++ b/tools/provisioner/go.mod
@@ -2,13 +2,17 @@ module github.com/formancehq/ledger/tools/provisioner
go 1.24.0
+replace github.com/formancehq/ledger => ../../
+
replace github.com/formancehq/ledger/pkg/client => ../../pkg/client
require (
- github.com/formancehq/go-libs/v3 v3.0.0-20250407134146-8be8ce3ddc42
- github.com/formancehq/ledger/pkg/client v0.0.0-00010101000000-000000000000
+ github.com/formancehq/go-libs/v3 v3.0.0-20250422121250-0689c5e8027f
+ github.com/formancehq/ledger v0.0.0-00010101000000-000000000000
+ github.com/formancehq/ledger/pkg/client v0.0.0-20250522125118-07406c497fbe
github.com/google/go-cmp v0.7.0
github.com/spf13/cobra v1.9.1
+ github.com/stretchr/testify v1.10.0
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.33.1
@@ -17,27 +21,210 @@ require (
)
require (
+ dario.cat/mergo v1.0.2 // indirect
+ filippo.io/edwards25519 v1.1.0 // indirect
+ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
+ github.com/ClickHouse/ch-go v0.66.0 // indirect
+ github.com/ClickHouse/clickhouse-go/v2 v2.35.0 // indirect
+ github.com/IBM/sarama v1.45.1 // indirect
+ github.com/Microsoft/go-winio v0.6.2 // indirect
+ github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
+ github.com/ThreeDotsLabs/watermill v1.4.6 // indirect
+ github.com/ThreeDotsLabs/watermill-http/v2 v2.3.1 // indirect
+ github.com/ThreeDotsLabs/watermill-kafka/v3 v3.0.6 // indirect
+ github.com/ThreeDotsLabs/watermill-nats/v2 v2.1.3 // indirect
+ github.com/ajg/form v1.5.1 // indirect
+ github.com/alitto/pond v1.9.2 // indirect
+ github.com/andybalholm/brotli v1.1.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.4 // indirect
+ github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect
+ github.com/aws/aws-sdk-go-v2/config v1.29.14 // indirect
+ github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect
+ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
+ github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.5.11 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect
+ github.com/aws/smithy-go v1.22.3 // 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/cenkalti/backoff/v5 v5.0.2 // indirect
+ github.com/containerd/continuity v0.4.5 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+ github.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 // indirect
+ github.com/docker/cli v27.4.1+incompatible // indirect
+ github.com/docker/docker v28.1.1+incompatible // indirect
+ github.com/docker/go-connections v0.5.0 // indirect
+ github.com/docker/go-units v0.5.0 // 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.3 // indirect
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
- github.com/ericlagergren/decimal v0.0.0-20221120152707-495c53812d05 // 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.16 // indirect
+ github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
+ github.com/go-chi/chi v4.1.2+incompatible // indirect
+ github.com/go-chi/chi/v5 v5.2.1 // indirect
+ github.com/go-chi/cors v1.2.1 // indirect
+ github.com/go-chi/render v1.0.3 // indirect
+ github.com/go-faster/city v1.0.1 // indirect
+ github.com/go-faster/errors v0.7.1 // 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-openapi/jsonpointer v0.21.1 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
+ github.com/go-sql-driver/mysql v1.9.2 // indirect
+ github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
+ github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/golang/snappy v1.0.0 // indirect
github.com/google/gnostic-models v0.6.9 // indirect
+ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
+ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // 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.26.3 // 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.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.5 // indirect
+ github.com/jackc/pgxlisten v0.0.0-20241106001234-1d6f6656415c // 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/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
+ github.com/klauspost/compress v1.18.0 // indirect
+ github.com/lib/pq v1.10.9 // 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-20250317134145-8bc96cf8fc35 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
+ github.com/mattn/go-colorable v0.1.14 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mitchellh/mapstructure v1.5.0 // indirect
+ github.com/moby/docker-image-spec v1.3.1 // indirect
+ github.com/moby/sys/user v0.4.0 // indirect
+ github.com/moby/term v0.5.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/muhlemmer/gu v0.3.1 // indirect
+ github.com/muhlemmer/httpforwarded v0.1.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+ github.com/nats-io/nats.go v1.42.0 // indirect
+ github.com/nats-io/nkeys v0.4.11 // indirect
+ github.com/nats-io/nuid v1.0.1 // indirect
+ github.com/oklog/ulid v1.3.1 // indirect
+ github.com/olivere/elastic/v7 v7.0.32 // indirect
+ github.com/onsi/ginkgo/v2 v2.23.4 // indirect
+ github.com/opencontainers/go-digest v1.0.0 // indirect
+ github.com/opencontainers/image-spec v1.1.1 // indirect
+ github.com/opencontainers/runc v1.2.3 // indirect
+ github.com/ory/dockertest/v3 v3.12.0 // indirect
+ github.com/paulmach/orb v0.11.1 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.4 // indirect
+ github.com/pierrec/lz4/v4 v4.1.22 // 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.5.1 // indirect
+ github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect
+ github.com/riandyrn/otelchi v0.12.1 // indirect
+ github.com/robfig/cron/v3 v3.0.1 // indirect
+ github.com/rs/cors v1.11.1 // indirect
+ github.com/sagikazarmark/locafero v0.9.0 // indirect
+ github.com/segmentio/asm v1.2.0 // indirect
+ github.com/shirou/gopsutil/v4 v4.25.4 // indirect
+ github.com/shomali11/util v0.0.0-20220717175126-f0771b70947f // indirect
+ github.com/shomali11/xsql v0.0.0-20190608141458-bf76292144df // indirect
+ github.com/shopspring/decimal v1.4.0 // indirect
+ github.com/sirupsen/logrus v1.9.3 // indirect
+ github.com/sourcegraph/conc v0.3.0 // indirect
+ github.com/spf13/afero v1.14.0 // indirect
+ github.com/spf13/cast v1.8.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect
+ github.com/spf13/viper v1.20.1 // indirect
+ github.com/stoewer/go-strcase v1.3.0 // indirect
+ github.com/subosito/gotenv v1.6.0 // indirect
+ github.com/tklauser/go-sysconf v0.3.15 // indirect
+ github.com/tklauser/numcpus v0.10.0 // indirect
+ github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
+ github.com/uptrace/bun v1.2.11 // indirect
+ github.com/uptrace/bun/dialect/pgdialect v1.2.11 // indirect
+ github.com/uptrace/bun/extra/bunotel v1.2.11 // 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.9-0.20240816141633-0a40785b4f41 // indirect
github.com/x448/float16 v0.8.4 // 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.8 // indirect
+ github.com/yusufpapurcu/wmi v1.2.4 // indirect
+ github.com/zitadel/oidc/v2 v2.12.2 // indirect
+ go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/host v0.60.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/runtime v0.60.0 // indirect
+ go.opentelemetry.io/contrib/propagators/b3 v1.35.0 // indirect
+ go.opentelemetry.io/otel v1.36.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 // indirect
+ go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 // indirect
+ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 // indirect
+ go.opentelemetry.io/otel/log v0.12.2 // indirect
+ go.opentelemetry.io/otel/metric v1.36.0 // indirect
+ go.opentelemetry.io/otel/sdk v1.36.0 // indirect
+ go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect
+ go.opentelemetry.io/otel/trace v1.36.0 // indirect
+ go.opentelemetry.io/proto/otlp v1.6.0 // indirect
+ go.uber.org/automaxprocs v1.6.0 // indirect
+ go.uber.org/dig v1.19.0 // indirect
+ go.uber.org/fx v1.24.0 // indirect
+ go.uber.org/mock v0.5.2 // indirect
+ go.uber.org/multierr v1.11.0 // indirect
+ go.uber.org/zap v1.27.0 // indirect
+ go.vallahaye.net/batcher v0.6.0 // indirect
+ golang.org/x/crypto v0.38.0 // indirect
+ golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.14.0 // indirect
@@ -45,14 +232,19 @@ require (
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/time v0.11.0 // indirect
+ golang.org/x/tools v0.33.0 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect
+ google.golang.org/grpc v1.72.1 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
+ gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
- k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
- sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
+ k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 // indirect
+ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
- sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
+ sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)
diff --git a/tools/provisioner/go.sum b/tools/provisioner/go.sum
index 667111afd8..52109bc507 100644
--- a/tools/provisioner/go.sum
+++ b/tools/provisioner/go.sum
@@ -1,94 +1,553 @@
+dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
+dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
+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-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
+github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/ClickHouse/ch-go v0.66.0 h1:hLslxxAVb2PHpbHr4n0d6aP8CEIpUYGMVT1Yj/Q5Img=
+github.com/ClickHouse/ch-go v0.66.0/go.mod h1:noiHWyLMJAZ5wYuq3R/K0TcRhrNA8h7o1AqHX0klEhM=
+github.com/ClickHouse/clickhouse-go/v2 v2.35.0 h1:ZMLZqxu+NiW55f4JS32kzyEbMb7CthGn3ziCcULOvSE=
+github.com/ClickHouse/clickhouse-go/v2 v2.35.0/go.mod h1:O2FFT/rugdpGEW2VKyEGyMUWyQU0ahmenY9/emxLPxs=
+github.com/IBM/sarama v1.45.1 h1:nY30XqYpqyXOXSNoe2XCgjj9jklGM1Ye94ierUb1jQ0=
+github.com/IBM/sarama v1.45.1/go.mod h1:qifDhA3VWSrQ1TjSMyxDl3nYL3oX2C83u+G6L79sq4w=
+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.4.6 h1:rWoXlxdBgUyg/bZ3OO0pON+nESVd9r6tnLTgkZ6CYrU=
+github.com/ThreeDotsLabs/watermill v1.4.6/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.6 h1:xK+VLDjYvBrRZDaFZ7WSqiNmZ9lcDG5RIilFVDZOVyQ=
+github.com/ThreeDotsLabs/watermill-kafka/v3 v3.0.6/go.mod h1:o1GcoF/1CSJ9JSmQzUkULvpZeO635pZe+WWrYNFlJNk=
+github.com/ThreeDotsLabs/watermill-nats/v2 v2.1.3 h1:/5IfNugBb9H+BvEHHNRnICmF3jaI9P7wVRzA12kDDDs=
+github.com/ThreeDotsLabs/watermill-nats/v2 v2.1.3/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/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
+github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
+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.4 h1:2jAwFwA0Xgcx94dUId+K24yFabsKYDtAhCgyMit6OqE=
+github.com/aws/aws-msk-iam-sasl-signer-go v1.0.4/go.mod h1:MVYeeOhILFFemC/XlYTClvBjYZrg/EPd3ts885KrNTI=
+github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
+github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
+github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM=
+github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
+github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.5.11 h1:qDk85oQdhwP4NR1RpkN+t40aN46/K96hF9J1vDRrkKM=
+github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.5.11/go.mod h1:f3MkXuZsT+wY24nLIP+gFUuIVQkpVopxbpUD/GUZK0Q=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
+github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8=
+github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
+github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k=
+github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
+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/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
+github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
+github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
+github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
+github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
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/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.4.1+incompatible h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI=
+github.com/docker/cli v27.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I=
+github.com/docker/docker v28.1.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/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.3 h1:K+0AjQp63JEZTEMZiwsI9g0+hAMNohwUOtY0RPGexmc=
+github.com/ebitengine/purego v0.8.3/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
-github.com/ericlagergren/decimal v0.0.0-20221120152707-495c53812d05 h1:S92OBrGuLLZsyM5ybUzgc/mPjIYk2AZqufieooe98uw=
-github.com/ericlagergren/decimal v0.0.0-20221120152707-495c53812d05/go.mod h1:M9R1FoZ3y//hwwnJtO51ypFGwm8ZfpxPT/ZLtO1mcgQ=
-github.com/formancehq/go-libs/v3 v3.0.0-20250407134146-8be8ce3ddc42 h1:rFWfsfJ/7YDqGKWP611qB3GO/IfV4RFHC6QPYFYtwhc=
-github.com/formancehq/go-libs/v3 v3.0.0-20250407134146-8be8ce3ddc42/go.mod h1:XkznJST08MyV+HzPYlpAUuzdm8GWXGYl80fOJdZpAzQ=
+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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/formancehq/go-libs/v3 v3.0.0-20250422121250-0689c5e8027f h1:/t3fKq/iXwo1KtFLE+2jtK3Ktm82OHqf6ZhuzHZWOos=
+github.com/formancehq/go-libs/v3 v3.0.0-20250422121250-0689c5e8027f/go.mod h1:T8GpmWRmRrS7Iy6tiz1gHsWMBUEOkCAIVhoXdJFM6Ns=
+github.com/formancehq/numscript v0.0.16 h1:kNNpPTmTvhRUrMXonZPMwUXUpJ06Io1WwC56Yf3nr1E=
+github.com/formancehq/numscript v0.0.16/go.mod h1:8WhBIqcK6zu27njxy7ZG7CaDX0MHtI9qF9Ggfj07wfU=
+github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
+github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
+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.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
+github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
+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-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
+github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
+github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
+github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
+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-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
+github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
+github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
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.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.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+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.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
+github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc=
+github.com/google/go-tpm v0.9.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-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/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-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
+github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
+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.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
+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.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.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
+github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
+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=
+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 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
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/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.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+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/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+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-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
+github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
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=
+github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
+github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
+github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
+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.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/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+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/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
+github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
+github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
+github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
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/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
+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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
-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/nats-io/jwt/v2 v2.7.4 h1:jXFuDDxs/GQjGDZGhNgH4tXzSUK6WQi2rsj4xmsNOtI=
+github.com/nats-io/jwt/v2 v2.7.4/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA=
+github.com/nats-io/nats-server/v2 v2.11.3 h1:AbGtXxuwjo0gBroLGGr/dE0vf24kTKdRnBq/3z/Fdoc=
+github.com/nats-io/nats-server/v2 v2.11.3/go.mod h1:6Z6Fd+JgckqzKig7DYwhgrE7bJ6fypPHnGPND+DqgMY=
+github.com/nats-io/nats.go v1.42.0 h1:ynIMupIOvf/ZWH/b2qda6WGKGNSjwOUutTpWRvAmhaM=
+github.com/nats-io/nats.go v1.42.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
+github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
+github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
+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/olivere/elastic/v7 v7.0.32 h1:R7CXvbu8Eq+WlsLgxmKVKPox0oOwAE/2T9Si5BnvK6E=
+github.com/olivere/elastic/v7 v7.0.32/go.mod h1:c7PVmLe3Fxq77PIfY/bZmxY/TAamBhCzZ8xDOE09a9k=
+github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
+github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
+github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
+github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
+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.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
+github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
+github.com/opencontainers/runc v1.2.3 h1:fxE7amCzfZflJO2lHXf4y/y8M1BoAqp+FVmG19oYB80=
+github.com/opencontainers/runc v1.2.3/go.mod h1:nSxcWUydXrsBZVYNSkTjoQ/N6rcyTtn+1SD5D4+kRIM=
+github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw=
+github.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE=
+github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU=
+github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
+github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
+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/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
+github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+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=
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/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
+github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
+github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
+github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
+github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=
+github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
+github.com/riandyrn/otelchi v0.12.1 h1:FdRKK3/RgZ/T+d+qTH5Uw3MFx0KwRF38SkdfTMMq/m8=
+github.com/riandyrn/otelchi v0.12.1/go.mod h1:weZZeUJURvtCcbWsdb7Y6F8KFZGedJlSrgUjq9VirV8=
+github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
+github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
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/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
+github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
+github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
+github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
+github.com/shirou/gopsutil/v4 v4.25.4 h1:cdtFO363VEOOFrUCjZRh4XVJkb548lyF0q0uTeMqYPw=
+github.com/shirou/gopsutil/v4 v4.25.4/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA=
+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/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
+github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
+github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
+github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
+github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
+github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk=
+github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
+github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
+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=
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.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.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.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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
+github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
+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.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
+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.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
+github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
+github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
+github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
+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.11 h1:l9dTymsdZZAoSZ1+Qo3utms0RffgkDbIv+1UGk8N1wQ=
+github.com/uptrace/bun v1.2.11/go.mod h1:ww5G8h59UrOnCHmZ8O1I/4Djc7M/Z3E+EWFS2KLB6dQ=
+github.com/uptrace/bun/dialect/pgdialect v1.2.11 h1:n0VKWm1fL1dwJK5TRxYYLaRKRe14BOg2+AQgpvqzG/M=
+github.com/uptrace/bun/dialect/pgdialect v1.2.11/go.mod h1:NvV1S/zwtwBnW8yhJ3XEKAQEw76SkeH7yUhfrx3W1Eo=
+github.com/uptrace/bun/extra/bundebug v1.2.11 h1:RyJmjITEXLRvFJwjD+u2U2eZijJhL7eIdzvW7FQSUgg=
+github.com/uptrace/bun/extra/bundebug v1.2.11/go.mod h1:K/cBN9HSW/hC17R1zVKcLOPi5PKG2PY1j7powaoCBFU=
+github.com/uptrace/bun/extra/bunotel v1.2.11 h1:ddt96XrbvlVZu5vBddP6WmbD6bdeJTaWY9jXlfuJKZE=
+github.com/uptrace/bun/extra/bunotel v1.2.11/go.mod h1:w6Mhie5tLFeP+5ryjq4PvgZEESRJ1iL2cbvxhm+f8q4=
+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.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/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
+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.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
+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.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
+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.8 h1:NwFghJfjaUW7tp+WE5mTLQQCfgseRsvgXjlSvk7x4t4=
+github.com/xo/dburl v0.23.8/go.mod h1:uazlaAQxj4gkshhfuuYyvwCBouOmNnG2aDxTCFZpmL4=
+github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
+github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
+github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
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/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.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
+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/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
+go.opentelemetry.io/contrib/instrumentation/host v0.60.0 h1:LD6TMRg2hfNzkMD36Pq0jeYBcSP9W0aJt41Zmje43Ig=
+go.opentelemetry.io/contrib/instrumentation/host v0.60.0/go.mod h1:GN4xnih1u2OQeRs8rNJ13XR8XsTqFopc57e/3Kf0h6c=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
+go.opentelemetry.io/contrib/instrumentation/runtime v0.60.0 h1:0NgN/3SYkqYJ9NBlDfl/2lzVlwos/YQLvi8sUrzJRBE=
+go.opentelemetry.io/contrib/instrumentation/runtime v0.60.0/go.mod h1:oxpUfhTkhgQaYIjtBt3T3w135dLoxq//qo3WPlPIKkE=
+go.opentelemetry.io/contrib/propagators/b3 v1.35.0 h1:DpwKW04LkdFRFCIgM3sqwTJA/QREHMeMHYPWP1WeaPQ=
+go.opentelemetry.io/contrib/propagators/b3 v1.35.0/go.mod h1:9+SNxwqvCWo1qQwUpACBY5YKNVxFJn5mlbXg/4+uKBg=
+go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
+go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 h1:zwdo1gS2eH26Rg+CoqVQpEK1h8gvt5qyU5Kk5Bixvow=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0/go.mod h1:rUKCPscaRWWcqGT6HnEmYrK+YNe5+Sw64xgQTOJ5b30=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0 h1:gAU726w9J8fwr4qRDqu1GYMNNs4gXrU+Pv20/N1UpB4=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0/go.mod h1:RboSDkp7N292rgu+T0MgVt2qgFGu6qa1RpZDOtpL76w=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 h1:nRVXXvf78e00EwY6Wp0YII8ww2JVWshZ20HfTlE11AM=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ=
+go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY=
+go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw=
+go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 h1:G8Xec/SgZQricwWBJF/mHZc7A02YHedfFDENwJEdRA0=
+go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0/go.mod h1:PD57idA/AiFD5aqoxGxCvT/ILJPeHy3MjqU/NS7KogY=
+go.opentelemetry.io/otel/log v0.12.2 h1:yob9JVHn2ZY24byZeaXpTVoPS6l+UrrxmxmPKohXTwc=
+go.opentelemetry.io/otel/log v0.12.2/go.mod h1:ShIItIxSYxufUMt+1H5a2wbckGli3/iCfuEbVZi/98E=
+go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
+go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
+go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
+go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
+go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
+go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
+go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
+go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
+go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
+go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
+go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
+go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
+go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4=
+go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
+go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg=
+go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo=
+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.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
+go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
+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=
+go.vallahaye.net/batcher v0.6.0 h1:aNqUGJyptsAiLYfS1qTPQO5Kh3wf4z57A3w+cpV4o/w=
+go.vallahaye.net/batcher v0.6.0/go.mod h1:7OX9A85hYVWrNgXKWkLjfKRoL6l04wLV0w4a8tNuDsI=
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.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
+golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
+golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
+golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
+golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
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/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-20200114155413-6afb5195e5aa/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-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.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.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
@@ -96,17 +555,43 @@ golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKl
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-20210220032951-036812b2e83c/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.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
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=
+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-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-20201204225414-ed752295db88/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-20210616094352-59db8d763f22/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.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.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+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/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
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.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
@@ -115,25 +600,41 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
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=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
-golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
+golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
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-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0=
+google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
+google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
+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/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+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=
+gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
+gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw=
k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw=
k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4=
@@ -144,14 +645,14 @@ k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
-k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
-k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
-sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
-sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
+k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 h1:jgJW5IePPXLGB8e/1wvd0Ich9QE97RvvF3a8J3fP/Lg=
+k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
+sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
-sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
-sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
+sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI=
+sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/tools/provisioner/justfile b/tools/provisioner/justfile
index 7f91d0d2f2..a1c2f30914 100755
--- a/tools/provisioner/justfile
+++ b/tools/provisioner/justfile
@@ -4,6 +4,7 @@ set positional-arguments
push-image version='latest':
docker buildx build . \
+ --build-context root=../.. \
-t ghcr.io/formancehq/ledger-provisioner:{{ version }} \
--push \
--platform linux/amd64,linux/arm64
diff --git a/tools/provisioner/pkg/config.go b/tools/provisioner/pkg/config.go
index f83cc9c218..33bc6dec7b 100644
--- a/tools/provisioner/pkg/config.go
+++ b/tools/provisioner/pkg/config.go
@@ -1,17 +1,31 @@
package provisionner
-type LedgerConfig struct {
+type LedgerCreateConfig struct {
Bucket string `yaml:"bucket"`
Features map[string]string `yaml:"features"`
Metadata map[string]string `yaml:"metadata"`
}
+type LedgerConfig struct {
+ LedgerCreateConfig `yaml:",inline"`
+ Exporters []string `yaml:"exporters"`
+}
+
+type ExporterConfig struct {
+ Driver string `yaml:"driver"`
+ Config map[string]any `yaml:"config"`
+}
+
type Config struct {
- Ledgers map[string]LedgerConfig `yaml:"ledgers"`
+ Ledgers map[string]LedgerConfig `yaml:"ledgers"`
+ Exporters map[string]ExporterConfig `yaml:"exporters"`
}
-func newState() State {
- return State{
- Ledgers: make(map[string]LedgerConfig),
+func (cfg *Config) setDefaults() {
+ if cfg.Ledgers == nil {
+ cfg.Ledgers = map[string]LedgerConfig{}
+ }
+ if cfg.Exporters == nil {
+ cfg.Exporters = map[string]ExporterConfig{}
}
}
diff --git a/tools/provisioner/pkg/reconciler.go b/tools/provisioner/pkg/reconciler.go
index 18dcf944cc..cf2d96828d 100644
--- a/tools/provisioner/pkg/reconciler.go
+++ b/tools/provisioner/pkg/reconciler.go
@@ -3,12 +3,14 @@ package provisionner
import (
"context"
"fmt"
+ . "github.com/formancehq/go-libs/v3/collectionutils"
"github.com/formancehq/go-libs/v3/pointer"
"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/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
+ "slices"
)
type Reconciler struct {
@@ -17,10 +19,7 @@ type Reconciler struct {
}
func (r Reconciler) Reconcile(ctx context.Context, cfg Config) error {
-
- if cfg.Ledgers == nil {
- cfg.Ledgers = map[string]LedgerConfig{}
- }
+ cfg.setDefaults()
state, err := r.store.Read(ctx)
if err != nil {
@@ -31,37 +30,152 @@ func (r Reconciler) Reconcile(ctx context.Context, cfg Config) error {
fmt.Printf("Failed to update state: %v\r\n", err)
}
}()
+ state.setDefaults()
- if state.Ledgers == nil {
- state.Ledgers = map[string]LedgerConfig{}
+ if err := r.handleExporters(ctx, cfg, state); err != nil {
+ return err
}
- for ledgerName, ledgerConfig := range cfg.Ledgers {
+ if err := r.handleLedgers(ctx, cfg, state); err != nil {
+ return err
+ }
- existingConfig, ok := state.Ledgers[ledgerName]
+ return nil
+}
+
+func (r Reconciler) handleExporters(ctx context.Context, cfg Config, state *State) error {
+ for exporterName, exporterConfig := range cfg.Exporters {
+ existingExporterState, ok := state.Exporters[exporterName]
if ok {
- if !cmp.Equal(ledgerConfig, existingConfig, cmpopts.EquateEmpty()) {
+ if !cmp.Equal(exporterConfig, existingExporterState.Config, cmpopts.EquateEmpty()) {
+ fmt.Printf("Config for exporter %s has changed, deleting exporter...\r\n", exporterName)
+ _, err := r.ledgerClient.Ledger.V2.DeleteExporter(ctx, operations.V2DeleteExporterRequest{
+ ExporterID: existingExporterState.ID,
+ })
+ if err != nil {
+ return fmt.Errorf("failed to delete exporter %s: %w", exporterName, err)
+ }
+ } else {
+ fmt.Printf("Config for exporter %s is up to date\r\n", exporterName)
+ continue
+ }
+ }
+
+ fmt.Printf("Creating exporter %s...\r\n", exporterName)
+ ret, err := r.ledgerClient.Ledger.V2.CreateExporter(ctx, components.V2CreateExporterRequest{
+ Driver: exporterConfig.Driver,
+ Config: exporterConfig.Config,
+ })
+ if err != nil {
+ return fmt.Errorf("failed to create exporter %s: %w", exporterName, err)
+ }
+ fmt.Printf("Exporter %s created.\r\n", exporterName)
+
+ state.Exporters[exporterName] = &ExporterState{
+ ID: ret.V2CreateExporterResponse.Data.ID,
+ Config: exporterConfig,
+ }
+ }
+
+ if state.Exporters != nil {
+ for exporterName, exporterState := range state.Exporters {
+ _, configExists := cfg.Exporters[exporterName]
+ if !configExists {
+ fmt.Printf("Exporter %s removed\r\n", exporterName)
+ _, err := r.ledgerClient.Ledger.V2.DeleteExporter(ctx, operations.V2DeleteExporterRequest{
+ ExporterID: exporterState.ID,
+ })
+ if err != nil {
+ return fmt.Errorf("failed to delete exporter %s: %w", exporterName, err)
+ }
+
+ state.removeExporter(exporterName)
+ }
+ }
+ }
+
+ return nil
+}
+
+func (r Reconciler) handleLedgers(ctx context.Context, cfg Config, state *State) error {
+ for ledgerName, ledgerConfig := range cfg.Ledgers {
+ ledgerState, ok := state.Ledgers[ledgerName]
+ if !ok {
+ ledgerState = &LedgerState{
+ Exporters: map[string]string{},
+ }
+ state.Ledgers[ledgerName] = ledgerState
+
+ fmt.Printf("Creating ledger %s...\r\n", ledgerName)
+ _, err := r.ledgerClient.Ledger.V2.CreateLedger(ctx, operations.V2CreateLedgerRequest{
+ Ledger: ledgerName,
+ V2CreateLedgerRequest: components.V2CreateLedgerRequest{
+ Bucket: pointer.For(ledgerConfig.Bucket),
+ Features: ledgerConfig.Features,
+ Metadata: ledgerConfig.Metadata,
+ },
+ })
+ if err != nil {
+ return fmt.Errorf("failed to create ledger %s: %w", ledgerName, err)
+ }
+ fmt.Printf("Ledger %s created.\r\n", ledgerName)
+
+ ledgerState.Config = ledgerConfig
+ } else {
+ if !cmp.Equal(ledgerConfig.LedgerCreateConfig, ledgerState.Config.LedgerCreateConfig, cmpopts.EquateEmpty()) {
fmt.Printf("Config for ledger %s was updated but it is not supported at this time\r\n", ledgerName)
} else {
fmt.Printf("Config for ledger %s is up to date\r\n", ledgerName)
}
- continue
}
- fmt.Printf("Creating ledger %s...\r\n", ledgerName)
- if _, err := r.ledgerClient.Ledger.V2.CreateLedger(ctx, operations.V2CreateLedgerRequest{
- Ledger: ledgerName,
- V2CreateLedgerRequest: components.V2CreateLedgerRequest{
- Bucket: pointer.For(ledgerConfig.Bucket),
- Features: ledgerConfig.Features,
- Metadata: ledgerConfig.Metadata,
- },
- }); err != nil {
- return fmt.Errorf("failed to create ledger %s: %w", ledgerName, err)
+ for _, exporter := range ledgerConfig.Exporters {
+ if slices.Contains(Keys(ledgerState.Exporters), exporter) {
+ continue
+ }
+
+ fmt.Printf(
+ "Detect new exporter binding for ledger %s and exporter %s, creating a new pipeline...\r\n",
+ ledgerName,
+ exporter,
+ )
+
+ ret, err := r.ledgerClient.Ledger.V2.CreatePipeline(ctx, operations.V2CreatePipelineRequest{
+ Ledger: ledgerName,
+ V2CreatePipelineRequest: &components.V2CreatePipelineRequest{
+ ExporterID: state.Exporters[exporter].ID,
+ },
+ })
+ if err != nil {
+ return fmt.Errorf("failed to create pipeline for ledger %s and exporter %s: %w", ledgerName, exporter, err)
+ }
+ fmt.Printf("Pipeline %s created.\r\n", ret.V2CreatePipelineResponse.Data.ID)
+
+ ledgerState.Exporters[exporter] = ret.V2CreatePipelineResponse.Data.ID
}
- fmt.Printf("Ledger %s created...\r\n", ledgerName)
- state.Ledgers[ledgerName] = ledgerConfig
+ for _, exporter := range Keys(ledgerState.Exporters) {
+ if slices.Contains(ledgerConfig.Exporters, exporter) {
+ continue
+ }
+
+ fmt.Printf(
+ "Detect removed exporter binding for ledger %s and exporter %s, deleting pipeline %s...\r\n",
+ ledgerName,
+ exporter,
+ ledgerState.Exporters[exporter],
+ )
+
+ _, err := r.ledgerClient.Ledger.V2.DeletePipeline(ctx, operations.V2DeletePipelineRequest{
+ Ledger: ledgerName,
+ PipelineID: ledgerState.Exporters[exporter],
+ })
+ if err != nil {
+ return fmt.Errorf("failed to delete pipeline for ledger %s and exporter %s: %w", ledgerName, exporter, err)
+ }
+
+ ledgerState.removeExporterBinding(exporter)
+ }
}
if state.Ledgers != nil {
diff --git a/tools/provisioner/pkg/reconciler_test.go b/tools/provisioner/pkg/reconciler_test.go
new file mode 100644
index 0000000000..651440fe7f
--- /dev/null
+++ b/tools/provisioner/pkg/reconciler_test.go
@@ -0,0 +1,411 @@
+//go:build it
+
+package provisionner
+
+import (
+ "github.com/formancehq/go-libs/v3/logging"
+ "github.com/formancehq/go-libs/v3/testing/deferred"
+ "github.com/formancehq/go-libs/v3/testing/docker"
+ "github.com/formancehq/go-libs/v3/testing/platform/pgtesting"
+ "github.com/formancehq/go-libs/v3/testing/testservice"
+ "github.com/formancehq/ledger/pkg/testserver"
+ "github.com/stretchr/testify/require"
+ "os"
+ "testing"
+)
+
+func TestReconciler(t *testing.T) {
+ t.Parallel()
+
+ dockerPool := docker.NewPool(t, logging.Testing())
+ pgServer := pgtesting.CreatePostgresServer(t, dockerPool)
+
+ type step struct {
+ cfg Config
+ expectedState State
+ }
+
+ type testCase struct {
+ name string
+ steps []step
+ }
+ for _, tc := range []testCase{
+ {
+ name: "nominal",
+ steps: []step{{
+ cfg: Config{},
+ expectedState: State{
+ Ledgers: map[string]*LedgerState{},
+ Exporters: map[string]*ExporterState{},
+ },
+ }},
+ },
+ {
+ name: "with a feature config",
+ steps: []step{{
+ cfg: Config{
+ Ledgers: map[string]LedgerConfig{
+ "ledger1": {
+ LedgerCreateConfig: LedgerCreateConfig{
+ Features: map[string]string{
+ "HASH_LOGS": "DISABLED",
+ },
+ },
+ },
+ },
+ },
+ expectedState: State{
+ Exporters: map[string]*ExporterState{},
+ Ledgers: map[string]*LedgerState{
+ "ledger1": {
+ Exporters: map[string]string{},
+ Config: LedgerConfig{
+ LedgerCreateConfig: LedgerCreateConfig{
+ Features: map[string]string{
+ "HASH_LOGS": "DISABLED",
+ },
+ },
+ },
+ },
+ },
+ },
+ }},
+ },
+ {
+ name: "3 ledgers",
+ steps: []step{{
+ cfg: Config{
+ Ledgers: map[string]LedgerConfig{
+ "ledger1": {},
+ "ledger2": {},
+ "ledger3": {},
+ },
+ },
+ expectedState: State{
+ Exporters: map[string]*ExporterState{},
+ Ledgers: map[string]*LedgerState{
+ "ledger1": {
+ Exporters: map[string]string{},
+ },
+ "ledger2": {
+ Exporters: map[string]string{},
+ },
+ "ledger3": {
+ Exporters: map[string]string{},
+ },
+ },
+ },
+ }},
+ },
+ {
+ name: "2 exporters",
+ steps: []step{{
+ cfg: Config{
+ Exporters: map[string]ExporterConfig{
+ "clickhouse1": {
+ Driver: "clickhouse",
+ Config: map[string]any{
+ "dsn": "clickhouse://srv1:8123",
+ },
+ },
+ "clickhouse2": {
+ Driver: "clickhouse",
+ Config: map[string]any{
+ "dsn": "clickhouse://srv2:8123",
+ },
+ },
+ },
+ },
+ expectedState: State{
+ Exporters: map[string]*ExporterState{
+ "clickhouse1": {
+ Config: ExporterConfig{
+ Driver: "clickhouse",
+ Config: map[string]any{
+ "dsn": "clickhouse://srv1:8123",
+ },
+ },
+ },
+ "clickhouse2": {
+ Config: ExporterConfig{
+ Driver: "clickhouse",
+ Config: map[string]any{
+ "dsn": "clickhouse://srv2:8123",
+ },
+ },
+ },
+ },
+ Ledgers: map[string]*LedgerState{},
+ },
+ }},
+ },
+ {
+ name: "1 exporter and a ledger bounded to it",
+ steps: []step{{
+ cfg: Config{
+ Exporters: map[string]ExporterConfig{
+ "clickhouse1": {
+ Driver: "clickhouse",
+ Config: map[string]any{
+ "dsn": "clickhouse://srv1:8123",
+ },
+ },
+ },
+ Ledgers: map[string]LedgerConfig{
+ "ledger1": {
+ Exporters: []string{"clickhouse1"},
+ },
+ },
+ },
+ expectedState: State{
+ Exporters: map[string]*ExporterState{
+ "clickhouse1": {
+ Config: ExporterConfig{
+ Driver: "clickhouse",
+ Config: map[string]any{
+ "dsn": "clickhouse://srv1:8123",
+ },
+ },
+ },
+ },
+ Ledgers: map[string]*LedgerState{
+ "ledger1": {
+ Config: LedgerConfig{
+ Exporters: []string{"clickhouse1"},
+ },
+ Exporters: map[string]string{
+ "clickhouse1": "",
+ },
+ },
+ },
+ },
+ }},
+ },
+ {
+ name: "removing exporter binding",
+ steps: []step{
+ {
+ cfg: Config{
+ Exporters: map[string]ExporterConfig{
+ "clickhouse1": {
+ Driver: "clickhouse",
+ Config: map[string]any{
+ "dsn": "clickhouse://srv1:8123",
+ },
+ },
+ },
+ Ledgers: map[string]LedgerConfig{
+ "ledger1": {
+ Exporters: []string{"clickhouse1"},
+ },
+ },
+ },
+ expectedState: State{
+ Exporters: map[string]*ExporterState{
+ "clickhouse1": {
+ Config: ExporterConfig{
+ Driver: "clickhouse",
+ Config: map[string]any{
+ "dsn": "clickhouse://srv1:8123",
+ },
+ },
+ },
+ },
+ Ledgers: map[string]*LedgerState{
+ "ledger1": {
+ Config: LedgerConfig{
+ Exporters: []string{"clickhouse1"},
+ },
+ Exporters: map[string]string{
+ "clickhouse1": "",
+ },
+ },
+ },
+ },
+ },
+ {
+ cfg: Config{
+ Exporters: map[string]ExporterConfig{
+ "clickhouse1": {
+ Driver: "clickhouse",
+ Config: map[string]any{
+ "dsn": "clickhouse://srv1:8123",
+ },
+ },
+ },
+ Ledgers: map[string]LedgerConfig{
+ "ledger1": {},
+ },
+ },
+ expectedState: State{
+ Exporters: map[string]*ExporterState{
+ "clickhouse1": {
+ Config: ExporterConfig{
+ Driver: "clickhouse",
+ Config: map[string]any{
+ "dsn": "clickhouse://srv1:8123",
+ },
+ },
+ },
+ },
+ Ledgers: map[string]*LedgerState{
+ "ledger1": {
+ Config: LedgerConfig{
+ Exporters: []string{},
+ },
+ Exporters: map[string]string{},
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "removing exporter without binding",
+ steps: []step{
+ {
+ cfg: Config{
+ Exporters: map[string]ExporterConfig{
+ "clickhouse1": {
+ Driver: "clickhouse",
+ Config: map[string]any{
+ "dsn": "clickhouse://srv1:8123",
+ },
+ },
+ },
+ Ledgers: map[string]LedgerConfig{},
+ },
+ expectedState: State{
+ Exporters: map[string]*ExporterState{
+ "clickhouse1": {
+ Config: ExporterConfig{
+ Driver: "clickhouse",
+ Config: map[string]any{
+ "dsn": "clickhouse://srv1:8123",
+ },
+ },
+ },
+ },
+ Ledgers: map[string]*LedgerState{},
+ },
+ },
+ {
+ cfg: Config{
+ Exporters: map[string]ExporterConfig{},
+ Ledgers: map[string]LedgerConfig{},
+ },
+ expectedState: State{
+ Exporters: map[string]*ExporterState{},
+ Ledgers: map[string]*LedgerState{},
+ },
+ },
+ },
+ },
+ {
+ name: "removing exporter with binding",
+ steps: []step{
+ {
+ cfg: Config{
+ Exporters: map[string]ExporterConfig{
+ "clickhouse1": {
+ Driver: "clickhouse",
+ Config: map[string]any{
+ "dsn": "clickhouse://srv1:8123",
+ },
+ },
+ },
+ Ledgers: map[string]LedgerConfig{
+ "ledger1": {
+ Exporters: []string{"clickhouse1"},
+ },
+ },
+ },
+ expectedState: State{
+ Exporters: map[string]*ExporterState{
+ "clickhouse1": {
+ Config: ExporterConfig{
+ Driver: "clickhouse",
+ Config: map[string]any{
+ "dsn": "clickhouse://srv1:8123",
+ },
+ },
+ },
+ },
+ Ledgers: map[string]*LedgerState{
+ "ledger1": {
+ Config: LedgerConfig{
+ Exporters: []string{"clickhouse1"},
+ },
+ Exporters: map[string]string{
+ "clickhouse1": "",
+ },
+ },
+ },
+ },
+ },
+ {
+ cfg: Config{
+ Exporters: map[string]ExporterConfig{},
+ Ledgers: map[string]LedgerConfig{
+ "ledger1": {
+ Exporters: []string{},
+ },
+ },
+ },
+ expectedState: State{
+ Exporters: map[string]*ExporterState{},
+ Ledgers: map[string]*LedgerState{
+ "ledger1": {
+ Config: LedgerConfig{
+ Exporters: []string{},
+ },
+ Exporters: map[string]string{},
+ },
+ },
+ },
+ },
+ },
+ },
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ db := pgServer.NewDatabase(t)
+
+ srv := testserver.NewTestServer(deferred.FromValue(db.ConnectionOptions()),
+ testservice.WithInstruments(
+ testservice.DebugInstrumentation(os.Getenv("DEBUG") == "true"),
+ testserver.ExperimentalExportersInstrumentation(),
+ testserver.ExperimentalFeaturesInstrumentation(),
+ ),
+ )
+
+ store := NewInMemoryStore()
+ r := NewReconciler(store, testserver.Client(srv))
+
+ for _, step := range tc.steps {
+ require.NoError(t, r.Reconcile(logging.TestingContext(), step.cfg))
+
+ storedState := store.state
+ expectedState := step.expectedState
+ for exporter := range storedState.Exporters {
+ if _, ok := expectedState.Exporters[exporter]; ok {
+ expectedState.Exporters[exporter].ID = storedState.Exporters[exporter].ID
+ }
+ }
+
+ for ledgerName, ledgerState := range storedState.Ledgers {
+ for exporterName, pipelineID := range ledgerState.Exporters {
+ if expectedLedgerState, ok := expectedState.Ledgers[ledgerName]; ok {
+ if _, ok := expectedLedgerState.Exporters[exporterName]; ok {
+ expectedLedgerState.Exporters[exporterName] = pipelineID
+ }
+ }
+ }
+ }
+
+ require.EqualValues(t, expectedState, storedState)
+ }
+ })
+ }
+}
diff --git a/tools/provisioner/pkg/state.go b/tools/provisioner/pkg/state.go
index 4bc9285695..7c303339c5 100644
--- a/tools/provisioner/pkg/state.go
+++ b/tools/provisioner/pkg/state.go
@@ -1,5 +1,48 @@
package provisionner
+import . "github.com/formancehq/go-libs/v3/collectionutils"
+
+type ExporterState struct {
+ ID string `yaml:"id"`
+ Config ExporterConfig `yaml:"config"`
+}
+
+type LedgerState struct {
+ Config LedgerConfig `yaml:"config"`
+
+ // Map the exporter name to the pipeline id
+ Exporters map[string]string `yaml:"exporters"`
+}
+
+func (state *LedgerState) removeExporterBinding(exporterName string) {
+ delete(state.Exporters, exporterName)
+ state.Config.Exporters = Filter(state.Config.Exporters, FilterNot(FilterEq(exporterName)))
+}
+
type State struct {
- Ledgers map[string]LedgerConfig
+ Ledgers map[string]*LedgerState `yaml:"ledgers"`
+ Exporters map[string]*ExporterState `yaml:"exporters"`
+}
+
+func (s *State) setDefaults() {
+ if s.Ledgers == nil {
+ s.Ledgers = make(map[string]*LedgerState)
+ }
+ if s.Exporters == nil {
+ s.Exporters = make(map[string]*ExporterState)
+ }
+}
+
+func (s *State) removeExporter(name string) {
+ delete(s.Exporters, name)
+ for _, ledger := range s.Ledgers {
+ ledger.removeExporterBinding(name)
+ }
+}
+
+func newState() State {
+ return State{
+ Ledgers: make(map[string]*LedgerState),
+ Exporters: make(map[string]*ExporterState),
+ }
}
diff --git a/tools/provisioner/pkg/store.go b/tools/provisioner/pkg/store.go
index 14a3eabe74..1fb4da1619 100644
--- a/tools/provisioner/pkg/store.go
+++ b/tools/provisioner/pkg/store.go
@@ -137,3 +137,22 @@ func NewK8SConfigMapStore(namespace, configMapName string) (*K8sConfigMapStore,
configMapName: configMapName,
}, nil
}
+
+type inMemoryStore struct {
+ state State
+}
+
+func (i *inMemoryStore) Read(_ context.Context) (*State, error) {
+ return &i.state, nil
+}
+
+func (i *inMemoryStore) Update(_ context.Context, state State) error {
+ i.state = state
+ return nil
+}
+
+var _ Store = (*inMemoryStore)(nil)
+
+func NewInMemoryStore() *inMemoryStore {
+ return &inMemoryStore{}
+}
\ No newline at end of file