{"id":21843957,"url":"https://github.com/fsaintjacques/fsjkit","last_synced_at":"2026-02-05T05:01:29.798Z","repository":{"id":159536855,"uuid":"629256439","full_name":"fsaintjacques/fsjkit","owner":"fsaintjacques","description":null,"archived":false,"fork":false,"pushed_at":"2024-03-14T22:03:05.000Z","size":140,"stargazers_count":3,"open_issues_count":4,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-07-09T15:56:02.151Z","etag":null,"topics":["golang","queue-tasks","transactions"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fsaintjacques.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2023-04-18T00:31:42.000Z","updated_at":"2023-10-03T00:53:32.000Z","dependencies_parsed_at":"2025-07-09T15:43:12.983Z","dependency_job_id":"146c48f5-c8b0-4035-b027-fbdde55f4819","html_url":"https://github.com/fsaintjacques/fsjkit","commit_stats":null,"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/fsaintjacques/fsjkit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fsaintjacques%2Ffsjkit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fsaintjacques%2Ffsjkit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fsaintjacques%2Ffsjkit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fsaintjacques%2Ffsjkit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fsaintjacques","download_url":"https://codeload.github.com/fsaintjacques/fsjkit/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fsaintjacques%2Ffsjkit/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29113188,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-05T03:44:17.043Z","status":"ssl_error","status_checked_at":"2026-02-05T03:44:12.077Z","response_time":65,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["golang","queue-tasks","transactions"],"created_at":"2024-11-27T22:17:47.378Z","updated_at":"2026-02-05T05:01:29.780Z","avatar_url":"https://github.com/fsaintjacques.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# fsjkit: fsaintjacques' toolkit\n\nA potpourri of go modules that are useful across projects. Each module tries to \nminimize the number of external dependencies and re-use the standard library \nfacilities as much as possible (database/sql, logging/slog, expvar). All the \ndependencies required for testing are isolated in the `e2e` module which is not \nmeant to be exported.\n\n## Modules\n\n- `tx`: The transaction module adds various wrappers over `sql.Tx`, notably\n  `tx.Transactor` a transaction manager. The transactor supports savepoint with transaction\n  enabling partial rollback of savepoints.\n- `mailbox`: The mailbox module exposes the `Mailbox` and `Consumer`, which allows\n  implementing the [outbox pattern](https://microservices.io/patterns/data/transactional-outbox.html) and more.\n  Multiple middlewares offer robustness via retries, timeouts and dead-letter pattern.\n- `docker`: The docker module exposes a facility to quickly boot various docker \n  containers locally for integration testing purposes. The only dependency is \n  `ory/dockertest` and its transitives dependencies.\n\n### tx: Improved Transactions\n\nThe `tx` module provides a `Transactor` interface that wraps `sql.Tx` and adds\nsavepoints and recursion support. The `Transactor` interface is implemented by \n`sql.DB` and `sql.Conn`. The transactor exposes a single `InTx` method that takes \na closure that will be executed in a transaction. If the closure returns an error,\nthe transaction will be rolled back. If the closure returns nil, the transaction\nwill be committed. If the closure panics, the transaction will be rolled back and\nthe panic will be re-thrown. If `InTx` is called again (recursively) in the closure,\nit will re-use the existing transaction instead of creating a new one, this is done\ntransparently via the `context.Context`.\n\n```go\nfunc (r *Repo) Get(ctx context.Context, id int) (*User, error) {\n    var user User\n    if err := r.transactor.InTx(ctx, func(ctx context.Context, tx *sql.Tx) error {\n        stmt := \"SELECT id, name FROM users WHERE id = $1\"\n        if err := tx.QueryRowContext(ctx, stmt, id).Scan(\u0026user.ID, \u0026user.Name); err != nil {\n            return err\n        }\n\n        return nil\n    }); err != nil {\n        return nil, err\n    }\n\n    return \u0026user, nil\n}\n\nfunc (r *Repo) Update(ctx context.Context, id int, name string) error {\n    return r.transactor.InTx(ctx, func(ctx context.Context, tx *sql.Tx) error {\n        // This will transparently re-use the existing transaction\n        user, err := r.Get(ctx, id)\n        if err != nil {\n            return err\n        }\n\n        const stmt = \"UPDATE users SET name = $1 WHERE id = $2\"\n        if _, err := tx.ExecContext(ctx, stmt, name, id); err != nil {\n            return err\n        }\n\n        return nil\n    })\n}\n```\n\nThe `Transactor` can be instantiated with the `WithSavepoint` option, which will\nenable savepoints. Savepoints are useful to rollback only a part of a transaction\ninstead of the whole thing. This is useful when you want to do multiple operations\nin a single transaction, but you want to be able to rollback only a part of it.\nThis is useful for integration testing, here's an example:\n\n```go\nfunc TestMySvc(t *testing.T) {\n  db, err := sql.Open(\"postgres\", \"...\")\n  transactor := tx.NewTransactor(db, tx.WithSavepoint())\n  // Assume that svc uses transactor.InTx in its methods\n  svc = NewFooSvc(transactor)\n\n  err := transactor.InTx(ctx, func(context.Context, tx *sql.Tx) error {\n    t.Run(\"test1\", func(t *testing.T) {\n      // If CreateFoo fails, the savepoint opened inside will be rolled back.\n      // This will *not* cancel the transaction. This is useful if CreateFoo\n      // inserts data in a table that is used by other tests. If savepoints \n      // weren't used, either the whole transaction would be rolled back, or\n      // the data would be left in the database and affect other tests.\n      svc.CreateFoo(ctx, \"bar\")\n    })\n\n    t.Run(\"test2\", func(t *testing.T) {\n      // No side effects of test1 will be visible here (assuming CreateFoo failed)\n      ...\n    })\n\n    // The side effects of test1 and test2 will be rolled back and not visible to\n    // other tests.\n    return errors.New(\"rollback\")\n  })\n}\n```\n\n### mailbox: Transactional outbox\n\nThe `mailbox` module provides a facility to implement the outbox pattern; it allows\ndeferring asynchronous operations to a later time in a transactional manner. For example,\ntriggering a webhook or sending an email. The module provides a `Mailbox` interface to put\nmessages in the outbox, and a `Consumer` interface to consume messages from the outbox.\n\nMessages are always put in the outbox in the context of a transaction. A `Consumer` runs\nin a separate goroutine and consumes messages from the outbox. The `Consumer` is responsible\nfor executing the message and marking it as done. If the message fails, it can be retried\nlater.\n\n### docker: Docker services for integration testing\n\nThe `docker` modules allows quickly interfacing with databases and remote services \nthat are available via docker images. This can make integration testing as easy as\nunit tests; start a docker image and the client to it. Here's an example of how to use \npostgres locally:\n\n```go\nimport (\n\t\"testing\"\n\n\t\"github.com/fsaintjacques/fsjkit/docker\"\n\n\t_ \"github.com/jackc/pgx/v5/stdlib\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar (\n\tpgCfg = docker.PostgresServiceConfig{Repository: \"postgres\", Tag: \"15\", Database: \"test\", Driver: \"pgx\"}\n\tpgSvc = docker.NewPostgresService(pgCfg)\n)\n\nfunc TestPostgresService(t *testing.T) {\n\tdb, err := pgSvc.Open()\n\trequire.NoError(t, err)\n\trequire.NotNil(t, db)\n\n\t_, err = db.Exec(\"SELECT 1;\")\n\trequire.NoError(t, err)\n}\n\nfunc TestMain(m *testing.M) {\n\tdocker.MainWithServices(m, pgSvc)\n}\n```\n\n## Testing\n\nTests (unit, integration) are found in the e2e module; the goal is to\nminimize the module dependencies. Because go modules doesn't support specifying\ntesting dependencies, this module takes that hit. It is not meant to be imported by\nexternal packages.  For example, most tests depends on [testify](https://github.com/stretchr/testify) or\n[dockertest](https://github.com/ory/dockertest) which can pull a lot of dependencies.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffsaintjacques%2Ffsjkit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffsaintjacques%2Ffsjkit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffsaintjacques%2Ffsjkit/lists"}