Skip to content

Commit

Permalink
Feat/support custom fk (#42)
Browse files Browse the repository at this point in the history
* feat: support custom foreign key

* test: add more test case

* feat: update readme

* refactor: change tag name

* feat: update readme

---------

Co-authored-by: Eyo Chen <[email protected]>
  • Loading branch information
eyo-chen and Eyo Chen authored Dec 15, 2024
1 parent a07937a commit b960a78
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 38 deletions.
10 changes: 6 additions & 4 deletions association.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type fkRef struct {
tableName string
fieldName string
foreignField string
fkName string
}

// nodeInfo is used to store the information of a node for later reference.
Expand Down Expand Up @@ -131,7 +132,7 @@ func (f *Factory[T]) insertAssocNode(ctx context.Context, nodes []assocNode) ([]
}

// set the foreign key field
if err := setForeignKey(v, dep.fieldName, d); err != nil {
if err := setForeignKey(v, dep.fieldName, d, dep.fkName); err != nil {
return nil, err
}
if dep.foreignField != "" {
Expand Down Expand Up @@ -259,6 +260,7 @@ func (f *Factory[T]) genAssocNodes(nodeInfoMap map[string]nodeInfo) ([]assocNode
tableName: t.tableName,
fieldName: t.fieldName,
foreignField: t.foreignField,
fkName: t.fkName,
})

// e.g. User(fk) -> SubCategory
Expand All @@ -281,7 +283,7 @@ func (f *Factory[T]) genAssocNodes(nodeInfoMap map[string]nodeInfo) ([]assocNode
}

// setForeignKey sets the value of the source's ID field to the target's foreign key(name) field
func setForeignKey(target interface{}, name string, source interface{}) error {
func setForeignKey(target interface{}, name string, source interface{}, fkName string) error {
targetField := reflect.ValueOf(target).Elem().FieldByName(name)
if !targetField.IsValid() {
return fmt.Errorf("%s: %w", name, errFieldNotFound)
Expand All @@ -291,9 +293,9 @@ func setForeignKey(target interface{}, name string, source interface{}) error {
return fmt.Errorf("%s: %w", name, errFieldCantSet)
}

sourceIDField := reflect.ValueOf(source).Elem().FieldByName("ID")
sourceIDField := reflect.ValueOf(source).Elem().FieldByName(fkName)
if !sourceIDField.IsValid() {
return fmt.Errorf("%s: %w", "ID", errFieldNotFound)
return fmt.Errorf("%s: %w", fkName, errFieldNotFound)
}

sourceIDKind := sourceIDField.Kind()
Expand Down
93 changes: 74 additions & 19 deletions gofacto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,12 @@ func setIDField(val reflect.Value) error {

// testAssocStruct is a struct with a foreign key to test the association functionality.
type testAssocStruct struct {
ID int
ForeignKey int `gofacto:"foreignKey,struct:testStructWithID,field:ForeignValue"`
ForeignKey2 *int `gofacto:"foreignKey,struct:testStructWithID2,field:ForeignValue2,table:test_struct_with_id2s"`
ForeignValue testStructWithID
ForeignValue2 *testStructWithID2
ID int
ForeignKey int `gofacto:"foreignKey,struct:testStructWithID,field:ForeignValue"`
ForeignKey2 *int `gofacto:"foreignKey,struct:testStructWithID2,field:ForeignValue2,table:test_struct_with_id2s"`
CustomForeignKey int `gofacto:"foreignKey,struct:testStructWithCustomFK,refField:OtherID"`
ForeignValue testStructWithID
ForeignValue2 *testStructWithID2
}

// testStructWithID is a struct with an ID field to test the insert functionality.
Expand All @@ -91,6 +92,11 @@ type testStructWithID3 struct {
Name string
}

type testStructWithCustomFK struct {
ID int
OtherID int
}

type testStructWithCycle struct {
ID int
ForeignKey int `gofacto:"foreignKey,struct:testStructWithCycle2"`
Expand Down Expand Up @@ -2575,18 +2581,19 @@ func setZero_OnBuilderListMany(t *testing.T) {

func TestWithOne(t *testing.T) {
for _, fn := range map[string]func(*testing.T){
"when on builder, insert successfully": withOne_OnBuilder,
"when on builder with multi level, insert successfully": withOne_OnBuilderMultiLevel,
"when on builder not pass ptr, return error": withOne_OnBuilderNotPassPtr,
"when on builder not pass struct, return error": withOne_OnBuilderNotPassStruct,
"when on builder with err, return error": withOne_OnBuilderWithErr,
"when on builder with cycle, return error": withOne_OnBuilderWithCycle,
"when on builder list, insert successfully": withOne_OnBuilderList,
"when on builder list with multi level, insert successfully": withOne_OnBuilderListMultiLevel,
"when on builder list not pass ptr, return error": withOne_OnBuilderListNotPassPtr,
"when on builder list not pass struct, return error": withOne_OnBuilderListNotPassStruct,
"when on builder list with err, return error": withOne_OnBuilderListWithErr,
"when on builder list with cycle, return error": withOne_OnBuilderListWithCycle,
"when on builder, insert successfully": withOne_OnBuilder,
"when on builder with multi level, insert successfully": withOne_OnBuilderMultiLevel,
"when on builder not pass ptr, return error": withOne_OnBuilderNotPassPtr,
"when on builder not pass struct, return error": withOne_OnBuilderNotPassStruct,
"when on builder with err, return error": withOne_OnBuilderWithErr,
"when on builder with cycle, return error": withOne_OnBuilderWithCycle,
"when on builder with wrong custom foreign key, return error": withOne_OnBuilderWithWrongCustomFK,
"when on builder list, insert successfully": withOne_OnBuilderList,
"when on builder list with multi level, insert successfully": withOne_OnBuilderListMultiLevel,
"when on builder list not pass ptr, return error": withOne_OnBuilderListNotPassPtr,
"when on builder list not pass struct, return error": withOne_OnBuilderListNotPassStruct,
"when on builder list with err, return error": withOne_OnBuilderListWithErr,
"when on builder list with cycle, return error": withOne_OnBuilderListWithCycle,
} {
t.Run(testutils.GetFunName(fn), func(t *testing.T) {
fn(t)
Expand All @@ -2599,7 +2606,12 @@ func withOne_OnBuilder(t *testing.T) {

assVal := testStructWithID{}
assVal2 := testStructWithID2{}
val, err := f.Build(mockCTX).WithOne(&assVal).WithOne(&assVal2).Insert()
assVal3 := testStructWithCustomFK{}
val, err := f.Build(mockCTX).
WithOne(&assVal).
WithOne(&assVal2).
WithOne(&assVal3).
Insert()
if err != nil {
t.Fatalf("unexpected error %v", err)
}
Expand All @@ -2619,6 +2631,10 @@ func withOne_OnBuilder(t *testing.T) {
if err := testutils.CompareVal(val.ForeignValue2, &assVal2); err != nil {
t.Fatal(err.Error())
}

if val.CustomForeignKey != assVal3.OtherID {
t.Fatalf("foreignKey should be %v", assVal3.OtherID)
}
}

func withOne_OnBuilderMultiLevel(t *testing.T) {
Expand Down Expand Up @@ -2720,6 +2736,31 @@ func withOne_OnBuilderWithCycle(t *testing.T) {
}
}

func withOne_OnBuilderWithWrongCustomFK(t *testing.T) {
type parent struct {
ID int `gofacto:"foreignKey,struct:child,refField:WrongFk"`
}

type child struct {
ID int
OtherID int
}

f := New(parent{}).WithDB(&mockDB{})

want := parent{}
wantErr := errFieldNotFound

val, err := f.Build(mockCTX).WithOne(&child{}).Insert()
if !errors.Is(err, wantErr) {
t.Fatalf("error should be %v", wantErr)
}

if err := testutils.CompareVal(val, want); err != nil {
t.Fatal(err.Error())
}
}

func withOne_OnBuilderList(t *testing.T) {
f := New(testAssocStruct{}).WithDB(&mockDB{})

Expand Down Expand Up @@ -2889,7 +2930,13 @@ func withMany_CorrectCase(t *testing.T) {
assVal2 := testStructWithID{}
assVal3 := testStructWithID2{}
assVal4 := testStructWithID2{}
vals, err := f.BuildList(mockCTX, 2).WithMany([]interface{}{&assVal1, &assVal2}).WithMany([]interface{}{&assVal3, &assVal4}).Insert()
assVal5 := testStructWithCustomFK{}
assVal6 := testStructWithCustomFK{}
vals, err := f.BuildList(mockCTX, 2).
WithMany([]interface{}{&assVal1, &assVal2}).
WithMany([]interface{}{&assVal3, &assVal4}).
WithMany([]interface{}{&assVal5, &assVal6}).
Insert()
if err != nil {
t.Fatalf("unexpected error %v", err)
}
Expand Down Expand Up @@ -2925,6 +2972,14 @@ func withMany_CorrectCase(t *testing.T) {
if err := testutils.CompareVal(vals[1].ForeignValue2, &assVal4); err != nil {
t.Fatal(err.Error())
}

if vals[0].CustomForeignKey != assVal5.OtherID {
t.Fatalf("CustomForeignKey should be %v", assVal5.OtherID)
}

if vals[1].CustomForeignKey != assVal6.OtherID {
t.Fatalf("CustomForeignKey should be %v", assVal6.OtherID)
}
}

func withMany_MultiLevel(t *testing.T) {
Expand Down
48 changes: 36 additions & 12 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,11 +196,7 @@ type Order struct {
Amount float64
}
```
The format of the tag is following:<br>
`gofacto:"foreignKey,struct:{{structName}},table:{{tableName}},field:{{fieldName}}"`<br>
- `struct` is the name of the associated struct. It is required.<br>
- `table` is the name of the table. It is optional, the snake case of the struct name(s) will be used if not provided.<br>
- `field` is the name of the foreign value fields within the struct. It is optional.
You can find more details about the tag format in [foreignKey tag](#foreignkey-tag).

```go
// build an order with one customer
Expand Down Expand Up @@ -287,8 +283,6 @@ Find out more [examples](https://github.com/eyo-chen/gofacto/blob/main/examples/
}
</details>



### Reset
Use `Reset` method to reset the factory.
```go
Expand All @@ -299,7 +293,7 @@ factory.Reset()
&nbsp;

### Set Configurations
#### WithBlueprint
### WithBlueprint
Use `WithBlueprint` method to set the blueprint function which is a clients defined function to generate the struct values.
```go
func blueprint(i int) *Order {
Expand All @@ -319,7 +313,7 @@ The signature of the blueprint function is following:<br>

Find out more [examples](https://github.com/eyo-chen/gofacto/blob/main/examples/blueprint_test.go).

#### WithStorageName
### WithStorageName
Use `WithStorageName` method to set the storage name.
```go
factory := gofacto.New(Order{}).
Expand All @@ -332,7 +326,7 @@ When using NoSQL databases, the storage name is the collection name. <br>

It is optional, the snake case of the struct name(s) will be used if not provided.<br>

#### WithDB
### WithDB
Use `WithDB` method to set the database connection.
```go
factory := gofacto.New(Order{}).
Expand All @@ -343,7 +337,7 @@ When using raw PostgreSQL, use `postgresf` package. <br>
When using MongoDB, use `mongof` package. <br>
When using GORM, use `gormf` package. <br>

#### WithIsSetZeroValue
### WithIsSetZeroValue
Use `WithIsSetZeroValue` method to set if the zero values are set.
```go
factory := gofacto.New(Order{}).
Expand All @@ -353,7 +347,37 @@ The zero values will not be set when building the struct if the flag is set to f

It is optional, it's true by default.

#### omit tag
### foreignKey tag
In order to build the struct with the associated struct, we need to set the correct tag in the struct to tell gofacto how to build the associated struct.

Suppose we have the following structs:<br>
`Project` struct has a foreign key `EmployeeID` to reference to `Employee` struct.
```go
type Project struct {
ID int
EmployeeID int `gofacto:"foreignKey,struct:Employee,table:employees,field:Employee,refField:OtherID"`
Employee Employee
}

type Employee struct {
ID int
OtherID int
Name string
}
```

The format of the tag is following:<br>
`gofacto:"foreignKey,struct:{{structName}},table:{{tableName}},field:{{fieldName}},refField:{{referenceFieldName}}"`<br>
- `foreignKey` is the tag name. It is required.
- `struct` specifies the name of the associated struct. It is required. In this case, `struct:Employee` indicates that `EmployeeID` is a foreign key to reference to `Employee` struct.
- `table` specifies the table name of the associated struct. It is optional, the snake case and lower case of the struct name(s) will be used if not provided. In this case, `table:employees` indicates that the table name of `Employee` struct is `employees`. However, we can omit it and gofacto will handle it in this example.
- `field` specifies which struct field contains the associated data. It is optional, and it's typically used with gorm. In this example, `field:Employee` indicates that the `Employee` field in the `Project` struct will hold the related `Employee` data after the relationship is loaded.
- `refField` specifies which field to join on in the referenced struct. By default, it joins on the `ID` field, but you can specify a different field. For example, `refField:OtherID` tells gofacto to match `Project.EmployeeID` with `Employee.OtherID` instead of `Employee.ID`.

Find out more [examples](https://github.com/eyo-chen/gofacto/blob/main/examples/association_test.go).


### omit tag
Use `omit` tag in the struct to ignore the field when building the struct.
```go
type Order struct {
Expand Down
21 changes: 18 additions & 3 deletions tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,20 @@ import (
"github.com/eyo-chen/gofacto/internal/utils"
)

const (
defaultFkName = "ID"
tagKeyStruct = "struct"
tagKeyTable = "table"
tagKeyField = "field"
tagKeyRefField = "refField"
)

// tag represents the metadata parsed from the custom tag
type tag struct {
fieldName string
structName string
tableName string
fkName string
foreignField string
omit bool
}
Expand Down Expand Up @@ -85,12 +94,14 @@ func parseTag(field reflect.StructField) (tag, bool, error) {
for _, subPart := range subParts[1:] {
kv := strings.SplitN(subPart, ":", 2)
switch kv[0] {
case "struct":
case tagKeyStruct:
t.structName = kv[1]
case "table":
case tagKeyTable:
t.tableName = kv[1]
case "field":
case tagKeyField:
t.foreignField = kv[1]
case tagKeyRefField:
t.fkName = kv[1]
default:
return tag{}, false, errTagFormat
}
Expand All @@ -101,5 +112,9 @@ func parseTag(field reflect.StructField) (tag, bool, error) {
t.tableName = utils.CamelToSnake(t.structName) + "s"
}

if t.fkName == "" {
t.fkName = defaultFkName
}

return t, true, nil
}

0 comments on commit b960a78

Please sign in to comment.