go-duck-cli 1.1.44 → 1.1.48
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.
- package/generators/multitenancy.js +11 -5
- package/generators/outbox.js +28 -6
- package/index.js +33 -11
- package/package.json +1 -1
- package/templates/go/controller.go.hbs +33 -0
- package/templates/go/entity.go.hbs +12 -16
- package/templates/kratos/service.go.hbs +28 -2
|
@@ -405,15 +405,21 @@ func CreateDatabaseAndMigrate(masterDB *gorm.DB) gin.HandlerFunc {
|
|
|
405
405
|
if req.TenantID != "" {
|
|
406
406
|
existing.TenantID = req.TenantID
|
|
407
407
|
}
|
|
408
|
-
masterDB.Save(&existing)
|
|
408
|
+
if saveErr := masterDB.Save(&existing).Error; saveErr != nil {
|
|
409
|
+
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to update role silo: %v", saveErr)})
|
|
410
|
+
return
|
|
411
|
+
}
|
|
409
412
|
tenantID = existing.TenantID
|
|
410
413
|
} else {
|
|
411
|
-
masterDB.Create(&models.TenantRole{
|
|
414
|
+
if createErr := masterDB.Create(&models.TenantRole{
|
|
412
415
|
TenantID: tenantID,
|
|
413
416
|
RoleName: req.RoleName,
|
|
414
417
|
DBName: req.DBName,
|
|
415
418
|
IsPrimary: isPrimary,
|
|
416
|
-
})
|
|
419
|
+
}).Error; createErr != nil {
|
|
420
|
+
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to provision role silo: %v", createErr)})
|
|
421
|
+
return
|
|
422
|
+
}
|
|
417
423
|
}
|
|
418
424
|
|
|
419
425
|
// 4. Migrate
|
|
@@ -480,8 +486,8 @@ package models
|
|
|
480
486
|
type TenantRole struct {
|
|
481
487
|
ID uint \`gorm:"primaryKey" json:"id"\`
|
|
482
488
|
TenantID string \`json:"tenantId" gorm:"unique;not null;index"\`
|
|
483
|
-
RoleName string \`json:"roleName" gorm:"
|
|
484
|
-
DBName string \`json:"dbName" gorm:"not null"\`
|
|
489
|
+
RoleName string \`json:"roleName" gorm:"not null;uniqueIndex:idx_role_db"\`
|
|
490
|
+
DBName string \`json:"dbName" gorm:"not null;uniqueIndex:idx_role_db"\`
|
|
485
491
|
MongoDBName string \`json:"mongoDbName"\`
|
|
486
492
|
IsPrimary bool \`json:"isPrimary" gorm:"default:false"\`
|
|
487
493
|
}
|
package/generators/outbox.js
CHANGED
|
@@ -42,8 +42,8 @@ import (
|
|
|
42
42
|
"{{app_name}}/models"
|
|
43
43
|
"{{app_name}}/config"
|
|
44
44
|
"{{app_name}}/middleware"
|
|
45
|
-
"gorm.io/gorm"
|
|
46
45
|
"gorm.io/gorm/clause"
|
|
46
|
+
"gorm.io/gorm/schema"
|
|
47
47
|
)
|
|
48
48
|
|
|
49
49
|
type OutboxWorker struct {
|
|
@@ -122,11 +122,18 @@ func (w *OutboxWorker) markFailed(db *gorm.DB, item *models.DistributedOutbox, r
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
func (w *OutboxWorker) relayMutation(targetDB *gorm.DB, item models.DistributedOutbox) error {
|
|
125
|
+
var rawData map[string]interface{}
|
|
125
126
|
var data map[string]interface{}
|
|
126
|
-
|
|
127
|
-
|
|
127
|
+
ns := schema.NamingStrategy{}
|
|
128
|
+
|
|
129
|
+
if item.Payload != "" && item.MutationType != "DELETE" && item.MutationType != "BULK_CREATE" && item.MutationType != "BULK_UPDATE" && item.MutationType != "BULK_PATCH" {
|
|
130
|
+
if err := json.Unmarshal([]byte(item.Payload), &rawData); err != nil {
|
|
128
131
|
return fmt.Errorf("payload unmarshal failed: %v", err)
|
|
129
132
|
}
|
|
133
|
+
data = make(map[string]interface{})
|
|
134
|
+
for k, v := range rawData {
|
|
135
|
+
data[ns.ColumnName("", k)] = v
|
|
136
|
+
}
|
|
130
137
|
}
|
|
131
138
|
|
|
132
139
|
table := strings.ToLower(item.EntityName)
|
|
@@ -139,15 +146,30 @@ func (w *OutboxWorker) relayMutation(targetDB *gorm.DB, item models.DistributedO
|
|
|
139
146
|
case "DELETE":
|
|
140
147
|
return targetDB.Table(table).Where("id = ?", item.EntityID).Delete(nil).Error
|
|
141
148
|
case "BULK_CREATE":
|
|
149
|
+
var rawList []map[string]interface{}
|
|
150
|
+
json.Unmarshal([]byte(item.Payload), &rawList)
|
|
142
151
|
var list []map[string]interface{}
|
|
143
|
-
|
|
152
|
+
for _, row := range rawList {
|
|
153
|
+
mappedRow := make(map[string]interface{})
|
|
154
|
+
for k, v := range row {
|
|
155
|
+
mappedRow[ns.ColumnName("", k)] = v
|
|
156
|
+
}
|
|
157
|
+
list = append(list, mappedRow)
|
|
158
|
+
}
|
|
144
159
|
return targetDB.Table(table).Clauses(clause.OnConflict{DoNothing: true}).Create(list).Error
|
|
145
160
|
case "BULK_UPDATE", "BULK_PATCH":
|
|
161
|
+
var rawList []map[string]interface{}
|
|
162
|
+
json.Unmarshal([]byte(item.Payload), &rawList)
|
|
146
163
|
var list []map[string]interface{}
|
|
147
|
-
|
|
164
|
+
for _, row := range rawList {
|
|
165
|
+
mappedRow := make(map[string]interface{})
|
|
166
|
+
for k, v := range row {
|
|
167
|
+
mappedRow[ns.ColumnName("", k)] = v
|
|
168
|
+
}
|
|
169
|
+
list = append(list, mappedRow)
|
|
170
|
+
}
|
|
148
171
|
err := targetDB.Transaction(func(tx *gorm.DB) error {
|
|
149
172
|
for _, row := range list {
|
|
150
|
-
// Identify ID for updates in bulk
|
|
151
173
|
id := row["id"]
|
|
152
174
|
if id == nil {
|
|
153
175
|
id = row["ID"]
|
package/index.js
CHANGED
|
@@ -82,7 +82,9 @@ type AuditLog struct {
|
|
|
82
82
|
package middleware
|
|
83
83
|
|
|
84
84
|
import (
|
|
85
|
+
"bytes"
|
|
85
86
|
"fmt"
|
|
87
|
+
"io"
|
|
86
88
|
"net/http"
|
|
87
89
|
"time"
|
|
88
90
|
|
|
@@ -98,6 +100,12 @@ func AuditMiddleware(db *gorm.DB) gin.HandlerFunc {
|
|
|
98
100
|
return
|
|
99
101
|
}
|
|
100
102
|
|
|
103
|
+
var bodyBytes []byte
|
|
104
|
+
if c.Request.Body != nil {
|
|
105
|
+
bodyBytes, _ = io.ReadAll(c.Request.Body)
|
|
106
|
+
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
|
107
|
+
}
|
|
108
|
+
|
|
101
109
|
// Simplified auditing logic
|
|
102
110
|
method := c.Request.Method
|
|
103
111
|
path := c.Request.URL.Path
|
|
@@ -125,16 +133,30 @@ func AuditMiddleware(db *gorm.DB) gin.HandlerFunc {
|
|
|
125
133
|
c.Next()
|
|
126
134
|
|
|
127
135
|
// Logic to capture entity ID and snapshot values would go here...
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
entityID := c.Param("id")
|
|
137
|
+
// If ID not in path param, try query param "id"
|
|
138
|
+
if entityID == "" {
|
|
139
|
+
entityID = c.Query("id")
|
|
140
|
+
}
|
|
141
|
+
var newValue string
|
|
142
|
+
newValue = string(bodyBytes)
|
|
143
|
+
// For CREATE actions, previous value is empty; for UPDATE/DELETE we could fetch old value later (placeholder)
|
|
144
|
+
if action == "UPDATE" || action == "DELETE" {
|
|
145
|
+
// Attempt to fetch previous record snapshot (basic placeholder implementation)
|
|
146
|
+
// This could be improved with model-specific hooks.
|
|
147
|
+
}
|
|
148
|
+
auditEntry := models.AuditLog{
|
|
149
|
+
EntityName: path,
|
|
150
|
+
EntityID: entityID,
|
|
151
|
+
TenantDB: tenantStr,
|
|
152
|
+
Action: action,
|
|
153
|
+
ModifiedBy: emailStr,
|
|
154
|
+
KeycloakID: kidStr,
|
|
155
|
+
ModifiedAt: time.Now(),
|
|
156
|
+
ClientIP: clientIP,
|
|
157
|
+
NewValue: newValue,
|
|
158
|
+
}
|
|
159
|
+
db.Create(&auditEntry)
|
|
138
160
|
}
|
|
139
161
|
}
|
|
140
162
|
`;
|
|
@@ -287,7 +309,7 @@ Handlebars.registerPartial('renderFields', `
|
|
|
287
309
|
{{> renderFields fields=children isDocument=../isDocument}}
|
|
288
310
|
} \`json:"{{name}}"{{#if ../isDocument}} bson:"{{name}}"{{/if}}\`
|
|
289
311
|
{{else}}
|
|
290
|
-
{{capitalize name}} {{toGoType type}} \`
|
|
312
|
+
{{capitalize name}} {{toGoType type}} \`bson:"{{#if (eq name "id")}}_id{{else}}{{name}}{{/if}}{{#if (eq name "id")}},omitempty{{/if}}" {{#if (eq name "id")}}gorm:"primaryKey;column:id"{{else}}{{#if unique}}gorm:"uniqueIndex"{{/if}}{{/if}} {{#if (isJson type)}}gorm:"type:{{toLowerCase type}};serializer:json"{{/if}} json:"{{name}}"{{#if required}} binding:"required"{{/if}}\`
|
|
291
313
|
{{/if}}
|
|
292
314
|
{{/each}}
|
|
293
315
|
`);
|
package/package.json
CHANGED
|
@@ -309,6 +309,17 @@ func (ctrl *{{capitalize name}}Controller) Update(c *gin.Context) {
|
|
|
309
309
|
{{/if}}
|
|
310
310
|
|
|
311
311
|
cache.ClearPattern("*:{{capitalize name}}:" + id)
|
|
312
|
+
|
|
313
|
+
{{#if isFederated}}
|
|
314
|
+
rolesInterface, _ := c.Get("tenantRoles")
|
|
315
|
+
roles := rolesInterface.([]string)
|
|
316
|
+
firstRole := "tenant"
|
|
317
|
+
if len(roles) > 0 { firstRole = roles[0] }
|
|
318
|
+
messaging.PublishEvent(ctrl.Config.GoDuck.Messaging.MQTT.TopicPrefix, firstRole, "UPDATE", "{{capitalize name}}", entity, nil)
|
|
319
|
+
{{else}}
|
|
320
|
+
messaging.PublishEvent(ctrl.Config.GoDuck.Messaging.MQTT.TopicPrefix, "tenant", "UPDATE", "{{capitalize name}}", entity, nil)
|
|
321
|
+
{{/if}}
|
|
322
|
+
|
|
312
323
|
c.JSON(http.StatusOK, entity)
|
|
313
324
|
}
|
|
314
325
|
|
|
@@ -327,6 +338,17 @@ func (ctrl *{{capitalize name}}Controller) Delete(c *gin.Context) {
|
|
|
327
338
|
{{/if}}
|
|
328
339
|
|
|
329
340
|
cache.ClearPattern("*:{{capitalize name}}:" + id)
|
|
341
|
+
|
|
342
|
+
{{#if isFederated}}
|
|
343
|
+
rolesInterface, _ := c.Get("tenantRoles")
|
|
344
|
+
roles := rolesInterface.([]string)
|
|
345
|
+
firstRole := "tenant"
|
|
346
|
+
if len(roles) > 0 { firstRole = roles[0] }
|
|
347
|
+
messaging.PublishEvent(ctrl.Config.GoDuck.Messaging.MQTT.TopicPrefix, firstRole, "DELETE", "{{capitalize name}}", map[string]string{"id": id}, nil)
|
|
348
|
+
{{else}}
|
|
349
|
+
messaging.PublishEvent(ctrl.Config.GoDuck.Messaging.MQTT.TopicPrefix, "tenant", "DELETE", "{{capitalize name}}", map[string]string{"id": id}, nil)
|
|
350
|
+
{{/if}}
|
|
351
|
+
|
|
330
352
|
c.JSON(http.StatusNoContent, nil)
|
|
331
353
|
}
|
|
332
354
|
|
|
@@ -350,6 +372,17 @@ func (ctrl *{{capitalize name}}Controller) Patch(c *gin.Context) {
|
|
|
350
372
|
{{/if}}
|
|
351
373
|
|
|
352
374
|
cache.ClearPattern("*:{{capitalize name}}:" + id)
|
|
375
|
+
|
|
376
|
+
{{#if isFederated}}
|
|
377
|
+
rolesInterface, _ := c.Get("tenantRoles")
|
|
378
|
+
roles := rolesInterface.([]string)
|
|
379
|
+
firstRole := "tenant"
|
|
380
|
+
if len(roles) > 0 { firstRole = roles[0] }
|
|
381
|
+
messaging.PublishEvent(ctrl.Config.GoDuck.Messaging.MQTT.TopicPrefix, firstRole, "PATCH", "{{capitalize name}}", updates, nil)
|
|
382
|
+
{{else}}
|
|
383
|
+
messaging.PublishEvent(ctrl.Config.GoDuck.Messaging.MQTT.TopicPrefix, "tenant", "PATCH", "{{capitalize name}}", updates, nil)
|
|
384
|
+
{{/if}}
|
|
385
|
+
|
|
353
386
|
c.JSON(http.StatusOK, gin.H{"message": "Patch successful"})
|
|
354
387
|
}
|
|
355
388
|
|
|
@@ -9,34 +9,30 @@ import (
|
|
|
9
9
|
|
|
10
10
|
type {{name}} struct {
|
|
11
11
|
{{#if isDocument}}
|
|
12
|
-
ID string `json:"id" bson:"_id,omitempty"`
|
|
12
|
+
ID string `gorm:"primaryKey;column:id" json:"id" bson:"_id,omitempty"`
|
|
13
13
|
{{else}}
|
|
14
|
-
ID uint `gorm:"primaryKey" json:"id"`
|
|
14
|
+
ID uint `gorm:"primaryKey;column:id" json:"id" bson:"_id,omitempty"`
|
|
15
15
|
{{/if}}
|
|
16
16
|
{{> renderFields fields=fields isDocument=isDocument}}
|
|
17
17
|
{{#if isAudited}}
|
|
18
|
-
CreatedBy string `
|
|
19
|
-
CreatedDate time.Time `
|
|
20
|
-
LastModifiedBy string `
|
|
21
|
-
LastModifiedDate time.Time `
|
|
18
|
+
CreatedBy string `gorm:"column:created_by" bson:"created_by" json:"createdBy"`
|
|
19
|
+
CreatedDate time.Time `gorm:"column:created_date" bson:"created_date" json:"createdDate"`
|
|
20
|
+
LastModifiedBy string `gorm:"column:last_modified_by" bson:"last_modified_by" json:"lastModifiedBy"`
|
|
21
|
+
LastModifiedDate time.Time `gorm:"column:last_modified_date" bson:"last_modified_date" json:"lastModifiedDate"`
|
|
22
22
|
{{else}}
|
|
23
|
-
CreatedAt time.Time `
|
|
24
|
-
UpdatedAt time.Time `
|
|
23
|
+
CreatedAt time.Time `gorm:"autoCreateTime;column:created_at" bson:"created_at" json:"createdAt"`
|
|
24
|
+
UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at" bson:"updated_at" json:"updatedAt"`
|
|
25
25
|
{{/if}}
|
|
26
26
|
{{#each relationships}}
|
|
27
27
|
{{#if (eq from.entity ../name)}}
|
|
28
|
-
{{capitalize from.field}} []{{capitalize to.entity}} `{{#unless ../isDocument}}{{#unless toIsDocument}}gorm:"foreignKey:{{capitalize to.field}}ID"{{else}}gorm:"-"{{/unless}}{{/unless}} json:"{{from.field}}" bson:"{{from.field}}"`
|
|
28
|
+
{{capitalize from.field}} []{{capitalize to.entity}} `{{#unless ../isDocument}}{{#unless toIsDocument}}gorm:"foreignKey:{{capitalize to.field}}ID"{{else}}gorm:"-"{{/unless}}{{else}}gorm:"-"{{/unless}} json:"{{from.field}}" bson:"{{from.field}}"`
|
|
29
29
|
{{else}}
|
|
30
|
-
{{capitalize to.field}}ID {{#if ../isDocument}}string{{else}}{{#if fromIsDocument}}string{{else}}*uint{{/if}}{{/if}} `
|
|
31
|
-
{{#unless
|
|
32
|
-
{{capitalize to.field}} *{{capitalize from.entity}} `{{#unless fromIsDocument}}gorm:"foreignKey:{{capitalize to.field}}ID"{{else}}gorm:"-"{{/unless}} json:"{{to.field}},omitempty"`
|
|
33
|
-
{{/unless}}
|
|
30
|
+
{{capitalize to.field}}ID {{#if ../isDocument}}string{{else}}{{#if fromIsDocument}}string{{else}}*uint{{/if}}{{/if}} `gorm:"column:{{toLowerCase to.field}}_id;index" bson:"{{toLowerCase to.field}}_id" json:"{{to.field}}Id"`
|
|
31
|
+
{{capitalize to.field}} *{{capitalize from.entity}} `{{#unless fromIsDocument}}gorm:"foreignKey:{{capitalize to.field}}ID"{{else}}gorm:"-"{{/unless}} json:"{{to.field}},omitempty" bson:"-"`
|
|
34
32
|
{{/if}}
|
|
35
33
|
{{/each}}
|
|
36
34
|
}
|
|
37
35
|
|
|
38
|
-
{{#unless isDocument}}
|
|
39
36
|
func ({{name}}) TableName() string {
|
|
40
37
|
return "{{toLowerCase name}}"
|
|
41
|
-
}
|
|
42
|
-
{{/unless}}
|
|
38
|
+
}
|
|
@@ -12,7 +12,9 @@ import (
|
|
|
12
12
|
"time"
|
|
13
13
|
{{/if}}
|
|
14
14
|
pb "{{projectName}}/api/v1"
|
|
15
|
+
"{{projectName}}/config"
|
|
15
16
|
"{{projectName}}/internal/repository"
|
|
17
|
+
"{{projectName}}/messaging"
|
|
16
18
|
"{{projectName}}/models"
|
|
17
19
|
{{#if (hasJson fields)}}
|
|
18
20
|
"gorm.io/datatypes"
|
|
@@ -105,6 +107,12 @@ func (s *{{capitalize name}}Service) Create{{capitalize name}}(ctx context.Conte
|
|
|
105
107
|
{{/if}}
|
|
106
108
|
{{/if}}
|
|
107
109
|
|
|
110
|
+
for role, pbEntity := range federatedData {
|
|
111
|
+
if config.AppConfig != nil {
|
|
112
|
+
messaging.PublishEvent(config.AppConfig.GoDuck.Messaging.MQTT.TopicPrefix, role, "CREATE", "{{capitalize name}}", pbEntity, nil)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
108
116
|
return &pb.{{capitalize name}}Reply{
|
|
109
117
|
FederatedData: federatedData,
|
|
110
118
|
}, nil
|
|
@@ -213,6 +221,12 @@ func (s *{{capitalize name}}Service) Update{{capitalize name}}(ctx context.Conte
|
|
|
213
221
|
{{/if}}
|
|
214
222
|
{{/if}}
|
|
215
223
|
|
|
224
|
+
for role, pbEntity := range federatedData {
|
|
225
|
+
if config.AppConfig != nil {
|
|
226
|
+
messaging.PublishEvent(config.AppConfig.GoDuck.Messaging.MQTT.TopicPrefix, role, "UPDATE", "{{capitalize name}}", pbEntity, nil)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
216
230
|
return &pb.{{capitalize name}}Reply{
|
|
217
231
|
FederatedData: federatedData,
|
|
218
232
|
}, nil
|
|
@@ -222,25 +236,37 @@ func (s *{{capitalize name}}Service) Delete{{capitalize name}}(ctx context.Conte
|
|
|
222
236
|
{{#if isDocument}}
|
|
223
237
|
{{#if isFederated}}
|
|
224
238
|
siloConns, _ := ctx.Value("tenantMongoConnections").(map[string]*mongo.Database)
|
|
225
|
-
for
|
|
239
|
+
for role, db := range siloConns {
|
|
226
240
|
db.Collection("{{toLowerCase name}}s").DeleteOne(ctx, bson.M{"_id": req.Id})
|
|
241
|
+
if config.AppConfig != nil {
|
|
242
|
+
messaging.PublishEvent(config.AppConfig.GoDuck.Messaging.MQTT.TopicPrefix, role, "DELETE", "{{capitalize name}}", map[string]interface{}{"id": req.Id}, nil)
|
|
243
|
+
}
|
|
227
244
|
}
|
|
228
245
|
{{else}}
|
|
229
246
|
db, ok := ctx.Value("tenantMongoDB").(*mongo.Database)
|
|
230
247
|
if ok {
|
|
231
248
|
db.Collection("{{toLowerCase name}}s").DeleteOne(ctx, bson.M{"_id": req.Id})
|
|
249
|
+
if config.AppConfig != nil {
|
|
250
|
+
messaging.PublishEvent(config.AppConfig.GoDuck.Messaging.MQTT.TopicPrefix, "tenant", "DELETE", "{{capitalize name}}", map[string]interface{}{"id": req.Id}, nil)
|
|
251
|
+
}
|
|
232
252
|
}
|
|
233
253
|
{{/if}}
|
|
234
254
|
{{else}}
|
|
235
255
|
{{#if isFederated}}
|
|
236
256
|
siloConns, _ := ctx.Value("tenantSiloConns").(map[string]*gorm.DB)
|
|
237
|
-
for
|
|
257
|
+
for role, db := range siloConns {
|
|
238
258
|
db.WithContext(ctx).Where("id = ?", req.Id).Delete(&models.{{capitalize name}}{})
|
|
259
|
+
if config.AppConfig != nil {
|
|
260
|
+
messaging.PublishEvent(config.AppConfig.GoDuck.Messaging.MQTT.TopicPrefix, role, "DELETE", "{{capitalize name}}", map[string]interface{}{"id": req.Id}, nil)
|
|
261
|
+
}
|
|
239
262
|
}
|
|
240
263
|
{{else}}
|
|
241
264
|
db, ok := ctx.Value("tenantDBConn").(*gorm.DB)
|
|
242
265
|
if ok {
|
|
243
266
|
db.WithContext(ctx).Where("id = ?", req.Id).Delete(&models.{{capitalize name}}{})
|
|
267
|
+
if config.AppConfig != nil {
|
|
268
|
+
messaging.PublishEvent(config.AppConfig.GoDuck.Messaging.MQTT.TopicPrefix, "tenant", "DELETE", "{{capitalize name}}", map[string]interface{}{"id": req.Id}, nil)
|
|
269
|
+
}
|
|
244
270
|
}
|
|
245
271
|
{{/if}}
|
|
246
272
|
{{/if}}
|