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