commit dc4b1cf031217fd64282e550567985f7d59aed5c Author: mya Date: Sun Jan 28 15:19:30 2024 -0600 basic implementation diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2111764 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2024 Mya Pitzeruse + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..75162b1 --- /dev/null +++ b/Makefile @@ -0,0 +1,32 @@ +define HELP_TEXT +Welcome! + +Targets: +help provides help text +deps download dependencies +deps/upgrade upgrade dependencies +deps/tidy tidy dependencies +test run tests +legal prepends legal header to source code + +endef +export HELP_TEXT + +help: + @echo "$$HELP_TEXT" + +deps: + go mod download + +deps/upgrade: + go get -u ./... + +deps/tidy: + go mod tidy + +test: + @go test -v -race -covermode=atomic -coverprofile=coverage.txt -coverpkg=./... ./... + +legal: .legal +.legal: + @git ls-files | xargs -I{} addlicense -f ./legal/header.txt -skip yaml -skip yml {} diff --git a/README.md b/README.md new file mode 100644 index 0000000..646b627 --- /dev/null +++ b/README.md @@ -0,0 +1,128 @@ +# gorm-crud + +Mostly developed for me. Templates basic CRUD operations using Go Generics. + +## Usage + +```shell +go get go.pitz.tech/gorm/crud +``` + +### Basic + +The basic usage of this library is as follows. This example demonstrates usage for a single resource type, however this +pattern can apply to multiple resources. + +```go +package main + +import ( + "gorm.io/gorm" + + "go.pitz.tech/gorm/crud" +) + +// Resource is an annotated database model. +type Resource struct{} + +// ResourceDB provides an abstraction for managing Resource data in the associated gorm.DB. +type ResourceDB struct { + List crud.ListFunc[Resource] + Create crud.CreateFunc[Resource] + Get crud.GetFunc[Resource] + Update crud.UpdateFunc[Resource] + Delete crud.DeleteFunc +} + +func main() { + // todo: open your database + var db *gorm.DB + + resources := &ResourceDB{ + List: crud.Lister[Resource](db), + Create: crud.Creator[Resource](db), + Get: crud.Getter[Resource](db), + Update: crud.Updater[Resource](db), + Delete: crud.Deleter[Resource](db), + } + + // data, err := resources.List(ctx, 0, 10) +} +``` + +### Transactions + +Directly managing transactions is particularly useful when you want to manage multiple resources in a single operation. + +```go +package main + +import ( + "context" + + "gorm.io/gorm" + "go.pitz.tech/gorm/crud" +) + +type Resource1 struct {} +type Resource2 struct {} + +type Resource1DB struct { + Create crud.CreateFunc[Resource1] +} + +type Resource2DB struct { + Create crud.CreateFunc[Resource2] +} + +func main() { + // todo: open your database + var db *gorm.DB + var resource1db *Resource1DB + var resource2db *Resource1DB + + // optionally specify transaction options + txn := db.Begin() + defer txn.Rollback() + + ctx := crud.Transaction(context.Background(), txn) + + err := resource1db.Create(ctx, &Resource1{}) + if err != nil { + // resource 1 and 2 do not exist! + return + } + + err = resource2db.Create(ctx, &Resource2{}) + if err != nil { + // resource 1 and 2 do not exist! + return + } + + err = txn.Commit().Error + if err != nil { + // resource 1 and 2 do not exist! + return + } + + // resource 1 and 2 exist! +} +``` + +### Extending + +These methods can easily be customized by providing concrete method definitions in the structure themselves. For +example, suppose a `UserDB` wanted to support a `GetByEmail` operation. Such a method might explicitly ask for the email +address and call a `Get` function with the explicit field as a filter. For instance: + +```go +func (db *UserDB) GetByEmail(ctx context.Context, email string) (*User, error) { + return db.Get(ctx, map[string]interface{}{ + "email": email, + }) +} +``` + +## License + +`MIT`. See [LICENSE](LICENSE) for more details. diff --git a/context.go b/context.go new file mode 100644 index 0000000..55fc76c --- /dev/null +++ b/context.go @@ -0,0 +1,29 @@ +// Copyright (C) 2024 Mya Pitzeruse +// SPDX-License-Identifier: MIT + +package crud + +import ( + "context" + + "gorm.io/gorm" +) + +type contextKey string + +const transaction contextKey = "go.pitz.tech/gorm/crud/txn" + +// Transaction returns the transaction associated with the context. If none is present, the db is used. +func Transaction(ctx context.Context, db *gorm.DB) *gorm.DB { + txn, ok := ctx.Value(transaction).(*gorm.DB) + if ok { + return txn + } + + return db +} + +// WithTransaction attaches the current transaction to provided context. +func WithTransaction(ctx context.Context, db *gorm.DB) context.Context { + return context.WithValue(ctx, transaction, db) +} diff --git a/creator.go b/creator.go new file mode 100644 index 0000000..90d849b --- /dev/null +++ b/creator.go @@ -0,0 +1,26 @@ +// Copyright (C) 2024 Mya Pitzeruse +// SPDX-License-Identifier: MIT + +package crud + +import ( + "context" + + "gorm.io/gorm" +) + +// CreateFunc defines the method signature of the function returned by the Creator method. It simplifies the operation +// of creating records in the database. +type CreateFunc[T any] func(ctx context.Context, record *T) error + +// Creator returns a function that can create data using gorm db. +func Creator[T any](db *gorm.DB) CreateFunc[T] { + prototype := new(T) + + return func(ctx context.Context, record *T) error { + return Transaction(ctx, db). + Model(prototype). + Create(record). + Error + } +} diff --git a/deleter.go b/deleter.go new file mode 100644 index 0000000..7a52494 --- /dev/null +++ b/deleter.go @@ -0,0 +1,30 @@ +// Copyright (C) 2024 Mya Pitzeruse +// SPDX-License-Identifier: MIT + +package crud + +import ( + "context" + + "gorm.io/gorm" +) + +// DeleteFunc defines the signature of the function returned by Deleter. It simplifies the interface for deleting data +// using gorm. +type DeleteFunc func(ctx context.Context, filters ...map[string]interface{}) error + +// Deleter returns a function that can delete data using gorm db. +func Deleter[T any](db *gorm.DB) DeleteFunc { + prototype := new(T) + + return func(ctx context.Context, filters ...map[string]interface{}) error { + q := Transaction(ctx, db). + Model(prototype) + + for _, filter := range filters { + q = q.Where(filter) + } + + return q.Delete(prototype).Error + } +} diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..5ab9374 --- /dev/null +++ b/doc.go @@ -0,0 +1,5 @@ +// Copyright (C) 2024 Mya Pitzeruse +// SPDX-License-Identifier: MIT + +// Package crud provides handlers that streamline database operations using generics in Go. +package crud diff --git a/getter.go b/getter.go new file mode 100644 index 0000000..017f745 --- /dev/null +++ b/getter.go @@ -0,0 +1,37 @@ +// Copyright (C) 2024 Mya Pitzeruse +// SPDX-License-Identifier: MIT + +package crud + +import ( + "context" + + "gorm.io/gorm" +) + +// GetFunc defines the signature of the function returned by Getter. It streamlines the process of reading single +// records from the database. +type GetFunc[T any] func(ctx context.Context, filters ...map[string]interface{}) (*T, error) + +// Getter returns a function that can read data from a gorm database. +func Getter[T any](db *gorm.DB) GetFunc[T] { + prototype := new(T) + + return func(ctx context.Context, filters ...map[string]interface{}) (*T, error) { + q := Transaction(ctx, db). + Model(prototype) + + for _, filter := range filters { + q = q.Where(filter) + } + + body := new(T) + + err := q.First(body).Error + if err != nil { + return nil, err + } + + return body, nil + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..144c6e1 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module go.pitz.tech/gorm/crud + +go 1.20 + +require gorm.io/gorm v1.25.6 + +require ( + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..515ee0e --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +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/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +gorm.io/gorm v1.25.6 h1:V92+vVda1wEISSOMtodHVRcUIOPYa2tgQtyF+DfFx+A= +gorm.io/gorm v1.25.6/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= diff --git a/legal/header.txt b/legal/header.txt new file mode 100644 index 0000000..f26b085 --- /dev/null +++ b/legal/header.txt @@ -0,0 +1,2 @@ +Copyright (C) {{ .Year }} Mya Pitzeruse +SPDX-License-Identifier: MIT diff --git a/lister.go b/lister.go new file mode 100644 index 0000000..f7119c8 --- /dev/null +++ b/lister.go @@ -0,0 +1,39 @@ +// Copyright (C) 2024 Mya Pitzeruse +// SPDX-License-Identifier: MIT + +package crud + +import ( + "context" + + "gorm.io/gorm" +) + +// ListFunc defines a function signature used to list data from the underlying gorm database. It ensures that size and +// scoping of queries is taken into consideration. +type ListFunc[T any] func(ctx context.Context, offset, count int, filters ...map[string]interface{}) ([]T, error) + +// Lister returns a ListFunc to be used to list data from gorm. +func Lister[T any](db *gorm.DB) ListFunc[T] { + prototype := new(T) + + return func(ctx context.Context, offset, count int, filters ...map[string]interface{}) ([]T, error) { + q := Transaction(ctx, db). + Model(prototype). + Offset(offset). + Limit(count) + + for _, filter := range filters { + q = q.Where(filter) + } + + result := make([]T, 0, count) + + err := q.Find(&result).Error + if err != nil { + return nil, err + } + + return result, nil + } +} diff --git a/updater.go b/updater.go new file mode 100644 index 0000000..eb2a739 --- /dev/null +++ b/updater.go @@ -0,0 +1,30 @@ +// Copyright (C) 2024 Mya Pitzeruse +// SPDX-License-Identifier: MIT + +package crud + +import ( + "context" + + "gorm.io/gorm" +) + +// UpdateFunc defines a signature that is used by Updater to return a function that can update data in the database. +type UpdateFunc[T any] func(ctx context.Context, patch T, filters ...map[string]interface{}) error + +// Updater returns a function used to update information in the database. +func Updater[T any](db *gorm.DB) UpdateFunc[T] { + prototype := new(T) + + return func(ctx context.Context, patch T, filters ...map[string]interface{}) error { + q := Transaction(ctx, db). + Model(prototype). + Limit(1) + + for _, filter := range filters { + q = q.Where(filter) + } + + return q.Updates(patch).Error + } +}