An open API service indexing awesome lists of open source software.

https://github.com/kozmod/oniontx

The library for transferring transaction management to the application layer.
https://github.com/kozmod/oniontx

clean-architecture delivery-layer go golang gorm-transaction hexagonal-architecture layer-architecture mongo-transaction onion-architecture pgx-transaction redis-transaction repository sql sql-transaction sql-transactions sqlx-transaction transaction tx

Last synced: about 1 month ago
JSON representation

The library for transferring transaction management to the application layer.

Awesome Lists containing this project

README

          

# oniontx drawing
[![test](https://github.com/kozmod/oniontx/actions/workflows/test.yml/badge.svg)](https://github.com/kozmod/oniontx/actions/workflows/test.yml)
[![Release](https://github.com/kozmod/oniontx/actions/workflows/release.yml/badge.svg)](https://github.com/kozmod/oniontx/actions/workflows/release.yml)
![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/kozmod/oniontx)
[![Go Report Card](https://goreportcard.com/badge/github.com/kozmod/oniontx)](https://goreportcard.com/report/github.com/kozmod/oniontx)
![GitHub release date](https://img.shields.io/github/release-date/kozmod/oniontx)
![GitHub last commit](https://img.shields.io/github/last-commit/kozmod/oniontx)
[![GitHub MIT license](https://img.shields.io/github/license/kozmod/oniontx)](https://github.com/kozmod/oniontx/blob/dev/LICENSE)

`oniontx` enables moving transaction management from the `Persistence` (repository) layer
to the `Application` (service) (service) layer using an owner-defined contract.

## Transactor

# drawing
🔴 **NOTE:** `Transactor` was designed to work with only a single instance of a "repository" (`*sql.DB`, etc.).
For multiple repositories, use `Transactor` with `Sage`[**ⓘ**](#saga).

### The key features:
- [**simple implementation for `stdlib`**](#libs)
- [**simple implementation for popular libraries**](#libs)
- [**custom implementation's contract**](#custom)
- [**simple testing with testing frameworks**](#testing)

---
### Default implementation examples for libs
[test/integration](https://github.com/kozmod/oniontx/tree/master/test) module contains examples
of default `Transactor` implementations (stdlib, sqlx, pgx, gorm, redis, mongo):
- [stdlib](https://github.com/kozmod/oniontx/tree/master/test/integration/internal/stdlib)
- [sqlx](https://github.com/kozmod/oniontx/tree/master/test/integration/internal/sqlx)
- [pgx](https://github.com/kozmod/oniontx/tree/master/test/integration/internal/pgx)
- [gorm](https://github.com/kozmod/oniontx/tree/master/test/integration/internal/gorm)
- [redis](https://github.com/kozmod/oniontx/tree/master/test/integration/internal/redis)
- [mongo](https://github.com/kozmod/oniontx/tree/master/test/integration/internal/mongo)

---

## Custom implementation
If required, `oniontx` provides the ability to
implement custom algorithms for managing transactions (see examples).
#### Interfaces:
```go
type (
// Mandatory
TxBeginner[T Tx] interface {
comparable
BeginTx(ctx context.Context) (T, error)
}

// Mandatory
Tx interface {
Rollback(ctx context.Context) error
Commit(ctx context.Context) error
}

// Optional - using to putting/getting transaction from `context.Context`
// (library contains default `СtxOperator` implementation)
СtxOperator[T Tx] interface {
Inject(ctx context.Context, tx T) context.Context
Extract(ctx context.Context) (T, bool)
}
)
```
### Examples
`❗` ️***These examples are based on the `stdlib` package.***

`TxBeginner` and `Tx` implementations:
```go
// Prepared contracts for execution
package db

import (
"context"
"database/sql"

"github.com/kozmod/oniontx/mtx"
)

// Executor represents common methods of sql.DB and sql.Tx.
type Executor interface {
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
}

// DB is sql.DB wrapper, implements mtx.TxBeginner.
type DB struct {
*sql.DB
}

func (db *DB) BeginTx(ctx context.Context) (*Tx, error) {
var txOptions sql.TxOptions
for _, opt := range opts {
opt.Apply(&txOptions)
}
tx, err := db.DB.BeginTx(ctx, &txOptions)
return &Tx{Tx: tx}, err
}

// Tx is sql.Tx wrapper, implements mtx.Tx.
type Tx struct {
*sql.Tx
}

func (t *Tx) Rollback(_ context.Context) error {
return t.Tx.Rollback()
}

func (t *Tx) Commit(_ context.Context) error {
return t.Tx.Commit()
}
```
`Repositories` implementation:
```go
package repoA

import (
"context"
"fmt"

"github.com/kozmod/oniontx/mtx"

"github.com/user/some_project/internal/db"
)

type RepositoryA struct {
Transactor *mtx.Transactor[*db.DB, *db.Tx]
}

func (r RepositoryA) Insert(ctx context.Context, val int) error {
var executor db.Executor
executor, ok := r.Transactor.TryGetTx(ctx)
if !ok {
executor = r.Transactor.TxBeginner()
}
_, err := executor.ExecContext(ctx, "UPDATE some_A SET value = $1", val)
if err != nil {
return fmt.Errorf("update 'some_A': %w", err)
}
return nil
}
```
```go
package repoB

import (
"context"
"fmt"

"github.com/kozmod/oniontx/mtx"

"github.com/user/some_project/internal/db"
)

type RepositoryB struct {
Transactor *mtx.Transactor[*db.DB, *db.Tx]
}

func (r RepositoryB) Insert(ctx context.Context, val int) error {
var executor db.Executor
executor, ok := r.Transactor.TryGetTx(ctx)
if !ok {
executor = r.Transactor.TxBeginner()
}
_, err := executor.ExecContext(ctx, "UPDATE some_A SET value = $1", val)
if err != nil {
return fmt.Errorf("update 'some_A': %w", err)
}
return nil
}
```
`UseCase` implementation:
```go
package usecase

import (
"context"
"fmt"
)

type (
// transactor is the contract of the mtx.Transactor
transactor interface {
WithinTx(ctx context.Context, fn func(ctx context.Context) error) (err error)
}

// Repo is the contract of repositories
repo interface {
Insert(ctx context.Context, val int) error
}
)

type UseCase struct {
RepoA repo
RepoB repo

Transactor transactor
}

func (s *UseCase) Exec(ctx context.Context, insert int) error {
err := s.Transactor.WithinTx(ctx, func(ctx context.Context) error {
if err := s.RepoA.Insert(ctx, insert); err != nil {
return fmt.Errorf("call repository A: %w", err)
}
if err := s.RepoB.Insert(ctx, insert); err != nil {
return fmt.Errorf("call repository B: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf(" execute: %w", err)
}
return nil
}
```
Configuring:
```go
package main

import (
"context"
"database/sql"
"os"

"github.com/kozmod/oniontx/mtx"

"github.com/user/some_project/internal/repoA"
"github.com/user/some_project/internal/repoB"
"github.com/user/some_project/internal/usecase"
)

func main() {
var (
database *sql.DB // database pointer

wrapper = &db.DB{DB: database}
operator = mtx.NewContextOperator[*db.DB, *db.Tx](&wrapper)
transactor = mtx.NewTransactor[*db.DB, *db.Tx](wrapper, operator)

repositoryA = repoA.RepositoryA{
Transactor: transactor,
}
repositoryB = repoB.RepositoryB{
Transactor: transactor,
}

useCase = usecase.UseCase{
RepoA: &repositoryA,
RepoB: &repositoryB,
Transactor: transactor,
}
)

err := useCase.Exec(context.Background(), 1)
if err != nil {
os.Exit(1)
}
}
```
---
#### Execution transaction in the different use cases
***Executing the same transaction for different `UseCases` using the same `Transactor` instance***

UseCases:
```go
package a

import (
"context"
"fmt"
)

type (
// transactor is the contract of the mtx.Transactor
transactor interface {
WithinTx(ctx context.Context, fn func(ctx context.Context) error) (err error)
}

// Repo is the contract of repositories
repoA interface {
Insert(ctx context.Context, val int) error
Delete(ctx context.Context, val float64) error
}
)

type UseCaseA struct {
Repo repoA

Transactor transactor
}

func (s *UseCaseA) Exec(ctx context.Context, insert int, delete float64) error {
err := s.Transactor.WithinTx(ctx, func(ctx context.Context) error {
if err := s.Repo.Insert(ctx, insert); err != nil {
return fmt.Errorf("call repository - insert: %w", err)
}
if err := s.Repo.Delete(ctx, delete); err != nil {
return fmt.Errorf("call repository - delete: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("usecaseA - execute: %w", err)
}
return nil
}
```
```go
package b

import (
"context"
"fmt"
)

type (
// transactor is the contract of the mtx.Transactor
transactor interface {
WithinTx(ctx context.Context, fn func(ctx context.Context) error) (err error)
}

// Repo is the contract of repositories
repoB interface {
Insert(ctx context.Context, val string) error
}

// Repo is the contract of the useCase
useCaseA interface {
Exec(ctx context.Context, insert int, delete float64) error
}
)

type UseCaseB struct {
Repo repoB
UseCaseA useCaseA

Transactor transactor
}

func (s *UseCaseB) Exec(ctx context.Context, insertA string, insertB int, delete float64) error {
err := s.Transactor.WithinTx(ctx, func(ctx context.Context) error {
if err := s.Repo.Insert(ctx, insertA); err != nil {
return fmt.Errorf("call repository - insert: %w", err)
}
if err := s.UseCaseA.Exec(ctx, insertB, delete); err != nil {
return fmt.Errorf("call usecaseB - exec: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("execute: %w", err)
}
return nil
}
```
Main:
```go
package main

import (
"context"
"database/sql"
"os"

"github.com/kozmod/oniontx/mtx"

"github.com/user/some_project/internal/db"
"github.com/user/some_project/internal/repoA"
"github.com/user/some_project/internal/repoB"
"github.com/user/some_project/internal/usecase/a"
"github.com/user/some_project/internal/usecase/b"
)

func main() {
var (
database *sql.DB // database pointer

wrapper = &db.DB{DB: database}
operator = mtx.NewContextOperator[*db.DB, *db.Tx](&wrapper)
transactor = mtx.NewTransactor[*db.DB, *db.Tx](wrapper, operator)

useCaseA = a.UseCaseA{
Repo: repoA.RepositoryA{
Transactor: transactor,
},
}

useCaseB = b.UseCaseB{
Repo: repoB.RepositoryB{
Transactor: transactor,
},
UseCaseA: &useCaseA,
}
)

err := useCaseB.Exec(context.Background(), "some_to_insert_useCase_A", 1, 1.1)
if err != nil {
os.Exit(1)
}
}
```

## Saga
The implementation of the `Saga` pattern.

Example:
```go
steps := []Step{
{
Name: "first step",
// Action — a function to execute
Action: func(ctx context.Context) error {
// Action logic.
return nil
},
// Compensation — a function to compensate an action when an error occurs.
//
// Parameters:
// - ctx: context for cancellation and deadlines (context that is passed through the action)
// - aroseErr: error from the previous action that needs compensation
Compensation: func(ctx context.Context, aroseErr error) error {
// Action compensation logic.
return nil
},
// CompensationOnFail needs to add the current compensation to the list of compensations.
CompensationOnFail: true,
},
}
// Saga execution.
err := NewSaga(steps).Execute(context.Background())
if err != nil {
// Error handling.
}
```

[More examples here](https://github.com/kozmod/oniontx/tree/master/test/integration/internal/saga).

## Testing

[test](https://github.com/kozmod/oniontx/tree/master/test) package contains useful examples for creating unit test:

- [vektra/mockery **+** stretchr/testify](https://github.com/kozmod/oniontx/tree/main/test/integration/internal/mock/mockery)
- [go.uber.org/mock/gomock **+** stretchr/testify](https://github.com/kozmod/oniontx/tree/main/test/integration/internal/mock/gomock)
- [gojuno/minimock **+** stretchr/testify](https://github.com/kozmod/oniontx/tree/main/test/integration/internal/mock/minimock)