From dc4b1cf031217fd64282e550567985f7d59aed5c Mon Sep 17 00:00:00 2001 From: mya Date: Sun, 28 Jan 2024 15:19:30 -0600 Subject: [PATCH] basic implementation --- LICENSE | 19 +++++++ Makefile | 32 ++++++++++++ README.md | 128 +++++++++++++++++++++++++++++++++++++++++++++++ context.go | 29 +++++++++++ creator.go | 26 ++++++++++ deleter.go | 30 +++++++++++ doc.go | 5 ++ getter.go | 37 ++++++++++++++ go.mod | 10 ++++ go.sum | 6 +++ legal/header.txt | 2 + lister.go | 39 +++++++++++++++ updater.go | 30 +++++++++++ 13 files changed, 393 insertions(+) create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 context.go create mode 100644 creator.go create mode 100644 deleter.go create mode 100644 doc.go create mode 100644 getter.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 legal/header.txt create mode 100644 lister.go create mode 100644 updater.go 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 + } +}