-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcrud_ctrl.go
More file actions
260 lines (234 loc) · 7.5 KB
/
crud_ctrl.go
File metadata and controls
260 lines (234 loc) · 7.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
package crudex
import (
"fmt"
"log/slog"
"net/http"
"os"
"strconv"
"strings"
"github.com/gin-gonic/gin"
odata "github.com/pboyd04/godata"
"gorm.io/gorm"
)
// FormBinder is a function that binds the form data to a model
type FormBinder[T IModel] func(c *gin.Context, out *T) error
// CrudCtrl is a controller that implements the basic CRUD operations for a model with a gorm backend
type CrudCtrl[T IModel] struct {
Db *gorm.DB
Router IRouter
Config IConfig
ModelName string
ModelKeyName string
FormBinder FormBinder[T]
}
// Returns the Name of the model
func (self *CrudCtrl[T]) GetModelName() string {
return self.ModelName
}
// BasePath returns the base path of the controller
func (self *CrudCtrl[T]) BasePath() string {
return self.Router.BasePath()
}
// New creates a new CRUD controller for the provided model
//
// It uses the default configuration
// See `NewWithOptions` for more control over the configuration
func New[T IModel]() *CrudCtrl[T] {
conf := GetConfig()
db := conf.DefaultDb()
modelType := extractType(*new(T))
router := conf.DefaultRouter().Group(fmt.Sprintf("/%s", strings.ToLower(modelType.Name())))
return NewWithOptions[T](db, router, conf)
}
// New creates a new CRUD controller for the provided model
//
// It uses the provided configuration to define its behaviour
func NewWithOptions[T IModel](db *gorm.DB, router IRouter, conf IConfig) *CrudCtrl[T] {
var name = fmt.Sprintf("%T", *new(T))
if strings.Contains(name, ".") {
name = strings.Split(name, ".")[1]
}
res := &CrudCtrl[T]{
FormBinder: DefaultFormHandler[T], // default form handler is used if none is provided
ModelName: name,
Db: db,
Config: conf,
Router: router,
}
if router != nil {
res.OnRouter(router)
} else {
slog.Warn("Router is nil for %s", slog.Any("name", name))
}
if db == nil {
slog.Warn("DB is nil for model", slog.Any("name", name))
}
if conf.AutoScaffold() {
res.ScaffoldDefaults()
}
return res
}
// OnRouter attaches the CRUD routes to the provided router
func (self *CrudCtrl[T]) OnRouter(r IRouter) *CrudCtrl[T] {
self.Router = r
if r != nil {
self.EnableRoutes(r)
}
return self
}
func (self *CrudCtrl[T]) EnableRoutes(r IRouter) *CrudCtrl[T] {
if r == nil {
panic("Router is nil, cannot enable routes")
}
r.GET("/", self.List)
if self.Config.HasUI() {
r.GET("/new", self.Form)
r.GET("/:id/edit", self.Form)
}
r.PUT("/new", self.Upsert)
r.GET("/:id", self.Details)
r.POST("/:id", self.Upsert)
r.DELETE("/:id", self.Delete)
return self
}
func (self *CrudCtrl[T]) ScaffoldDefaults() *CrudCtrl[T] {
model := *new(T)
rootDir := self.Config.ScaffoldRootDir()
if _, err := os.Stat(rootDir); os.IsNotExist(err) {
if os.MkdirAll(rootDir, 0755) != nil {
panic("Failed to create directory")
}
}
GenListTmpl(model, rootDir)
GenDetailTmpl(model, rootDir)
GenFormTmpl(model, rootDir)
return self
}
// Creates and Flushes custom scaffold template
func (self *CrudCtrl[T]) Scaffold(scaffoldTmpl string, conf *ScaffoldDataModelConfigurator) *CrudCtrl[T] {
model := *new(T)
rootDir := self.Config.ScaffoldRootDir()
if _, err := os.Stat(rootDir); os.IsNotExist(err) {
if os.MkdirAll(rootDir, 0755) != nil {
panic("Failed to create directory")
}
}
err := NewScaffoldDataModel(model, conf).Flush(scaffoldTmpl, self.Config.ScaffoldStrategy())
if err != nil {
panic(err)
}
return self
}
// WithFormBinder sets the form binder for the controller to be used when binding form data to the model on the POST and PUT requests.
// It is used in the Upsert method of the controller
// If not set, the default form binder is used which assumes that the form field names are the same as the model field names(case sensitive)
func (self *CrudCtrl[T]) WithFormBinder(handler FormBinder[T]) *CrudCtrl[T] {
self.FormBinder = handler
return self
}
// List is a handler that lists all the items of the model
// it is a GET request
// !Requres the template to be named as modelName-list.html where the modelName is lowercased model name
func (self *CrudCtrl[T]) List(c *gin.Context) {
var items []T
dbRes, error := odata.GetGormSettingsFromGin(c, self.Db)
if error != nil {
c.String(http.StatusBadRequest, c.Error(error).Error())
c.Abort()
return
}
dbRes.Find(&items)
self.Respond(c,
gin.H{fmt.Sprintf("%sList", self.ModelName): &items, "Path": self.Router.BasePath()},
fmt.Sprintf("%s-list.html", strings.ToLower(self.ModelName)))
}
// Details is a handler that shows the details of a single item of the model
// it is a GET request
// !Requires the template to be named as modelName-details.html where the modelName is lowercased model name
func (self *CrudCtrl[T]) Details(c *gin.Context) {
template := fmt.Sprintf("%s.html", strings.ToLower(self.ModelName))
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err == nil {
var item T
self.Db.First(&item, id)
self.Respond(c, gin.H{self.ModelName: item, "Path": fmt.Sprintf("%s/%s", self.Router.BasePath(), idStr)}, template)
} else {
_ = c.Error(err)
c.String(http.StatusBadRequest, fmt.Sprintf("Invalid ID for %s: %d", self.ModelName, id))
}
}
// Form is a handler that shows the form for editing an item of the model
// it is a GET request
// !Requires the template to be named as modelName-edit.html where the modelName is lowercased model name
func (self *CrudCtrl[T]) Form(c *gin.Context) {
template := fmt.Sprintf("%s-form.html", strings.ToLower(self.ModelName))
idStr := c.Param("id")
if idStr == "" {
self.Respond(c, gin.H{"Path": self.Router.BasePath()}, template)
} else {
id, err := strconv.ParseUint(idStr, 10, 64)
if err == nil {
var item T
self.Db.First(&item, id)
self.Respond(c, gin.H{self.ModelName: item, "Path": fmt.Sprintf("%s/%s", self.Router.BasePath(), idStr)}, template)
} else {
_ = c.Error(err)
c.String(http.StatusBadRequest, fmt.Sprintf("Invalid ID for %s: %d", self.ModelName, id))
}
}
}
// Upsert is a handler that saves an item of the model
// it is a POST or PUT request depending on the presence of the id parameter
// !Requires the form fields to be named as the model field names(case sensitive)
// It redirects to the details page of the saved item
func (self *CrudCtrl[T]) Upsert(c *gin.Context) {
var item T
if err := self.FormBinder(c, &item); err != nil {
c.String(http.StatusBadRequest, c.Error(err).Error())
c.Abort()
return
}
idStr := c.Param("id")
isNew := idStr == ""
if !isNew {
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
msg := fmt.Sprintf("Invalid ID: %d", id)
c.String(http.StatusBadRequest, msg)
c.Abort()
return
}
item.SetID(uint(id))
}
res := self.Db.Save(&item)
if res.Error != nil {
c.String(http.StatusBadRequest, c.Error(res.Error).Error())
c.Abort()
return
}
c.Header("HX-Redirect", fmt.Sprintf("%s/%d", self.BasePath(), item.GetID()))
c.String(http.StatusOK, "Saved")
c.Abort()
}
// Delete is a handler that deletes an item of the model
// it is a DELETE request
func (self *CrudCtrl[T]) Delete(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
msg := fmt.Sprintf("Invalid ID: %d", id)
c.String(http.StatusBadRequest, msg)
c.Abort()
return
}
var item T
self.Db.Delete(&item, id)
c.Header("HX-Redirect", self.Router.BasePath())
c.String(http.StatusOK, "Deleted")
c.Abort()
}
// Respond is a function creates a response based on the request headers, the data and the template
func (self *CrudCtrl[T]) Respond(c *gin.Context, data gin.H, templateName string) {
RespondWithConfig(200, c, data, templateName, self.Config)
}