go-duck-cli 1.0.9 → 1.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +30 -15
  2. package/generators/ai_docs.js +130 -0
  3. package/generators/broker.js +63 -0
  4. package/generators/config.js +149 -7
  5. package/generators/devops.js +210 -43
  6. package/generators/docs.js +23 -4
  7. package/generators/elasticsearch.js +263 -0
  8. package/generators/kratos.js +229 -41
  9. package/generators/metering.js +280 -48
  10. package/generators/migrations.js +92 -198
  11. package/generators/mqtt.js +2 -39
  12. package/generators/multitenancy.js +274 -71
  13. package/generators/nats.js +39 -0
  14. package/generators/outbox.js +171 -0
  15. package/generators/postgrest.js +7 -3
  16. package/generators/postman.js +405 -0
  17. package/generators/repository.js +27 -0
  18. package/generators/router.js +27 -0
  19. package/generators/security.js +95 -14
  20. package/generators/serverless.js +147 -0
  21. package/generators/storage.js +589 -0
  22. package/generators/swagger.js +84 -60
  23. package/generators/telemetry.js +23 -32
  24. package/generators/websocket.js +55 -21
  25. package/index.js +493 -116
  26. package/package.json +6 -4
  27. package/parser/gdl.js +163 -24
  28. package/templates/docs/index.html.hbs +5 -5
  29. package/templates/docs/layout.hbs +221 -62
  30. package/templates/docs/pages/audit.hbs +83 -35
  31. package/templates/docs/pages/cli.hbs +18 -0
  32. package/templates/docs/pages/configuration.hbs +241 -0
  33. package/templates/docs/pages/datadog.hbs +46 -0
  34. package/templates/docs/pages/elasticsearch.hbs +121 -0
  35. package/templates/docs/pages/federation.hbs +241 -0
  36. package/templates/docs/pages/gdl-advanced.hbs +91 -0
  37. package/templates/docs/pages/gdl-annotations.hbs +137 -0
  38. package/templates/docs/pages/gdl-entities.hbs +134 -0
  39. package/templates/docs/pages/gdl-relationships.hbs +80 -0
  40. package/templates/docs/pages/gdl.hbs +60 -204
  41. package/templates/docs/pages/graphql.hbs +58 -44
  42. package/templates/docs/pages/grpc.hbs +53 -90
  43. package/templates/docs/pages/hybrid-store.hbs +127 -0
  44. package/templates/docs/pages/index.hbs +418 -149
  45. package/templates/docs/pages/keycloak.hbs +43 -0
  46. package/templates/docs/pages/legend.hbs +116 -0
  47. package/templates/docs/pages/mosquitto.hbs +39 -0
  48. package/templates/docs/pages/multitenancy.hbs +139 -71
  49. package/templates/docs/pages/otel.hbs +40 -0
  50. package/templates/docs/pages/realtime.hbs +38 -12
  51. package/templates/docs/pages/redis.hbs +40 -0
  52. package/templates/docs/pages/rest.hbs +120 -202
  53. package/templates/docs/pages/saga.hbs +94 -0
  54. package/templates/docs/pages/security.hbs +150 -44
  55. package/templates/docs/pages/serverless.hbs +157 -0
  56. package/templates/docs/pages/storage.hbs +127 -0
  57. package/templates/docs/pages/wizard.hbs +683 -0
  58. package/templates/docs/triple_identity_registry.png +0 -0
  59. package/templates/go/controller.go.hbs +287 -283
  60. package/templates/go/entity.go.hbs +17 -15
  61. package/templates/go/main.go.hbs +47 -180
  62. package/templates/go/migrator.go.hbs +65 -0
  63. package/templates/go/router.go.hbs +272 -0
  64. package/templates/graphql/resolver.go.hbs +53 -34
  65. package/templates/graphql/schema.graphql.hbs +17 -5
  66. package/templates/kratos/service.go.hbs +169 -34
  67. package/templates/proto/entity.proto.hbs +10 -14
  68. package/test_nested.gdl +21 -0
  69. package/templates/docs/intro.mp4 +0 -0
  70. package/test_parser.js +0 -9
@@ -1,339 +1,343 @@
1
1
  package controllers
2
2
 
3
3
  import (
4
- "fmt"
5
- "net/http"
6
- "strconv"
7
-
8
- "{{app_name}}/config"
9
- "{{app_name}}/messaging"
10
- "{{app_name}}/models"
11
- "{{app_name}}/cache"
12
- "{{app_name}}/resilience"
13
-
14
- "github.com/gin-gonic/gin"
15
- "gorm.io/gorm"
4
+ "net/http"
5
+ "strconv"
6
+ {{#if isSearchable}}
7
+ "context"
8
+ {{/if}}
9
+ {{#if (or isDocument isFederated)}}
10
+ "fmt"
11
+ {{/if}}
12
+ {{#if isFederated}}
13
+ "encoding/json"
14
+ "sync"
15
+ {{/if}}
16
+
17
+ "{{app_name}}/config"
18
+ "{{app_name}}/messaging"
19
+ "{{app_name}}/models"
20
+ "{{app_name}}/cache"
21
+ {{#if isSearchable}}
22
+ "{{app_name}}/internal/search"
23
+ {{/if}}
24
+ "github.com/gin-gonic/gin"
25
+ {{#if isDocument}}
26
+ "go.mongodb.org/mongo-driver/bson"
27
+ "go.mongodb.org/mongo-driver/mongo"
28
+ "go.mongodb.org/mongo-driver/mongo/options"
29
+ {{else}}
30
+ "gorm.io/gorm"
31
+ {{/if}}
16
32
  )
17
33
 
18
34
  type {{capitalize name}}Controller struct {
19
- DB *gorm.DB
20
- Config *config.Config
35
+ {{#if isDocument}}
36
+ MongoClient *mongo.Client
37
+ {{else}}
38
+ DB *gorm.DB
39
+ {{/if}}
40
+ Config *config.Config
21
41
  }
22
42
 
23
- // Create{{capitalize name}}
24
- db := ctrl.DB
25
- if tdb, exists := c.Get("tenantDBConn"); exists {
26
- db = tdb.(*gorm.DB)
27
- }
28
-
43
+ // Create handles creating a new {{capitalize name}}
44
+ func (ctrl *{{capitalize name}}Controller) Create(c *gin.Context) {
45
+ ctx := c.Request.Context()
29
46
  var entity models.{{capitalize name}}
30
47
  if err := c.ShouldBindJSON(&entity); err != nil {
31
48
  c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
32
49
  return
33
50
  }
34
- if err := db.WithContext(ctx).Create(&entity).Error; err != nil {
35
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
36
- return
37
- }
38
-
39
- // Dynamic Cache Invalidation (Tenant Aware)
40
- cache.ClearPattern(tenantStr + ":{{capitalize name}}*")
41
51
 
42
- // MQTT Event (Resilient)
43
- resilience.Execute(func() (interface{}, error) {
44
- messaging.PublishEvent(ctrl.Config.GoDuck.Messaging.MQTT.TopicPrefix, "CREATE", "{{capitalize name}}", entity, nil)
45
- return nil, nil
46
- })
47
-
48
- c.JSON(http.StatusCreated, entity)
49
- }
50
-
51
- // GetAll{{capitalize name}}s (with filtering, pagination, and lazy/eager loading)
52
- func (ctrl *{{capitalize name}}Controller) GetAll(c *gin.Context) {
53
- db := ctrl.DB
54
- if tdb, exists := c.Get("tenantDBConn"); exists {
55
- db = tdb.(*gorm.DB)
52
+ {{#if isDocument}}
53
+ // 🦆 MongoDB Path
54
+ db, _ := c.Get("tenantMongoDB")
55
+ tenantDB := db.(*mongo.Database)
56
+ collection := tenantDB.Collection("{{toLowerCase name}}s")
57
+
58
+ {{#if isFederated}}
59
+ // 🦆 Federated MongoDB Path
60
+ siloConns, _ := c.Get("tenantMongoConnections")
61
+ conns := siloConns.(map[string]*mongo.Database)
62
+ primaryRole, _ := c.Get("primaryRole")
63
+ firstRole := primaryRole.(string)
64
+
65
+ // Since MongoDB doesn't do cross-DB transactions easily, we record the Outbox in the Primary Postgres Silo
66
+ // (If Postgres is disabled, we'd use a Mongo-native outbox pool)
67
+ masterDB, _ := c.Get("tenantDBConn")
68
+ postgresDB := masterDB.(*gorm.DB)
69
+
70
+ err := postgresDB.Transaction(func(tx *gorm.DB) error {
71
+ res, err := collection.InsertOne(ctx, entity)
72
+ if err != nil { return err }
73
+ entity.ID = fmt.Sprintf("%v", res.InsertedID)
74
+
75
+ payload, _ := json.Marshal(entity)
76
+ for role := range conns {
77
+ if role == firstRole { continue }
78
+ tx.Create(&models.DistributedOutbox{
79
+ MutationType: "CREATE",
80
+ EntityName: "{{capitalize name}}",
81
+ EntityID: entity.ID,
82
+ Payload: string(payload),
83
+ TargetSilo: role,
84
+ SourceSilo: firstRole,
85
+ Status: "PENDING",
86
+ })
87
+ }
88
+ return nil
89
+ })
90
+ if err != nil {
91
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Federated Mongo write failed: " + err.Error()})
92
+ return
56
93
  }
57
- var entities []models.{{capitalize name}}
58
- ctx := c.Request.Context()
59
- query := db.WithContext(ctx)
60
-
61
- // 1. Pagination
62
- page, _ := strconv.Atoi(c.DefaultQuery("page", "0"))
63
- size, _ := strconv.Atoi(c.DefaultQuery("size", "20"))
64
- query = query.Offset(page * size).Limit(size)
65
-
66
- // 2. Eager Loading (Full Bodied) vs Lazy (IDs Only)
67
- eager := c.Query("eager") == "true"
68
- if eager {
69
- {{#each relationships}}
70
- query = query.Preload("{{capitalize from.field}}")
71
- query = query.Preload("{{capitalize to.field}}")
72
- {{/each}}
73
- }
74
-
75
- // 3. Simple Filtering (Optimized for CRUD)
76
- // Example: ?name=eq.John
77
- for key, values := range c.Request.URL.Query() {
78
- if key == "page" || key == "size" || key == "eager" || key == "sort" {
79
- continue
80
- }
81
- for _, val := range values {
82
- query = query.Where(key+" = ?", val)
83
- }
84
- }
85
-
86
- if err := query.Find(&entities).Error; err != nil {
87
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
88
- return
89
- }
90
- c.JSON(http.StatusOK, entities)
91
- }
92
-
93
- // GetByID
94
- func (ctrl *{{capitalize name}}Controller) GetByID(c *gin.Context) {
95
- id := c.Param("id")
96
- tenant, _ := c.Get("tenantDB")
97
- tenantStr := fmt.Sprintf("%v", tenant)
98
- ctx := c.Request.Context()
99
-
100
- // Tenant-Aware Cache Key
101
- cacheKey := fmt.Sprintf("%s:{{capitalize name}}:%s", tenantStr, id)
102
-
103
- var entity models.{{capitalize name}}
104
-
105
- // 1. Check Distributed Cache (Redis)
106
- if cache.Get(cacheKey, &entity) {
107
- c.JSON(http.StatusOK, entity)
108
- return
109
- }
110
-
111
- // 2. Fallback to DB (With Context for Tracing)
112
- db := ctrl.DB
113
- if tdb, exists := c.Get("tenantDBConn"); exists {
114
- db = tdb.(*gorm.DB)
94
+ messaging.PublishEvent(ctrl.Config.GoDuck.Messaging.MQTT.TopicPrefix, firstRole, "CREATE", "{{capitalize name}}", entity, nil)
95
+ {{else}}
96
+ res, err := collection.InsertOne(ctx, entity)
97
+ if err != nil {
98
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
99
+ return
115
100
  }
116
- query := db.WithContext(ctx)
117
- if c.Query("eager") == "true" {
118
- {{#each relationships}}
119
- query = query.Preload("{{capitalize from.field}}")
120
- query = query.Preload("{{capitalize to.field}}")
121
- {{/each}}
122
- }
123
-
124
- if err := query.First(&entity, id).Error; err != nil {
125
- c.JSON(http.StatusNotFound, gin.H{"error": "Resource not found"})
126
- return
127
- }
128
-
129
- // 3. Update Cache (Resilient)
130
- resilience.Execute(func() (interface{}, error) {
131
- cache.Set(cacheKey, entity, ctrl.Config.GoDuck.Cache.Redis.TTL)
132
- return nil, nil
133
- })
134
-
135
- c.JSON(http.StatusOK, entity)
136
- }
137
-
138
- // Update (PUT) - Full Update
139
- db := ctrl.DB
140
- if tdb, exists := c.Get("tenantDBConn"); exists {
141
- db = tdb.(*gorm.DB)
101
+ entity.ID = fmt.Sprintf("%v", res.InsertedID)
102
+ messaging.PublishEvent(ctrl.Config.GoDuck.Messaging.MQTT.TopicPrefix, "tenant", "CREATE", "{{capitalize name}}", entity, nil)
103
+ {{/if}}
104
+ {{else}}
105
+ // 🦆 GORM Path
106
+ db, _ := c.Get("tenantDBConn")
107
+ tenantDB := db.(*gorm.DB)
108
+ {{#if isFederated}}
109
+ siloConns, _ := c.Get("tenantSiloConns")
110
+ conns := siloConns.(map[string]*gorm.DB)
111
+ primaryRole, _ := c.Get("primaryRole")
112
+ firstRole := primaryRole.(string)
113
+
114
+ err := tenantDB.Transaction(func(tx *gorm.DB) error {
115
+ if err := tx.WithContext(ctx).Create(&entity).Error; err != nil { return err }
116
+ payload, _ := json.Marshal(entity)
117
+ for role := range conns {
118
+ if role == firstRole { continue }
119
+ tx.Create(&models.DistributedOutbox{ MutationType: "CREATE", EntityName: "{{capitalize name}}", EntityID: fmt.Sprintf("%v", entity.ID), Payload: string(payload), TargetSilo: role, SourceSilo: firstRole, Status: "PENDING" })
120
+ }
121
+ return nil
122
+ })
123
+ if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}); return }
124
+ messaging.PublishEvent(ctrl.Config.GoDuck.Messaging.MQTT.TopicPrefix, firstRole, "CREATE", "{{capitalize name}}", entity, nil)
125
+ {{else}}
126
+ if err := tenantDB.WithContext(ctx).Create(&entity).Error; err != nil {
127
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
128
+ return
142
129
  }
143
-
144
- var entity models.{{capitalize name}}
145
- if err := db.WithContext(ctx).First(&entity, id).Error; err != nil {
146
- c.JSON(http.StatusNotFound, gin.H{"error": "Resource not found"})
147
- return
148
- }
149
-
150
- // Capture Previous State
151
- prev := entity
152
-
153
- if err := c.ShouldBindJSON(&entity); err != nil {
154
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
155
- return
156
- }
157
-
158
- if err := db.WithContext(ctx).Save(&entity).Error; err != nil {
159
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
160
- return
161
- }
162
-
163
- // Cache Invalidation (Tenant Aware)
164
- cache.Delete(fmt.Sprintf("%s:{{capitalize name}}:%s", tenantStr, id))
165
-
166
- // MQTT Event (Resilient)
167
- resilience.Execute(func() (interface{}, error) {
168
- messaging.PublishEvent(ctrl.Config.GoDuck.Messaging.MQTT.TopicPrefix, "UPDATE", "{{capitalize name}}", entity, prev)
169
- return nil, nil
170
- })
171
-
172
- c.JSON(http.StatusOK, entity)
173
- }
174
-
175
- // Patch (PATCH) - Partial Update
176
- db := ctrl.DB
177
- if tdb, exists := c.Get("tenantDBConn"); exists {
178
- db = tdb.(*gorm.DB)
130
+ messaging.PublishEvent(ctrl.Config.GoDuck.Messaging.MQTT.TopicPrefix, "tenant", "CREATE", "{{capitalize name}}", entity, nil)
131
+ {{/if}}
132
+ {{/if}}
133
+
134
+ cache.ClearPattern("*:{{capitalize name}}*")
135
+ {{#if isSearchable}}
136
+ if ctrl.Config.GoDuck.Elasticsearch.AutoSync {
137
+ go search.SyncToElasticsearch(context.Background(), "{{capitalize name}}", entity.ID, entity, ctrl.Config)
179
138
  }
139
+ {{/if}}
180
140
 
181
- var entity models.{{capitalize name}}
182
- if err := db.WithContext(ctx).First(&entity, id).Error; err != nil {
183
- c.JSON(http.StatusNotFound, gin.H{"error": "Resource not found"})
184
- return
185
- }
186
- prev := entity
187
-
188
- var updates map[string]interface{}
189
- if err := c.ShouldBindJSON(&updates); err != nil {
190
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
191
- return
192
- }
193
-
194
- if err := db.WithContext(ctx).Model(&entity).Updates(updates).Error; err != nil {
195
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
196
- return
141
+ c.JSON(http.StatusCreated, entity)
197
142
  }
198
143
 
199
- // Fetch updated
200
- db.WithContext(ctx).First(&entity, id)
201
-
202
- // Cache Invalidation (Tenant Aware)
203
- cache.Delete(fmt.Sprintf("%s:{{capitalize name}}:%s", tenantStr, id))
204
-
205
- // MQTT Event (Resilient)
206
- resilience.Execute(func() (interface{}, error) {
207
- messaging.PublishEvent(ctrl.Config.GoDuck.Messaging.MQTT.TopicPrefix, "PATCH", "{{capitalize name}}", entity, prev)
208
- return nil, nil
209
- })
210
-
211
- c.JSON(http.StatusOK, gin.H{"message": "Updated successfully", "data": entity})
212
- }
213
-
214
- // BulkCreate handles creating multiple entities in one transaction
215
- db := ctrl.DB
216
- if tdb, exists := c.Get("tenantDBConn"); exists {
217
- db = tdb.(*gorm.DB)
144
+ // GetAll handles parallel read aggregation for both SQL and Mongo
145
+ func (ctrl *{{capitalize name}}Controller) GetAll(c *gin.Context) {
146
+ ctx := c.Request.Context()
147
+ page, _ := strconv.Atoi(c.DefaultQuery("page", "0"))
148
+ size, _ := strconv.Atoi(c.DefaultQuery("size", "20"))
149
+
150
+ {{#if isDocument}}
151
+ // 🦆 MongoDB Path
152
+ {{#if isFederated}}
153
+ if c.DefaultQuery("federated", "false") == "true" {
154
+ siloConns, _ := c.Get("tenantMongoConnections")
155
+ conns := siloConns.(map[string]*mongo.Database)
156
+ federatedData := make(map[string][]models.{{capitalize name}})
157
+
158
+ var wg sync.WaitGroup
159
+ var mu sync.Mutex
160
+
161
+ for role, siloDB := range conns {
162
+ wg.Add(1)
163
+ go func(r string, db *mongo.Database) {
164
+ defer wg.Done()
165
+ var entities []models.{{capitalize name}}
166
+ opts := options.Find().SetSkip(int64(page * size)).SetLimit(int64(size))
167
+ cursor, err := db.Collection("{{toLowerCase name}}s").Find(ctx, bson.M{}, opts)
168
+ if err == nil {
169
+ cursor.All(ctx, &entities)
170
+ mu.Lock()
171
+ federatedData[r] = entities
172
+ mu.Unlock()
173
+ }
174
+ }(role, siloDB)
175
+ }
176
+ wg.Wait()
177
+ c.JSON(http.StatusOK, federatedData)
178
+ return
218
179
  }
180
+ {{/if}}
219
181
 
182
+ db, _ := c.Get("tenantMongoDB")
183
+ tenantDB := db.(*mongo.Database)
220
184
  var entities []models.{{capitalize name}}
221
- if err := c.ShouldBindJSON(&entities); err != nil {
222
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
223
- return
185
+ opts := options.Find().SetSkip(int64(page * size)).SetLimit(int64(size))
186
+
187
+ // PostgREST-lite Filter Translation (Simple eq check for now)
188
+ filter := bson.M{}
189
+ for k, v := range c.Request.URL.Query() {
190
+ if k != "page" && k != "size" && k != "federated" {
191
+ filter[k] = v[0]
192
+ }
224
193
  }
225
194
 
226
- if err := db.WithContext(ctx).Create(&entities).Error; err != nil {
195
+ cursor, err := tenantDB.Collection("{{toLowerCase name}}s").Find(ctx, filter, opts)
196
+ if err != nil {
227
197
  c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
228
198
  return
229
199
  }
200
+ cursor.All(ctx, &entities)
201
+ c.JSON(http.StatusOK, entities)
230
202
 
231
- // Dynamic Cache Invalidation (Tenant Aware)
232
- cache.ClearPattern(tenantStr + ":{{capitalize name}}*")
233
-
234
- // MQTT Event (Resilient)
235
- resilience.Execute(func() (interface{}, error) {
236
- messaging.PublishEvent(ctrl.Config.GoDuck.Messaging.MQTT.TopicPrefix, "BULK_CREATE", "{{capitalize name}}", entities, nil)
237
- return nil, nil
238
- })
239
-
240
- c.JSON(http.StatusCreated, entities)
241
- }
242
-
243
- // BulkUpdate handles updating multiple entities in one transaction
244
- db := ctrl.DB
245
- if tdb, exists := c.Get("tenantDBConn"); exists {
246
- db = tdb.(*gorm.DB)
247
- }
248
-
249
- var entities []models.{{capitalize name}}
250
- if err := c.ShouldBindJSON(&entities); err != nil {
251
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
203
+ {{else}}
204
+ // 🦆 GORM Path
205
+ db, _ := c.Get("tenantDBConn")
206
+ tenantDB := db.(*gorm.DB)
207
+
208
+ {{#if isFederated}}
209
+ if c.DefaultQuery("federated", "false") == "true" {
210
+ siloConns, _ := c.Get("tenantSiloConns")
211
+ conns := siloConns.(map[string]*gorm.DB)
212
+ federatedData := make(map[string][]models.{{capitalize name}})
213
+
214
+ var wg sync.WaitGroup
215
+ var mu sync.Mutex
216
+
217
+ for role, siloDB := range conns {
218
+ wg.Add(1)
219
+ go func(r string, db *gorm.DB) {
220
+ defer wg.Done()
221
+ var entities []models.{{capitalize name}}
222
+ if err := db.WithContext(ctx).Offset(page * size).Limit(size).Find(&entities).Error; err == nil {
223
+ mu.Lock()
224
+ federatedData[r] = entities
225
+ mu.Unlock()
226
+ }
227
+ }(role, siloDB)
228
+ }
229
+ wg.Wait()
230
+ c.JSON(http.StatusOK, federatedData)
252
231
  return
253
232
  }
233
+ {{/if}}
254
234
 
255
- err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
256
- for _, e := range entities {
257
- if err := tx.Save(&e).Error; err != nil {
258
- return err
259
- }
260
- cache.Delete(fmt.Sprintf("%s:{{capitalize name}}:%d", tenantStr, e.ID))
261
- }
262
- return nil
263
- })
264
-
265
- if err != nil {
235
+ var entities []models.{{capitalize name}}
236
+ if err := tenantDB.WithContext(ctx).Offset(page * size).Limit(size).Find(&entities).Error; err != nil {
266
237
  c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
267
238
  return
268
239
  }
269
-
270
- // MQTT Event (Resilient)
271
- resilience.Execute(func() (interface{}, error) {
272
- messaging.PublishEvent(ctrl.Config.GoDuck.Messaging.MQTT.TopicPrefix, "BULK_UPDATE", "{{capitalize name}}", entities, nil)
273
- return nil, nil
274
- })
275
-
276
240
  c.JSON(http.StatusOK, entities)
241
+ {{/if}}
277
242
  }
278
243
 
279
- // BulkPatch handles partial updates for multiple entities
280
- db := ctrl.DB
281
- if tdb, exists := c.Get("tenantDBConn"); exists {
282
- db = tdb.(*gorm.DB)
244
+ func (ctrl *{{capitalize name}}Controller) GetByID(c *gin.Context) {
245
+ ctx := c.Request.Context()
246
+ id := c.Param("id")
247
+
248
+ {{#if isDocument}}
249
+ db, _ := c.Get("tenantMongoDB")
250
+ tenantDB := db.(*mongo.Database)
251
+ var entity models.{{capitalize name}}
252
+ err := tenantDB.Collection("{{toLowerCase name}}s").FindOne(ctx, bson.M{"_id": id}).Decode(&entity)
253
+ if err != nil {
254
+ c.JSON(http.StatusNotFound, gin.H{"error": "Not found"})
255
+ return
283
256
  }
284
-
285
- var updates []struct {
286
- ID uint `json:"id"`
287
- Changes map[string]interface{} `json:"changes"`
257
+ c.JSON(http.StatusOK, entity)
258
+ {{else}}
259
+ db, _ := c.Get("tenantDBConn")
260
+ tenantDB := db.(*gorm.DB)
261
+ var entity models.{{capitalize name}}
262
+ if err := tenantDB.WithContext(ctx).First(&entity, id).Error; err != nil {
263
+ c.JSON(http.StatusNotFound, gin.H{"error": "Not found"})
264
+ return
288
265
  }
289
- if err := c.ShouldBindJSON(&updates); err != nil {
266
+ c.JSON(http.StatusOK, entity)
267
+ {{/if}}
268
+ }
269
+
270
+ func (ctrl *{{capitalize name}}Controller) Update(c *gin.Context) {
271
+ ctx := c.Request.Context()
272
+ id := c.Param("id")
273
+ var entity models.{{capitalize name}}
274
+ if err := c.ShouldBindJSON(&entity); err != nil {
290
275
  c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
291
276
  return
292
277
  }
293
278
 
294
- err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
295
- for _, u := range updates {
296
- if err := tx.Model(&models.{{capitalize name}}{}).Where("id = ?", u.ID).Updates(u.Changes).Error; err != nil {
297
- return err
298
- }
299
- cache.Delete(fmt.Sprintf("%s:{{capitalize name}}:%d", tenantStr, u.ID))
300
- }
301
- return nil
302
- })
303
-
279
+ {{#if isDocument}}
280
+ db, _ := c.Get("tenantMongoDB")
281
+ tenantDB := db.(*mongo.Database)
282
+ _, err := tenantDB.Collection("{{toLowerCase name}}s").ReplaceOne(ctx, bson.M{"_id": id}, entity)
304
283
  if err != nil {
305
284
  c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
306
285
  return
307
286
  }
308
-
309
- c.JSON(http.StatusOK, gin.H{"message": "Bulk patch completed successfully"})
310
- }
311
-
312
- // Delete
313
- db := ctrl.DB
314
- if tdb, exists := c.Get("tenantDBConn"); exists {
315
- db = tdb.(*gorm.DB)
287
+ {{else}}
288
+ db, _ := c.Get("tenantDBConn")
289
+ tenantDB := db.(*gorm.DB)
290
+ if err := tenantDB.WithContext(ctx).Model(&models.{{capitalize name}}{}).Where("id = ?", id).Save(&entity).Error; err != nil {
291
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
292
+ return
316
293
  }
294
+ {{/if}}
317
295
 
318
- var entity models.{{capitalize name}}
319
- if err := db.WithContext(ctx).First(&entity, id).Error; err != nil {
320
- c.JSON(http.StatusNotFound, gin.H{"error": "Resource not found"})
321
- return
296
+ cache.ClearPattern("*:{{capitalize name}}:" + id)
297
+ c.JSON(http.StatusOK, entity)
322
298
  }
323
299
 
324
- if err := db.WithContext(ctx).Delete(&entity).Error; err != nil {
325
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
326
- return
300
+ func (ctrl *{{capitalize name}}Controller) Delete(c *gin.Context) {
301
+ ctx := c.Request.Context()
302
+ id := c.Param("id")
303
+
304
+ {{#if isDocument}}
305
+ db, _ := c.Get("tenantMongoDB")
306
+ tenantDB := db.(*mongo.Database)
307
+ tenantDB.Collection("{{toLowerCase name}}s").DeleteOne(ctx, bson.M{"_id": id})
308
+ {{else}}
309
+ db, _ := c.Get("tenantDBConn")
310
+ tenantDB := db.(*gorm.DB)
311
+ tenantDB.WithContext(ctx).Where("id = ?", id).Delete(&models.{{capitalize name}}{})
312
+ {{/if}}
313
+
314
+ cache.ClearPattern("*:{{capitalize name}}:" + id)
315
+ c.JSON(http.StatusNoContent, nil)
327
316
  }
328
317
 
329
- // Cache Invalidation (Tenant Aware)
330
- cache.Delete(fmt.Sprintf("%s:{{capitalize name}}:%s", tenantStr, id))
331
-
332
- // MQTT Event (Resilient)
333
- resilience.Execute(func() (interface{}, error) {
334
- messaging.PublishEvent(ctrl.Config.GoDuck.Messaging.MQTT.TopicPrefix, "DELETE", "{{capitalize name}}", entity, nil)
335
- return nil, nil
336
- })
318
+ func (ctrl *{{capitalize name}}Controller) Patch(c *gin.Context) {
319
+ ctx := c.Request.Context()
320
+ id := c.Param("id")
321
+ var updates map[string]interface{}
322
+ if err := c.ShouldBindJSON(&updates); err != nil {
323
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
324
+ return
325
+ }
326
+
327
+ {{#if isDocument}}
328
+ db, _ := c.Get("tenantMongoDB")
329
+ tenantDB := db.(*mongo.Database)
330
+ tenantDB.Collection("{{toLowerCase name}}s").UpdateOne(ctx, bson.M{"_id": id}, bson.M{"$set": updates})
331
+ {{else}}
332
+ db, _ := c.Get("tenantDBConn")
333
+ tenantDB := db.(*gorm.DB)
334
+ tenantDB.WithContext(ctx).Model(&models.{{capitalize name}}{}).Where("id = ?", id).Updates(updates)
335
+ {{/if}}
336
+
337
+ cache.ClearPattern("*:{{capitalize name}}:" + id)
338
+ c.JSON(http.StatusOK, gin.H{"message": "Patch successful"})
339
+ }
337
340
 
338
- c.JSON(http.StatusNoContent, nil)
339
- }
341
+ func (ctrl *{{capitalize name}}Controller) BulkCreate(c *gin.Context) { c.JSON(http.StatusNotImplemented, gin.H{"error": "Bulk operations use primary silo isolation by default."}) }
342
+ func (ctrl *{{capitalize name}}Controller) BulkUpdate(c *gin.Context) { c.JSON(http.StatusNotImplemented, gin.H{"error": "Bulk operations use primary silo isolation by default."}) }
343
+ func (ctrl *{{capitalize name}}Controller) BulkPatch(c *gin.Context) { c.JSON(http.StatusNotImplemented, gin.H{"error": "Bulk operations use primary silo isolation by default."}) }