Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/content/docs/3-repositories.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ the name of the function field and **automatically generates the GORM query code
A query method name must follow a strict grammatical structure:
`[Prefix][Subject]By[Conditions]OrderBy[SortRules]`

1. **Prefix:** Must be `find`, `Find`, `query`, or `Query`.
1. **Prefix:** Must be `find`, `Find`, `query`, `Query`, `list`, `List`, `stream`, or `Stream`.
2. **Subject:** (Optional) Capitalized words describing what is being found (e.g., `ActiveUsers`).
3. **Separator:** Must include `By`.
4. **Conditions:** Field names followed by an optional Operator, separated by `And` or `Or`.
Expand Down Expand Up @@ -69,7 +69,7 @@ entirely protected against SQL injection attacks by default.

Your generated function can define the following return signatures:
* **Single Entity:** `*T` or `Optional[*T]`
* **Multiple Entities:** `[]*T`
* **Multiple Entities:** `[]T` or `Stream[T]` (Java 8-style lazy Stream from `github.com/BeamFoundry/rays/pkg/util/stream`)
* **Query Builder:** `*gorm.DB` (Returns the query before mapping to an object)
* **Error Handling:** You may append `Error` as a second return parameter to any of the above to catch database faults.

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/BeamFoundry/rays-orm
go 1.27

require (
github.com/BeamFoundry/rays v0.2.2
github.com/BeamFoundry/rays v0.3.0
github.com/onsi/ginkgo/v2 v2.33.1
github.com/onsi/gomega v1.44.0
github.com/orandin/slog-gorm v1.4.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
github.com/BeamFoundry/rays v0.2.2 h1:jgh5fGon3pRa1RedXQZil3r1FWfY0YzhLx728sNdLaY=
github.com/BeamFoundry/rays v0.2.2/go.mod h1:5wiQwphroNUY4tWjLrKjoV4B/pHb4ldh8XUFSFnzvQo=
github.com/BeamFoundry/rays v0.3.0 h1:cNAkHcIRfBbHEj9ZhJl0VOw5cft8jdA+y8R5h1/qGUc=
github.com/BeamFoundry/rays v0.3.0/go.mod h1:5wiQwphroNUY4tWjLrKjoV4B/pHb4ldh8XUFSFnzvQo=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
Expand Down
56 changes: 56 additions & 0 deletions pkg/core/core_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
. "github.com/BeamFoundry/rays/pkg/core"
. "github.com/BeamFoundry/rays/pkg/lang"
. "github.com/BeamFoundry/rays/pkg/testing"
. "github.com/BeamFoundry/rays/pkg/util/stream"
)

func TestOrm(t *testing.T) {
Expand Down Expand Up @@ -69,6 +70,9 @@ type UserRepositoryInterface interface {
// FindByUserNameWithAnimals(string) *User
// FindByUserNameWithAnimalsOrderByName(string) *User
FindAllByName(string) []User
FindStreamByName(string) Stream[User]
StreamAllByName(string) Stream[User]
FindStreamWithErrorByName(string) (Stream[User], Error)
FindOneByName(string) Optional[*User]
FindAllByAgeIn([]uint) []User
FindAllByUserNameAndAge(string, uint) []User
Expand All @@ -88,6 +92,9 @@ type UserRepository struct {
// findByUserNameWithAnimals func(string) *User
// findByUserNameWithAnimalsOrderByName func(string) *User
findAllByName func(string) []User
findStreamByName func(string) Stream[User]
streamAllByName func(string) Stream[User]
findStreamWithErrorByName func(string) (Stream[User], Error)
findOneByName func(string) Optional[*User]
findAllByAgeIn func([]uint) []User
findAllByUserNameAndAge func(string, uint) []User
Expand Down Expand Up @@ -120,6 +127,7 @@ type UserRepository struct {
findBadPointerByName func(string) *Foo
findBadStructByName func(string) Foo
findBadSliceByName func(string) []Foo
findBadStreamByName func(string) Stream[Foo]
findBadElemByName func(string) string
// findDistinctByName func(string) []User
findByNameOrderByFoo func(string) []User
Expand All @@ -142,6 +150,18 @@ func (this *UserRepository) FindWithErrorByUserName(name string) (*User, Error)
return this.findWithErrorByUserName(name)
}

func (this *UserRepository) FindStreamByName(name string) Stream[User] {
return this.findStreamByName(name)
}

func (this *UserRepository) StreamAllByName(name string) Stream[User] {
return this.streamAllByName(name)
}

func (this *UserRepository) FindStreamWithErrorByName(name string) (Stream[User], Error) {
return this.findStreamWithErrorByName(name)
}

/*
func (this *UserRepository) FindByUserNameWithAnimals(name string) *User {
return this.findByUserNameWithAnimals(name)
Expand Down Expand Up @@ -249,6 +269,42 @@ var _ = Describe("UserRepository", func() {
Expect(u.Name).To(Equal("Dorothy"))
})

It("can find many records returning a Stream", func() {
u := []*User{
{Name: "Dorothy", UserName: "dorothy2", Age: 30},
{Name: "Dorothy", UserName: "dorothy3", Age: 40},
}
_, err := userRepository.SaveAll(u)
Expect(err).NotTo(HaveOccurred())

st := userRepository.FindStreamByName("Dorothy")
names := st.Filter(func(user User) bool { return user.Age >= 30 }).
SortedBy(func(user User) int { return int(user.Age) }).
Map(func(user User) string { return user.UserName }).
ToSlice()
Expect(names).To(Equal([]string{"dorothy2", "dorothy3"}))
})

It("can use Stream prefix to return a Stream", func() {
st := userRepository.StreamAllByName("Dorothy")
count := st.Count()
Expect(count).To(BeNumerically("==", 1))
})

It("can return a Stream with Error", func() {
st, err := userRepository.FindStreamWithErrorByName("Dorothy")
Expect(err).NotTo(HaveOccurred())
Expect(st.Count()).To(BeNumerically("==", 1))

stEmpty, err := userRepository.FindStreamWithErrorByName("NonExistent")
Expect(err).NotTo(HaveOccurred())
Expect(stEmpty.Count()).To(BeNumerically("==", 0))
})

It("rejects invalid Stream return type with mismatched entity", func() {
Expect(userRepository.findBadStreamByName).To(BeNil())
})

It("can find optionally a record", func() {
optional := userRepository.FindOneByName("Dorothy")
u, ok := optional.Get()
Expand Down
21 changes: 15 additions & 6 deletions pkg/core/query_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
. "github.com/BeamFoundry/rays/pkg/core"
. "github.com/BeamFoundry/rays/pkg/lang"
. "github.com/BeamFoundry/rays/pkg/util"
. "github.com/BeamFoundry/rays/pkg/util/stream"
"gorm.io/gorm"
)

Expand Down Expand Up @@ -118,8 +119,8 @@ func (this *gormQueryBuilder[T, ID]) Build(function *reflect.StructField) (*gorm
name := CaseUtils(function.Name).Capitalize().String()

// 1. Verify Prefix
if !String(name).StartsWith("Find") && !String(name).StartsWith("Query") && !String(name).StartsWith("List") {
return nil, NewError("IllegalArgumentError", "Function name must start with Find, Query, find, or query")
if !String(name).StartsWith("Find") && !String(name).StartsWith("Query") && !String(name).StartsWith("List") && !String(name).StartsWith("Stream") {
return nil, NewError("IllegalArgumentError", "Function name must start with Find, Query, List, or Stream")
}

// 2. Find the "By" separator
Expand Down Expand Up @@ -233,9 +234,10 @@ func (this *gormQuery[T, ID]) validate() Error {
sliceTType := reflect.SliceOf(entity)
optionalTType := reflect.TypeFor[Optional[*T]]()
gormType := reflect.TypeFor[*gorm.DB]()
streamTType := reflect.TypeFor[Stream[T]]()

if retType != ptrTType && retType != sliceTType && retType != optionalTType && retType != gormType {
return NewError("IllegalArgumentError", "Invalid return type; must be *T, []T, Optional[*T] or *gorm.DB")
if retType != ptrTType && retType != sliceTType && retType != optionalTType && retType != gormType && retType != streamTType {
return NewError("IllegalArgumentError", "Invalid return type; must be *T, []T, Optional[*T], Stream[T] or *gorm.DB")
}

this.logger.Info("Created repository function", "entity", entity, "function", this.function.Name)
Expand All @@ -247,6 +249,7 @@ func (this *gormQuery[T, ID]) Query(tx GormTransactional) func(in []reflect.Valu
one := output == reflect.TypeFor[*T]()
multi := output.Kind() == reflect.Slice
optional := output == reflect.TypeFor[Optional[*T]]()
stream := output == reflect.TypeFor[Stream[T]]()

var hasError bool
if this.function.Type.NumOut() > 1 && this.function.Type.Out(1) == reflect.TypeFor[Error]() {
Expand Down Expand Up @@ -292,11 +295,17 @@ func (this *gormQuery[T, ID]) Query(tx GormTransactional) func(in []reflect.Valu
ret := []reflect.Value{}
var res *gorm.DB

if multi {
switch {
case multi:
var t []T
res = db.Find(&t)
ret = append(ret, reflect.ValueOf(t))
} else {
case stream:
var t []T
res = db.Find(&t)
s := Stream[T]{}.FromSlice(t)
ret = append(ret, reflect.ValueOf(s))
default:
var t T
res = db.First(&t)

Expand Down
Loading