Skip to content

Common interface for generated classes #427

Open

Description

@simon-winter

When building business webApps one needs to expose much of the database with the default operations of CRUD.
An ORM lib would handle this, but i prefer to stay much closer to SQL queries than pushing everything into an ORM pattern and learn all the quirks and maintenance of such a system.

Ideally i would like to be able to write common functions for every model/table struct generated through jet's code gen, so that i can, for example, provide Create/Read/Update/Delete Rest endpoints for my database entities without having to write/generate these functions for each type.

Describe the solution you'd like
A possibility to access the table functionalities provided by this lib via interface or common struct. For example embed a "jettable" in the generated struct isntead of the postgres.table like this:

type JetTable struct{
	postgres.Table

	ID        postgres.ColumnInteger
	AllColumns     postgres.ColumnList
	MutableColumns postgres.ColumnList
}

type messageTable struct {
	JetTable	

	// Columns	
	ChannelID postgres.ColumnInteger
	Text      postgres.ColumnString
	IDUser    postgres.ColumnInteger	
}

so i could write functions for the jettable that allow me to implement the CRUD operations where i don't need the actual columns, but just ID and MutableColumns.
Would also be possible by providing some interface functions to access these fields instead of the embedded struct.

Also, currently tables and models are completly unrelated, maybe it could be part of the solution to wrap these two packages into one "entity" package, that allows access to either the model or the table of an entity via a single struct, so i don't have to write the boilerplate code to combine an empty struct and its table i.e.:
message := entity.NewEntity(model.Message{}, &table.Message, db, ginEngine) could be handled in a package in a sleeker way.

This is what i try to achieve:

//main: 
message := entity.NewEntity(model.Message{}, &table.Message.JetTable, db, ginEngine)

//entity
type Entity struct {
	Name      string
	Model     model.Model
	Table     *table.JetTable
	Db        *sql.DB
	GinEngine *gin.Engine
}

func NewEntity(model model.Model, table *table.JetTable, db *sql.DB, ginEngine *gin.Engine) (e Entity) {
	name := reflect.TypeOf(model).Name()
	e = Entity{
		Name:      name,
		Model:     model,
		Table:     table,
		Db:        db,
		GinEngine: ginEngine,
	}

 ---> // automatic create endpoint for any entity created
	ginEngine.POST("/"+name, func(ctx *gin.Context) { e.Create(ctx) })
	return
}


func (e Entity) Create(ctx *gin.Context) {
	if !e.bindModel(ctx) {
		return
	}

	// Insert model into database
	stmt := e.Table.
		INSERT(e.Table.MutableColumns).
		MODEL(e.Model).
		RETURNING(e.Table.AllColumns)

	err := stmt.Query(e.Db, e.Model)
	handleError(ctx, err)

	// Return inserted message
	ctx.JSON(http.StatusOK, e.Model)
}

Activity

  1. simon-winter commented on Nov 24, 2024

    @simon-winter
    Author

    i will try to modify the generator i find a nice solution for myself to achieve this, but maybe i oversee an easy solution or you want to give it a spin to make it a proper implementation knowing your lib.

    i will propose my solution to this, if i find one, as PR

  2. simon-winter commented on Nov 24, 2024

    @simon-winter
    Author

    Note: My embedding solution is flawed as i cannot access the parent struct from the model struct without reflection. Interfaces are the way to go

  3. houtn11 commented on Nov 25, 2024

    @houtn11

    If I understood correctly, you are probably looking for some sort of CRUD HTTP generator library.
    Since your queries are hardcoded and always the same, a type-safe SQL builder is not that useful in your case.

  4. simon-winter commented on Nov 30, 2024

    @simon-winter
    Author

    @houten11 No, generating http stubs is not the way to go. I want a framework where i can easily provide controlled access to my database via http endpoints. I will need to craft specific database retrievals and operations with the SQL builder for different, specific usecases. Exposing these over http has a lot of reoccuring code, so making tables/models accessible through interfaces/a base class allows me to abstract these operations away and build my own little http/SQL framework.

    I can imagine there is other usecases aswell where you want to define unified access to models/tables.

    My current solution allows me already unified access over an interface, i abstracted CRUD operations already.
    The method signatures are still scaffolding but i think with next iteration i will find a way of unifying table and model in entity without suppllying them manually. Also i will try to embed table and model again, so i can access their properties both over entity without needing to call the members directly

    //main
    entity.NewEntity(model.User{}, table.User, db, ginEngine)
    
    //other
    type Entity[T model.ModelProvider] struct {
    	Name      string
    	Model     T
    	Table     table.TableProvider
    	Db        *sql.DB
    	GinEngine *gin.Engine
    }
    
    func NewEntity[T model.ModelProvider](model T, table table.TableProvider, db *sql.DB, ginEngine *gin.Engine) (e Entity[T]) {
    	e = Entity[T]{
    		Name:      reflect.TypeOf(model).Name(),
    		Model:     model,
    		Table:     table,
    		Db:        db,
    		GinEngine: ginEngine,
    	}
    	// create fitting table 
    	ginEngine.POST("/"+e.Name, func(ctx *gin.Context) { e.Create(ctx) })
    	ginEngine.GET("/"+e.Name, func(ctx *gin.Context) { e.Read(ctx) })
    	ginEngine.PUT("/"+e.Name, func(ctx *gin.Context) { e.Update(ctx) })
    	ginEngine.DELETE("/"+e.Name, func(ctx *gin.Context) { e.Delete(ctx) })
    	return
    }
    
    func (e Entity[T]) Create(ctx *gin.Context) {
    	if !bindModel(ctx, &e.Model) {
    		return
    	}
    
    	// Insert model into database
    	stmt := e.Table.GetPostgresTable().
    		INSERT(e.Table.GetMutableColumns()).	
    		MODEL(&e.Model).
    		RETURNING(e.Table.GetAllColumns())
    
    
    
    	err := stmt.Query(e.Db, &e.Model)
    	handleError(ctx, err)
    
    	// Return inserted message
    	ctx.JSON(http.StatusCreated, e.Model)
    }
    type TableProvider interface {
    	GetAllColumns() postgres.ColumnList
    	GetMutableColumns() postgres.ColumnList
    	GetID() postgres.ColumnInteger
    	GetTable() postgres.Table
    }
    
    type ModelProvider interface {
    	GetID() int64
    }
  5. go-jet commented on Dec 4, 2024

    @go-jet
    Owner

    If I understand correctly, you would like to add methods such as GetAllColumns() and GetMutableColumns() to the table SQL builder. Since we already have AllColumns and MutableColumns fields, having both fields and methods might be confusing for the users. Therefore, I'm hesitant to include those methods in the library. However, you can still fork the project and implement these changes in generator manually. Check generator_template.go

  6. simon-winter commented on Dec 4, 2024

    @simon-winter
    Author

    Providing the functions and defining the matching interface allows writing functions for generated classes in general. Thats not confusing, thats a proper interface for this lib to interact with its types.

  7. go-jet commented on Dec 4, 2024

    @go-jet
    Owner

    Provided interface is set, at least until next major version (3). In the current version (2), I want to avoid having duplicate lists—one accessed as a field and the other as a method.

    Another way to achieve what you need is by using reflection:

    func GetColumnList(table any, fieldName string) ColumnList {
    	tableValue := reflect.ValueOf(table).Elem()
    
    	columnListValue := tableValue.FieldByName(fieldName)
    
    	return columnListValue.Interface().(ColumnList)
    }

    Your example using this function:

    type Entity[T model.ModelProvider] struct {
    	Name      string
    	Model     T
    	Table     postgres.Table           // !!!
    	Db        *sql.DB
    	GinEngine *gin.Engine
    }
    
    
    func (e Entity[T]) Create(ctx *gin.Context) {
    	if !bindModel(ctx, &e.Model) {
    		return
    	}
    
    	// Insert model into database
    	stmt := e.Table.
    		INSERT(GetColumnList(e.Table, "MutableColumns"))).	
    		MODEL(&e.Model).
    		RETURNING(GetColumnList(e.Table, "AllColumns")))
    
    
    
    	err := stmt.Query(e.Db, &e.Model)
    	handleError(ctx, err)
    
    	// Return inserted message
    	ctx.JSON(http.StatusCreated, e.Model)
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions