mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-06-09 23:33:45 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c42fa11c1a |
@@ -235,9 +235,9 @@ func (p *NestedCUDProcessor) injectForeignKeys(data map[string]interface{}, mode
|
|||||||
}
|
}
|
||||||
|
|
||||||
for parentKey, parentID := range parentIDs {
|
for parentKey, parentID := range parentIDs {
|
||||||
dbColName := reflection.GetForeignKeyColumn(modelType, parentKey)
|
dbColNames := reflection.GetForeignKeyColumn(modelType, parentKey)
|
||||||
|
|
||||||
if dbColName == "" {
|
if len(dbColNames) == 0 {
|
||||||
// No explicit tag found — fall back to naming convention by scanning scalar fields.
|
// No explicit tag found — fall back to naming convention by scanning scalar fields.
|
||||||
for i := 0; i < modelType.NumField(); i++ {
|
for i := 0; i < modelType.NumField(); i++ {
|
||||||
field := modelType.Field(i)
|
field := modelType.Field(i)
|
||||||
@@ -248,13 +248,13 @@ func (p *NestedCUDProcessor) injectForeignKeys(data map[string]interface{}, mode
|
|||||||
strings.EqualFold(jsonName, parentKey+"_id") ||
|
strings.EqualFold(jsonName, parentKey+"_id") ||
|
||||||
strings.EqualFold(jsonName, parentKey+"id") ||
|
strings.EqualFold(jsonName, parentKey+"id") ||
|
||||||
strings.EqualFold(field.Name, parentKey+"ID") {
|
strings.EqualFold(field.Name, parentKey+"ID") {
|
||||||
dbColName = reflection.GetColumnName(field)
|
dbColNames = []string{reflection.GetColumnName(field)}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if dbColName != "" {
|
for _, dbColName := range dbColNames {
|
||||||
if _, exists := data[dbColName]; !exists {
|
if _, exists := data[dbColName]; !exists {
|
||||||
logger.Debug("Injecting foreign key: %s = %v", dbColName, parentID)
|
logger.Debug("Injecting foreign key: %s = %v", dbColName, parentID)
|
||||||
data[dbColName] = parentID
|
data[dbColName] = parentID
|
||||||
|
|||||||
@@ -973,21 +973,23 @@ func GetRelationType(model interface{}, fieldName string) RelationType {
|
|||||||
return RelationUnknown
|
return RelationUnknown
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetForeignKeyColumn returns the DB column name of the foreign key that the
|
// GetForeignKeyColumn returns the DB column names of the foreign key(s) that
|
||||||
// relation field identified by parentKey owns on modelType.
|
// the relation field identified by parentKey owns on modelType. Composite keys
|
||||||
|
// (e.g. bun "join:a=b,join:c=d" or GORM "foreignKey:ColA,ColB") yield multiple
|
||||||
|
// entries. Returns nil when no tag is found (caller should fall back to
|
||||||
|
// convention).
|
||||||
//
|
//
|
||||||
// It checks tags in priority order:
|
// It checks tags in priority order:
|
||||||
// 1. Bun join: tag — e.g. `bun:"rel:belongs-to,join:department_id=id"` → "department_id"
|
// 1. Bun join: tag — e.g. `bun:"rel:belongs-to,join:department_id=id"` → ["department_id"]
|
||||||
// 2. GORM foreignKey: tag — e.g. `gorm:"foreignKey:DepartmentID"` → column of DepartmentID field
|
// 2. GORM foreignKey: tag — e.g. `gorm:"foreignKey:DepartmentID"` → [column of DepartmentID field]
|
||||||
// 3. Returns "" when no tag is found (caller should fall back to convention)
|
|
||||||
//
|
//
|
||||||
// parentKey is matched case-insensitively against the field name and JSON tag.
|
// parentKey is matched case-insensitively against the field name and JSON tag.
|
||||||
func GetForeignKeyColumn(modelType reflect.Type, parentKey string) string {
|
func GetForeignKeyColumn(modelType reflect.Type, parentKey string) []string {
|
||||||
for modelType.Kind() == reflect.Ptr || modelType.Kind() == reflect.Slice {
|
for modelType.Kind() == reflect.Ptr || modelType.Kind() == reflect.Slice {
|
||||||
modelType = modelType.Elem()
|
modelType = modelType.Elem()
|
||||||
}
|
}
|
||||||
if modelType.Kind() != reflect.Struct {
|
if modelType.Kind() != reflect.Struct {
|
||||||
return ""
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < modelType.NumField(); i++ {
|
for i := 0; i < modelType.NumField(); i++ {
|
||||||
@@ -999,34 +1001,42 @@ func GetForeignKeyColumn(modelType reflect.Type, parentKey string) string {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bun: join:local_col=foreign_col
|
// Bun: join:local_col=foreign_col (one join: part per pair)
|
||||||
|
var bunCols []string
|
||||||
for _, part := range strings.Split(field.Tag.Get("bun"), ",") {
|
for _, part := range strings.Split(field.Tag.Get("bun"), ",") {
|
||||||
part = strings.TrimSpace(part)
|
part = strings.TrimSpace(part)
|
||||||
if strings.HasPrefix(part, "join:") {
|
if strings.HasPrefix(part, "join:") {
|
||||||
// join: may contain multiple pairs separated by spaces: "join:a=b join:c=d"
|
|
||||||
// but typically it's a single pair; take the first local column
|
|
||||||
pair := strings.TrimPrefix(part, "join:")
|
pair := strings.TrimPrefix(part, "join:")
|
||||||
if idx := strings.Index(pair, "="); idx > 0 {
|
if idx := strings.Index(pair, "="); idx > 0 {
|
||||||
return pair[:idx]
|
bunCols = append(bunCols, pair[:idx])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(bunCols) > 0 {
|
||||||
|
return bunCols
|
||||||
|
}
|
||||||
|
|
||||||
// GORM: foreignKey:FieldName
|
// GORM: foreignKey:FieldA,FieldB
|
||||||
for _, part := range strings.Split(field.Tag.Get("gorm"), ";") {
|
for _, part := range strings.Split(field.Tag.Get("gorm"), ";") {
|
||||||
part = strings.TrimSpace(part)
|
part = strings.TrimSpace(part)
|
||||||
if strings.HasPrefix(part, "foreignKey:") {
|
if strings.HasPrefix(part, "foreignKey:") {
|
||||||
fkFieldName := strings.TrimPrefix(part, "foreignKey:")
|
var cols []string
|
||||||
|
for _, fkFieldName := range strings.Split(strings.TrimPrefix(part, "foreignKey:"), ",") {
|
||||||
|
fkFieldName = strings.TrimSpace(fkFieldName)
|
||||||
if fkField, ok := modelType.FieldByName(fkFieldName); ok {
|
if fkField, ok := modelType.FieldByName(fkFieldName); ok {
|
||||||
return getColumnNameFromField(fkField)
|
cols = append(cols, getColumnNameFromField(fkField))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(cols) > 0 {
|
||||||
|
return cols
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ""
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return ""
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRelationModel gets the model type for a relation field
|
// GetRelationModel gets the model type for a relation field
|
||||||
|
|||||||
@@ -15,6 +15,13 @@ type bunEmployee struct {
|
|||||||
Department *fkDept `bun:"rel:belongs-to,join:dept_id=id" json:"department"`
|
Department *fkDept `bun:"rel:belongs-to,join:dept_id=id" json:"department"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bunCompositeEmployee has a composite bun join: (two join: parts).
|
||||||
|
type bunCompositeEmployee struct {
|
||||||
|
DeptID string `bun:"dept_id" json:"dept_id"`
|
||||||
|
TenantID string `bun:"tenant_id" json:"tenant_id"`
|
||||||
|
Department *fkDept `bun:"rel:belongs-to,join:dept_id=id,join:tenant_id=id" json:"department"`
|
||||||
|
}
|
||||||
|
|
||||||
// gormEmployee uses gorm foreignKey: tag (mirrors testmodels.Employee).
|
// gormEmployee uses gorm foreignKey: tag (mirrors testmodels.Employee).
|
||||||
type gormEmployee struct {
|
type gormEmployee struct {
|
||||||
DepartmentID string `json:"department_id"`
|
DepartmentID string `json:"department_id"`
|
||||||
@@ -23,6 +30,13 @@ type gormEmployee struct {
|
|||||||
Manager *fkDept `gorm:"foreignKey:ManagerID;references:ID" json:"manager"`
|
Manager *fkDept `gorm:"foreignKey:ManagerID;references:ID" json:"manager"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// gormCompositeEmployee has a composite GORM foreignKey.
|
||||||
|
type gormCompositeEmployee struct {
|
||||||
|
DeptID string `json:"dept_id"`
|
||||||
|
TenantID string `json:"tenant_id"`
|
||||||
|
Department *fkDept `gorm:"foreignKey:DeptID,TenantID" json:"department"`
|
||||||
|
}
|
||||||
|
|
||||||
// conventionEmployee has no explicit FK tag — relies on naming convention.
|
// conventionEmployee has no explicit FK tag — relies on naming convention.
|
||||||
type conventionEmployee struct {
|
type conventionEmployee struct {
|
||||||
DepartmentID string `json:"department_id"`
|
DepartmentID string `json:"department_id"`
|
||||||
@@ -39,20 +53,26 @@ func TestGetForeignKeyColumn(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
modelType reflect.Type
|
modelType reflect.Type
|
||||||
parentKey string
|
parentKey string
|
||||||
want string
|
want []string
|
||||||
}{
|
}{
|
||||||
// Bun join: tag
|
// Bun join: tag
|
||||||
{
|
{
|
||||||
name: "bun join tag returns local column",
|
name: "bun join tag returns local column",
|
||||||
modelType: reflect.TypeOf(bunEmployee{}),
|
modelType: reflect.TypeOf(bunEmployee{}),
|
||||||
parentKey: "department",
|
parentKey: "department",
|
||||||
want: "dept_id",
|
want: []string{"dept_id"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "bun join tag matched via json tag (case-insensitive)",
|
name: "bun join tag matched via json tag (case-insensitive)",
|
||||||
modelType: reflect.TypeOf(bunEmployee{}),
|
modelType: reflect.TypeOf(bunEmployee{}),
|
||||||
parentKey: "Department",
|
parentKey: "Department",
|
||||||
want: "dept_id",
|
want: []string{"dept_id"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bun composite join returns all local columns",
|
||||||
|
modelType: reflect.TypeOf(bunCompositeEmployee{}),
|
||||||
|
parentKey: "department",
|
||||||
|
want: []string{"dept_id", "tenant_id"},
|
||||||
},
|
},
|
||||||
|
|
||||||
// GORM foreignKey: tag
|
// GORM foreignKey: tag
|
||||||
@@ -60,19 +80,25 @@ func TestGetForeignKeyColumn(t *testing.T) {
|
|||||||
name: "gorm foreignKey resolves to column name",
|
name: "gorm foreignKey resolves to column name",
|
||||||
modelType: reflect.TypeOf(gormEmployee{}),
|
modelType: reflect.TypeOf(gormEmployee{}),
|
||||||
parentKey: "department",
|
parentKey: "department",
|
||||||
want: "department_id",
|
want: []string{"department_id"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "gorm foreignKey resolves second relation",
|
name: "gorm foreignKey resolves second relation",
|
||||||
modelType: reflect.TypeOf(gormEmployee{}),
|
modelType: reflect.TypeOf(gormEmployee{}),
|
||||||
parentKey: "manager",
|
parentKey: "manager",
|
||||||
want: "manager_id",
|
want: []string{"manager_id"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "gorm foreignKey matched case-insensitively",
|
name: "gorm foreignKey matched case-insensitively",
|
||||||
modelType: reflect.TypeOf(gormEmployee{}),
|
modelType: reflect.TypeOf(gormEmployee{}),
|
||||||
parentKey: "Department",
|
parentKey: "Department",
|
||||||
want: "department_id",
|
want: []string{"department_id"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gorm composite foreignKey returns all columns",
|
||||||
|
modelType: reflect.TypeOf(gormCompositeEmployee{}),
|
||||||
|
parentKey: "department",
|
||||||
|
want: []string{"dept_id", "tenant_id"},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Pointer and slice unwrapping
|
// Pointer and slice unwrapping
|
||||||
@@ -80,43 +106,43 @@ func TestGetForeignKeyColumn(t *testing.T) {
|
|||||||
name: "pointer to struct is unwrapped",
|
name: "pointer to struct is unwrapped",
|
||||||
modelType: reflect.TypeOf(&gormEmployee{}),
|
modelType: reflect.TypeOf(&gormEmployee{}),
|
||||||
parentKey: "department",
|
parentKey: "department",
|
||||||
want: "department_id",
|
want: []string{"department_id"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "slice of struct is unwrapped",
|
name: "slice of struct is unwrapped",
|
||||||
modelType: reflect.TypeOf([]gormEmployee{}),
|
modelType: reflect.TypeOf([]gormEmployee{}),
|
||||||
parentKey: "department",
|
parentKey: "department",
|
||||||
want: "department_id",
|
want: []string{"department_id"},
|
||||||
},
|
},
|
||||||
|
|
||||||
// No tag — returns "" so caller can fall back to convention
|
// No tag — returns nil so caller can fall back to convention
|
||||||
{
|
{
|
||||||
name: "relation with no FK tag returns empty string",
|
name: "relation with no FK tag returns nil",
|
||||||
modelType: reflect.TypeOf(conventionEmployee{}),
|
modelType: reflect.TypeOf(conventionEmployee{}),
|
||||||
parentKey: "department",
|
parentKey: "department",
|
||||||
want: "",
|
want: nil,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Unknown parent key
|
// Unknown parent key
|
||||||
{
|
{
|
||||||
name: "unknown parent key returns empty string",
|
name: "unknown parent key returns nil",
|
||||||
modelType: reflect.TypeOf(gormEmployee{}),
|
modelType: reflect.TypeOf(gormEmployee{}),
|
||||||
parentKey: "nonexistent",
|
parentKey: "nonexistent",
|
||||||
want: "",
|
want: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "non-struct type returns empty string",
|
name: "non-struct type returns nil",
|
||||||
modelType: reflect.TypeOf(""),
|
modelType: reflect.TypeOf(""),
|
||||||
parentKey: "department",
|
parentKey: "department",
|
||||||
want: "",
|
want: nil,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got := GetForeignKeyColumn(tt.modelType, tt.parentKey)
|
got := GetForeignKeyColumn(tt.modelType, tt.parentKey)
|
||||||
if got != tt.want {
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
t.Errorf("GetForeignKeyColumn(%v, %q) = %q, want %q", tt.modelType, tt.parentKey, got, tt.want)
|
t.Errorf("GetForeignKeyColumn(%v, %q) = %v, want %v", tt.modelType, tt.parentKey, got, tt.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user