1

basic implementation

This commit is contained in:
Mya 2024-01-28 15:19:30 -06:00
commit dc4b1cf031
Signed by: mya
GPG Key ID: 4DFBA0F91AC3544A
13 changed files with 393 additions and 0 deletions

19
LICENSE Normal file
View File

@ -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.

32
Makefile Normal file
View File

@ -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 {}

128
README.md Normal file
View File

@ -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.

29
context.go Normal file
View File

@ -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)
}

26
creator.go Normal file
View File

@ -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
}
}

30
deleter.go Normal file
View File

@ -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
}
}

5
doc.go Normal file
View File

@ -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

37
getter.go Normal file
View File

@ -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
}
}

10
go.mod Normal file
View File

@ -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
)

6
go.sum Normal file
View File

@ -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=

2
legal/header.txt Normal file
View File

@ -0,0 +1,2 @@
Copyright (C) {{ .Year }} Mya Pitzeruse
SPDX-License-Identifier: MIT

39
lister.go Normal file
View File

@ -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
}
}

30
updater.go Normal file
View File

@ -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
}
}