repo

package
v1.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 7, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

Documentation

Overview

Package repo is a generic, tenant-scoped repository over GORM.

It exists for one reason above all the convenience: the tenant filter is applied by the repository, not by the caller.

Hand-written scoping means every query in the codebase has to remember `.Where("account_id = ?", tenant)`, and the one that forgets does not fail — it returns another customer's rows, silently, forever, until somebody notices. Here, a tenant-scoped repository applies the filter itself and REFUSES a request with no tenant on the context rather than quietly running unscoped. The unsafe operation still exists, spelled Unscoped, because reconcilers genuinely need it; it just cannot happen by omission.

type ProductRepo struct{ *repo.Repository[entity.Product] }

func NewProductRepo(db *gorm.DB) *ProductRepo {
    return &ProductRepo{repo.New[entity.Product](db,
        repo.WithEntityName("product"),
        repo.WithTenantColumn("account_id"),
    )}
}

Index

Constants

View Source
const DefaultTenantColumn = "account_id"

DefaultTenantColumn is the column a tenant-scoped repository filters on.

Variables

This section is empty.

Functions

func InTx

func InTx(ctx context.Context) bool

InTx reports whether ctx is inside a transaction.

func Transaction

func Transaction(ctx context.Context, db *gorm.DB, fn func(ctx context.Context) error) error

Transaction runs fn inside one database transaction.

fn receives a context carrying the transaction, so every repository call it makes joins that transaction automatically. Return an error and everything rolls back; return nil and it commits.

Nesting is handled by JOINING the outer transaction rather than opening a savepoint. That is the behaviour a service wants: when a charge inside a provisioning transaction fails, the resource row must roll back with it, and a savepoint would let the outer work commit without the charge.

func TxFrom

func TxFrom(ctx context.Context, fallback *gorm.DB) *gorm.DB

TxFrom returns the transaction on ctx, or fallback when there is none.

Every repository method routes through this, which is what makes transactions composable: a service calls three repositories inside one Transaction block and all three join it, without any of them taking a *gorm.DB parameter or knowing a transaction exists.

func WithTx

func WithTx(ctx context.Context, tx *gorm.DB) context.Context

WithTx puts a transaction on the context.

Types

type Option

type Option func(cfg *config)

Option configures a Repository.

func Tenanted

func Tenanted() Option

Tenanted is WithTenantColumn(DefaultTenantColumn).

func WithEntityName

func WithEntityName(name string) Option

WithEntityName sets the noun used in error messages: "product", "invoice". Without it, errors say "record", which is true and useless.

func WithSortable

func WithSortable(cols ...string) Option

WithSortable sets the columns List will accept a client sort on.

func WithTenantColumn

func WithTenantColumn(col string) Option

WithTenantColumn makes the repository tenant-scoped on that column.

Setting it changes the safety contract: every method then REQUIRES a tenant on the context and returns an error without one.

type Page

type Page[T any] struct {
	Items []T   `json:"items"`
	Total int64 `json:"total"`
}

Page is one page of results plus the total.

type Repository

type Repository[T any] struct {
	// contains filtered or unexported fields
}

Repository is the data-access surface for one entity type.

func New

func New[T any](db *gorm.DB, opts ...Option) *Repository[T]

New builds a repository.

func (*Repository[T]) All

func (r *Repository[T]) All(ctx context.Context, scopes ...gormx.Scope) ([]T, error)

All returns every matching row, unpaginated.

Use it only where the result set is bounded by design — the regions of a platform, the rules of one security group. For anything a tenant can grow without limit, use List; "it is only a few hundred rows" is a statement about today.

func (*Repository[T]) Count

func (r *Repository[T]) Count(ctx context.Context, scopes ...gormx.Scope) (int64, error)

Count returns the number of matching rows.

func (*Repository[T]) Create

func (r *Repository[T]) Create(ctx context.Context, model *T) error

Create inserts one row.

It sets the tenant column from the context when the repository is scoped, so a caller cannot insert a row under another account by setting the field — the context wins over the payload, always.

func (*Repository[T]) CreateMany

func (r *Repository[T]) CreateMany(ctx context.Context, models []T) error

CreateMany inserts in batches of 200.

func (*Repository[T]) DB

func (r *Repository[T]) DB(ctx context.Context) *gorm.DB

DB returns the session bound to ctx and to the transaction on it, if any.

Use it for a query this repository does not cover. It is NOT tenant-scoped — call Scoped for that.

func (*Repository[T]) Delete

func (r *Repository[T]) Delete(ctx context.Context, scopes ...gormx.Scope) (int64, error)

Delete removes rows matching the scopes (a soft delete when the entity has gorm.DeletedAt). It refuses an empty scope list for the same reason UpdateFields does.

func (*Repository[T]) DeleteByID

func (r *Repository[T]) DeleteByID(ctx context.Context, id uuid.UUID) error

DeleteByID removes one row, reporting NotFound when nothing matched.

func (*Repository[T]) Exists

func (r *Repository[T]) Exists(ctx context.Context, scopes ...gormx.Scope) (bool, error)

Exists reports whether any row matches, without loading one.

func (*Repository[T]) FindByID

func (r *Repository[T]) FindByID(ctx context.Context, id uuid.UUID, scopes ...gormx.Scope) (*T, error)

FindByID returns one row by primary key, scoped to the tenant.

A row belonging to another account returns NotFound, not Forbidden: Forbidden would confirm the id exists to someone who should not know it.

func (*Repository[T]) FindOne

func (r *Repository[T]) FindOne(ctx context.Context, scopes ...gormx.Scope) (*T, error)

FindOne returns the first row matching the scopes.

func (*Repository[T]) List

func (r *Repository[T]) List(ctx context.Context, q pagination.Query, scopes ...gormx.Scope) (Page[T], error)

List returns a page of rows and the unpaginated total.

It runs the COUNT and the SELECT as two queries against the same scopes. That is two round trips rather than one window function — the trade is deliberate: the window-function form breaks the moment a scope adds a JOIN that multiplies rows, and a wrong total is worse than a second query.

func (*Repository[T]) LockByID

func (r *Repository[T]) LockByID(ctx context.Context, id uuid.UUID) (*T, error)

LockByID loads one row FOR UPDATE. It must be called inside a transaction; a row lock outside one is released immediately and protects nothing.

func (*Repository[T]) Save

func (r *Repository[T]) Save(ctx context.Context, model *T) error

Save writes every field of model, including the zero ones.

func (*Repository[T]) Scoped

func (r *Repository[T]) Scoped(ctx context.Context) (*gorm.DB, error)

Scoped returns the session with the tenant filter applied, or an error when the repository is tenant-scoped and the context carries no tenant.

func (*Repository[T]) Sortable

func (r *Repository[T]) Sortable() []string

Sortable returns the allow-list, for a controller to pass to the paginator.

func (*Repository[T]) Unscoped

func (r *Repository[T]) Unscoped(ctx context.Context) *gorm.DB

Unscoped returns the session WITHOUT the tenant filter.

Legitimate callers: a reconciler sweeping every account, a platform report, an admin tool. Every use is a deliberate crossing of the tenancy boundary — make it obvious at the call site and keep it out of request paths.

func (*Repository[T]) UpdateByID

func (r *Repository[T]) UpdateByID(ctx context.Context, id uuid.UUID, fields map[string]any) error

UpdateByID applies a column→value map to one row and reports NotFound when nothing matched — which covers both "no such id" and "not yours".

func (*Repository[T]) UpdateFields

func (r *Repository[T]) UpdateFields(ctx context.Context, fields map[string]any, scopes ...gormx.Scope) (int64, error)

UpdateFields applies a column→value map to the rows matching the scopes, within the tenant.

A map, not a struct, because GORM skips zero-valued struct fields: updating with a struct can never set a column to false, 0 or "". That silent skip is a recurring production defect — a PATCH that disables something appears to succeed and changes nothing.

It REFUSES an empty scope list, because an update with no WHERE rewrites every row the tenant owns.

type TxManager

type TxManager struct {
	// contains filtered or unexported fields
}

TxManager runs transactions against one database. Register it in the container so a service can depend on "a way to run a transaction" without depending on *gorm.DB.

func NewTxManager

func NewTxManager(db *gorm.DB) *TxManager

NewTxManager builds a manager.

func (*TxManager) Do

func (m *TxManager) Do(ctx context.Context, fn func(ctx context.Context) error) error

Do runs fn in a transaction.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL