go-duck-cli 1.0.0
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/README.md +130 -0
- package/generators/cache.js +107 -0
- package/generators/config.js +173 -0
- package/generators/devops.js +212 -0
- package/generators/docs.js +74 -0
- package/generators/graphql.js +38 -0
- package/generators/kratos.js +157 -0
- package/generators/logger.js +68 -0
- package/generators/metering.js +143 -0
- package/generators/migrations.js +240 -0
- package/generators/mqtt.js +87 -0
- package/generators/multitenancy.js +130 -0
- package/generators/postgrest.js +115 -0
- package/generators/repository.js +28 -0
- package/generators/resilience.js +69 -0
- package/generators/security.js +168 -0
- package/generators/swagger.js +145 -0
- package/generators/telemetry.js +121 -0
- package/generators/websocket.js +162 -0
- package/index.js +592 -0
- package/package.json +23 -0
- package/parser/gdl.js +162 -0
- package/templates/application.yml.hbs +18 -0
- package/templates/docs/gin_bottle.png +0 -0
- package/templates/docs/index.html.hbs +226 -0
- package/templates/docs/intro.mp4 +0 -0
- package/templates/docs/kratos_mark.png +0 -0
- package/templates/docs/layout.hbs +106 -0
- package/templates/docs/logo.png +0 -0
- package/templates/docs/pages/audit.hbs +39 -0
- package/templates/docs/pages/cli.hbs +83 -0
- package/templates/docs/pages/gdl.hbs +223 -0
- package/templates/docs/pages/graphql.hbs +51 -0
- package/templates/docs/pages/grpc.hbs +100 -0
- package/templates/docs/pages/index.hbs +181 -0
- package/templates/docs/pages/integrations.hbs +83 -0
- package/templates/docs/pages/observability.hbs +34 -0
- package/templates/docs/pages/realtime.hbs +43 -0
- package/templates/docs/pages/rest.hbs +149 -0
- package/templates/docs/pages/security.hbs +31 -0
- package/templates/go/controller.go.hbs +236 -0
- package/templates/go/entity.go.hbs +34 -0
- package/templates/go/enum.go.hbs +7 -0
- package/templates/go/main.go.hbs +186 -0
- package/templates/graphql/resolver.go.hbs +50 -0
- package/templates/graphql/schema.graphql.hbs +64 -0
- package/templates/kratos/service.go.hbs +104 -0
- package/templates/proto/entity.proto.hbs +95 -0
- package/test_parser.js +9 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
package controllers
|
|
2
|
+
|
|
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"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
type {{capitalize name}}Controller struct {
|
|
19
|
+
DB *gorm.DB
|
|
20
|
+
Config *config.Config
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Create{{capitalize name}}
|
|
24
|
+
func (ctrl *{{capitalize name}}Controller) Create(c *gin.Context) {
|
|
25
|
+
tenant, _ := c.Get("tenantDB")
|
|
26
|
+
tenantStr := fmt.Sprintf("%v", tenant)
|
|
27
|
+
ctx := c.Request.Context()
|
|
28
|
+
|
|
29
|
+
var entity models.{{capitalize name}}
|
|
30
|
+
if err := c.ShouldBindJSON(&entity); err != nil {
|
|
31
|
+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
if err := ctrl.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
|
+
|
|
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
|
+
var entities []models.{{capitalize name}}
|
|
54
|
+
ctx := c.Request.Context()
|
|
55
|
+
query := ctrl.DB.WithContext(ctx)
|
|
56
|
+
|
|
57
|
+
// 1. Pagination
|
|
58
|
+
page, _ := strconv.Atoi(c.DefaultQuery("page", "0"))
|
|
59
|
+
size, _ := strconv.Atoi(c.DefaultQuery("size", "20"))
|
|
60
|
+
query = query.Offset(page * size).Limit(size)
|
|
61
|
+
|
|
62
|
+
// 2. Eager Loading (Full Bodied) vs Lazy (IDs Only)
|
|
63
|
+
eager := c.Query("eager") == "true"
|
|
64
|
+
if eager {
|
|
65
|
+
{{#each relationships}}
|
|
66
|
+
query = query.Preload("{{capitalize from.field}}")
|
|
67
|
+
query = query.Preload("{{capitalize to.field}}")
|
|
68
|
+
{{/each}}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 3. Simple Filtering (Optimized for CRUD)
|
|
72
|
+
// Example: ?name=eq.John
|
|
73
|
+
for key, values := range c.Request.URL.Query() {
|
|
74
|
+
if key == "page" || key == "size" || key == "eager" || key == "sort" {
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
for _, val := range values {
|
|
78
|
+
query = query.Where(key+" = ?", val)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if err := query.Find(&entities).Error; err != nil {
|
|
83
|
+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
c.JSON(http.StatusOK, entities)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// GetByID
|
|
90
|
+
func (ctrl *{{capitalize name}}Controller) GetByID(c *gin.Context) {
|
|
91
|
+
id := c.Param("id")
|
|
92
|
+
tenant, _ := c.Get("tenantDB")
|
|
93
|
+
tenantStr := fmt.Sprintf("%v", tenant)
|
|
94
|
+
ctx := c.Request.Context()
|
|
95
|
+
|
|
96
|
+
// Tenant-Aware Cache Key
|
|
97
|
+
cacheKey := fmt.Sprintf("%s:{{capitalize name}}:%s", tenantStr, id)
|
|
98
|
+
|
|
99
|
+
var entity models.{{capitalize name}}
|
|
100
|
+
|
|
101
|
+
// 1. Check Distributed Cache (Redis)
|
|
102
|
+
if cache.Get(cacheKey, &entity) {
|
|
103
|
+
c.JSON(http.StatusOK, entity)
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 2. Fallback to DB (With Context for Tracing)
|
|
108
|
+
query := ctrl.DB.WithContext(ctx)
|
|
109
|
+
if c.Query("eager") == "true" {
|
|
110
|
+
{{#each relationships}}
|
|
111
|
+
query = query.Preload("{{capitalize from.field}}")
|
|
112
|
+
query = query.Preload("{{capitalize to.field}}")
|
|
113
|
+
{{/each}}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if err := query.First(&entity, id).Error; err != nil {
|
|
117
|
+
c.JSON(http.StatusNotFound, gin.H{"error": "Resource not found"})
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 3. Update Cache (Resilient)
|
|
122
|
+
resilience.Execute(func() (interface{}, error) {
|
|
123
|
+
cache.Set(cacheKey, entity, ctrl.Config.GoDuck.Cache.Redis.TTL)
|
|
124
|
+
return nil, nil
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
c.JSON(http.StatusOK, entity)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Update (PUT) - Full Update
|
|
131
|
+
func (ctrl *{{capitalize name}}Controller) Update(c *gin.Context) {
|
|
132
|
+
id := c.Param("id")
|
|
133
|
+
tenant, _ := c.Get("tenantDB")
|
|
134
|
+
tenantStr := fmt.Sprintf("%v", tenant)
|
|
135
|
+
ctx := c.Request.Context()
|
|
136
|
+
|
|
137
|
+
var entity models.{{capitalize name}}
|
|
138
|
+
if err := ctrl.DB.WithContext(ctx).First(&entity, id).Error; err != nil {
|
|
139
|
+
c.JSON(http.StatusNotFound, gin.H{"error": "Resource not found"})
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Capture Previous State
|
|
144
|
+
prev := entity
|
|
145
|
+
|
|
146
|
+
if err := c.ShouldBindJSON(&entity); err != nil {
|
|
147
|
+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if err := ctrl.DB.WithContext(ctx).Save(&entity).Error; err != nil {
|
|
152
|
+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Cache Invalidation (Tenant Aware)
|
|
157
|
+
cache.Delete(fmt.Sprintf("%s:{{capitalize name}}:%s", tenantStr, id))
|
|
158
|
+
|
|
159
|
+
// MQTT Event (Resilient)
|
|
160
|
+
resilience.Execute(func() (interface{}, error) {
|
|
161
|
+
messaging.PublishEvent(ctrl.Config.GoDuck.Messaging.MQTT.TopicPrefix, "UPDATE", "{{capitalize name}}", entity, prev)
|
|
162
|
+
return nil, nil
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
c.JSON(http.StatusOK, entity)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Patch (PATCH) - Partial Update
|
|
169
|
+
func (ctrl *{{capitalize name}}Controller) Patch(c *gin.Context) {
|
|
170
|
+
id := c.Param("id")
|
|
171
|
+
tenant, _ := c.Get("tenantDB")
|
|
172
|
+
tenantStr := fmt.Sprintf("%v", tenant)
|
|
173
|
+
ctx := c.Request.Context()
|
|
174
|
+
|
|
175
|
+
var entity models.{{capitalize name}}
|
|
176
|
+
if err := ctrl.DB.WithContext(ctx).First(&entity, id).Error; err != nil {
|
|
177
|
+
c.JSON(http.StatusNotFound, gin.H{"error": "Resource not found"})
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
prev := entity
|
|
181
|
+
|
|
182
|
+
var updates map[string]interface{}
|
|
183
|
+
if err := c.ShouldBindJSON(&updates); err != nil {
|
|
184
|
+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if err := ctrl.DB.WithContext(ctx).Model(&entity).Updates(updates).Error; err != nil {
|
|
189
|
+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Fetch updated
|
|
194
|
+
ctrl.DB.WithContext(ctx).First(&entity, id)
|
|
195
|
+
|
|
196
|
+
// Cache Invalidation (Tenant Aware)
|
|
197
|
+
cache.Delete(fmt.Sprintf("%s:{{capitalize name}}:%s", tenantStr, id))
|
|
198
|
+
|
|
199
|
+
// MQTT Event (Resilient)
|
|
200
|
+
resilience.Execute(func() (interface{}, error) {
|
|
201
|
+
messaging.PublishEvent(ctrl.Config.GoDuck.Messaging.MQTT.TopicPrefix, "PATCH", "{{capitalize name}}", entity, prev)
|
|
202
|
+
return nil, nil
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
c.JSON(http.StatusOK, gin.H{"message": "Updated successfully", "data": entity})
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Delete
|
|
209
|
+
func (ctrl *{{capitalize name}}Controller) Delete(c *gin.Context) {
|
|
210
|
+
id := c.Param("id")
|
|
211
|
+
tenant, _ := c.Get("tenantDB")
|
|
212
|
+
tenantStr := fmt.Sprintf("%v", tenant)
|
|
213
|
+
ctx := c.Request.Context()
|
|
214
|
+
|
|
215
|
+
var entity models.{{capitalize name}}
|
|
216
|
+
if err := ctrl.DB.WithContext(ctx).First(&entity, id).Error; err != nil {
|
|
217
|
+
c.JSON(http.StatusNotFound, gin.H{"error": "Resource not found"})
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if err := ctrl.DB.WithContext(ctx).Delete(&entity).Error; err != nil {
|
|
222
|
+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Cache Invalidation (Tenant Aware)
|
|
227
|
+
cache.Delete(fmt.Sprintf("%s:{{capitalize name}}:%s", tenantStr, id))
|
|
228
|
+
|
|
229
|
+
// MQTT Event (Resilient)
|
|
230
|
+
resilience.Execute(func() (interface{}, error) {
|
|
231
|
+
messaging.PublishEvent(ctrl.Config.GoDuck.Messaging.MQTT.TopicPrefix, "DELETE", "{{capitalize name}}", entity, nil)
|
|
232
|
+
return nil, nil
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
c.JSON(http.StatusNoContent, nil)
|
|
236
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
package models
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"time"
|
|
5
|
+
{{#if (hasJson fields)}}
|
|
6
|
+
"gorm.io/datatypes"
|
|
7
|
+
{{/if}}
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
type {{name}} struct {
|
|
11
|
+
ID uint `gorm:"primaryKey" json:"id"`
|
|
12
|
+
{{#each fields}}
|
|
13
|
+
{{capitalize name}} {{toGoType type}} `{{#if unique}}gorm:"uniqueIndex" {{/if}}{{#if (isJson type)}}gorm:"type:{{toLowerCase type}};serializer:json" {{/if}}json:"{{name}}" {{#if required}}binding:"required"{{/if}}`
|
|
14
|
+
{{/each}}
|
|
15
|
+
{{#if (eq annotation "@Audited")}}
|
|
16
|
+
CreatedBy string `gorm:"column:created_by" json:"createdBy"`
|
|
17
|
+
CreatedDate time.Time `gorm:"column:created_date" json:"createdDate"`
|
|
18
|
+
LastModifiedBy string `gorm:"column:last_modified_by" json:"lastModifiedBy"`
|
|
19
|
+
LastModifiedDate time.Time `gorm:"column:last_modified_date" json:"lastModifiedDate"`
|
|
20
|
+
LastModifiedUserID string `gorm:"column:last_modified_user_id" json:"lastModifiedUserId"`
|
|
21
|
+
{{else}}
|
|
22
|
+
CreatedAt time.Time `gorm:"autoCreateTime" json:"createdAt"`
|
|
23
|
+
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updatedAt"`
|
|
24
|
+
{{/if}}
|
|
25
|
+
{{#each relationships}}
|
|
26
|
+
{{#if (eq from.entity ../name)}}
|
|
27
|
+
{{capitalize from.field}} []{{capitalize to.entity}} `gorm:"foreignKey:{{capitalize to.field}}ID" json:"{{from.field}}"`
|
|
28
|
+
{{else}}
|
|
29
|
+
{{capitalize to.field}}ID *uint `gorm:"column:{{to.field}}_id;index" json:"{{to.field}}Id"`
|
|
30
|
+
{{capitalize to.field}} *{{capitalize from.entity}} `gorm:"foreignKey:{{capitalize to.field}}ID"
|
|
31
|
+
json:"{{to.field}},omitempty"`
|
|
32
|
+
{{/if}}
|
|
33
|
+
{{/each}}
|
|
34
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"log"
|
|
7
|
+
"net/http"
|
|
8
|
+
|
|
9
|
+
"github.com/gin-gonic/gin"
|
|
10
|
+
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
|
|
11
|
+
"gorm.io/driver/postgres"
|
|
12
|
+
"gorm.io/gorm"
|
|
13
|
+
"gorm.io/plugin/opentelemetry/tracing"
|
|
14
|
+
"{{app_name}}/management"
|
|
15
|
+
"{{app_name}}/middleware"
|
|
16
|
+
"{{app_name}}/controllers"
|
|
17
|
+
"{{app_name}}/graph"
|
|
18
|
+
"{{app_name}}/ws"
|
|
19
|
+
"{{app_name}}/config"
|
|
20
|
+
"{{app_name}}/logger"
|
|
21
|
+
"{{app_name}}/messaging"
|
|
22
|
+
"{{app_name}}/cache"
|
|
23
|
+
"{{app_name}}/resilience"
|
|
24
|
+
"{{app_name}}/internal/telemetry"
|
|
25
|
+
"{{app_name}}/internal/repository"
|
|
26
|
+
"{{app_name}}/internal/server"
|
|
27
|
+
k_grpc "github.com/go-kratos/kratos/v2/transport/grpc"
|
|
28
|
+
// go-duck-needle-add-import
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
func main() {
|
|
32
|
+
// 1. Load Configuration
|
|
33
|
+
appConfig, err := config.LoadConfig()
|
|
34
|
+
if err != nil {
|
|
35
|
+
log.Fatalf("Failed to load configuration: %v", err)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 2. Initialize Logging & Observability (Datadog)
|
|
39
|
+
logger.InitLogger(appConfig)
|
|
40
|
+
logger.Info("Starting %s version %s...", appConfig.GoDuck.Name, appConfig.GoDuck.Version)
|
|
41
|
+
|
|
42
|
+
// 3. Initialize OpenTelemetry Tracing
|
|
43
|
+
shutdown, err := telemetry.InitTelemetry(appConfig)
|
|
44
|
+
if err != nil {
|
|
45
|
+
log.Printf("Warning: Failed to initialize OpenTelemetry: %v", err)
|
|
46
|
+
}
|
|
47
|
+
defer shutdown(context.Background())
|
|
48
|
+
|
|
49
|
+
// 4. Initialize Resilience Layer (Circuit Breaker)
|
|
50
|
+
resilience.InitResilience(appConfig)
|
|
51
|
+
|
|
52
|
+
// 5. Initialize MQTT Messaging (for Webhooks/Audit)
|
|
53
|
+
messaging.InitMQTT(appConfig)
|
|
54
|
+
|
|
55
|
+
// 6. Initialize Distributed Caching (Redis)
|
|
56
|
+
cache.InitCache(appConfig)
|
|
57
|
+
|
|
58
|
+
// 7. Initialize master DB connection with Pool Tuning & Tracing
|
|
59
|
+
masterDB, err := gorm.Open(postgres.Open(appConfig.GetDSN()), &gorm.Config{})
|
|
60
|
+
if err != nil {
|
|
61
|
+
log.Fatalf("Failed to connect to master database: %v", err)
|
|
62
|
+
}
|
|
63
|
+
// go-duck-needle-add-init-server
|
|
64
|
+
|
|
65
|
+
// Inject GORM OTel Plugin
|
|
66
|
+
if err := masterDB.Use(tracing.NewPlugin()); err != nil {
|
|
67
|
+
log.Printf("Warning: Failed to inject GORM OTel plugin: %v", err)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
sqlDB, _ := masterDB.DB()
|
|
71
|
+
sqlDB.SetMaxOpenConns(appConfig.GoDuck.Datasource.MaxOpenConns)
|
|
72
|
+
sqlDB.SetMaxIdleConns(appConfig.GoDuck.Datasource.MaxIdleConns)
|
|
73
|
+
sqlDB.SetConnMaxLifetime(appConfig.GoDuck.Datasource.ConnMaxLifetime)
|
|
74
|
+
|
|
75
|
+
// 8. Initialize Repository
|
|
76
|
+
repo := repository.NewRepository(masterDB)
|
|
77
|
+
// go-duck-needle-add-init-repository
|
|
78
|
+
|
|
79
|
+
// 9. Initialize & Start Kratos gRPC Server (in background)
|
|
80
|
+
go func() {
|
|
81
|
+
grpcSrv := server.NewGRPCServer(appConfig, repo)
|
|
82
|
+
logger.Info("Starting Kratos gRPC server on %s", appConfig.GoDuck.Server.GRPC.Addr)
|
|
83
|
+
if err := grpcSrv.Start(context.Background()); err != nil {
|
|
84
|
+
logger.Error("Failed to start Kratos gRPC server: %v", err)
|
|
85
|
+
}
|
|
86
|
+
}()
|
|
87
|
+
// go-duck-needle-add-grpc-start
|
|
88
|
+
|
|
89
|
+
r := gin.Default()
|
|
90
|
+
|
|
91
|
+
// 8. Global Middleware (OTel, Rate Limit & CORS)
|
|
92
|
+
if appConfig.GoDuck.Telemetry.OTel.Enabled {
|
|
93
|
+
r.Use(otelgin.Middleware(appConfig.GoDuck.Name))
|
|
94
|
+
}
|
|
95
|
+
r.Use(middleware.RateLimitMiddleware(appConfig))
|
|
96
|
+
r.Use(middleware.CORSMiddleware(appConfig))
|
|
97
|
+
|
|
98
|
+
// Health Check
|
|
99
|
+
r.GET("/health", func(c *gin.Context) {
|
|
100
|
+
c.JSON(http.StatusOK, gin.H{"status": "UP"})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// Swagger Docs & UI
|
|
104
|
+
r.StaticFile("/swagger.json", "./docs/swagger.json")
|
|
105
|
+
r.GET("/swagger", func(c *gin.Context) {
|
|
106
|
+
c.Header("Content-Type", "text/html; charset=utf-8")
|
|
107
|
+
c.String(http.StatusOK, \`
|
|
108
|
+
<!DOCTYPE html>
|
|
109
|
+
<html lang="en">
|
|
110
|
+
|
|
111
|
+
<head>
|
|
112
|
+
<meta charset="UTF-8">
|
|
113
|
+
<title>Swagger UI</title>
|
|
114
|
+
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@3/swagger-ui.css">
|
|
115
|
+
</head>
|
|
116
|
+
|
|
117
|
+
<body>
|
|
118
|
+
<div id="swagger-ui"></div>
|
|
119
|
+
<script src="https://unpkg.com/swagger-ui-dist@3/swagger-ui-bundle.js"> </script>
|
|
120
|
+
<script>
|
|
121
|
+
window.onload = function () {
|
|
122
|
+
window.ui = SwaggerUIBundle({
|
|
123
|
+
url: "/swagger.json",
|
|
124
|
+
dom_id: '#swagger-ui',
|
|
125
|
+
deepLinking: true,
|
|
126
|
+
presets: [SwaggerUIBundle.presets.apis],
|
|
127
|
+
layout: "BaseLayout"
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
</script>
|
|
131
|
+
</body>
|
|
132
|
+
|
|
133
|
+
</html>
|
|
134
|
+
\`)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// Management APIs (Run-time DB creation)
|
|
138
|
+
mgmt := r.Group("/management")
|
|
139
|
+
{
|
|
140
|
+
mgmt.POST("/db/create", management.CreateDatabaseAndMigrate(masterDB))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 9. Secured Application APIs
|
|
144
|
+
api := r.Group("/api")
|
|
145
|
+
api.Use(middleware.JWTMiddleware())
|
|
146
|
+
api.Use(middleware.TenantMiddleware(masterDB))
|
|
147
|
+
api.Use(middleware.AuditMiddleware(masterDB))
|
|
148
|
+
api.Use(middleware.MeteringMiddleware(masterDB))
|
|
149
|
+
{
|
|
150
|
+
// Observability
|
|
151
|
+
auditCtrl := controllers.AuditController{DB: masterDB}
|
|
152
|
+
api.GET("/audit", auditCtrl.GetLogs)
|
|
153
|
+
|
|
154
|
+
meteringCtrl := controllers.MeteringController{DB: masterDB}
|
|
155
|
+
api.POST("/metering/limit", meteringCtrl.SetLimit)
|
|
156
|
+
api.GET("/metering/usage", meteringCtrl.GetUsage)
|
|
157
|
+
|
|
158
|
+
// Search
|
|
159
|
+
searchCtrl := controllers.SearchController{DB: masterDB}
|
|
160
|
+
api.GET("/rpc/:table", searchCtrl.GenericSearch)
|
|
161
|
+
|
|
162
|
+
{{#each entities}}
|
|
163
|
+
// {{name}} Routes
|
|
164
|
+
{{toLowerCase name}}Ctrl := controllers.{{capitalize name}}Controller{DB: masterDB, Config: appConfig}
|
|
165
|
+
api.POST("/{{toLowerCase name}}s", {{toLowerCase name}}Ctrl.Create)
|
|
166
|
+
api.GET("/{{toLowerCase name}}s", {{toLowerCase name}}Ctrl.GetAll)
|
|
167
|
+
api.GET("/{{toLowerCase name}}s/:id", {{toLowerCase name}}Ctrl.GetByID)
|
|
168
|
+
api.PUT("/{{toLowerCase name}}s/:id", {{toLowerCase name}}Ctrl.Update)
|
|
169
|
+
api.PATCH("/{{toLowerCase name}}s/:id", {{toLowerCase name}}Ctrl.Patch)
|
|
170
|
+
api.DELETE("/{{toLowerCase name}}s/:id", {{toLowerCase name}}Ctrl.Delete)
|
|
171
|
+
{{/each}}
|
|
172
|
+
// go-duck-needle-add-route
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 10. GraphQL
|
|
176
|
+
r.POST("/graphql", func(c *gin.Context) {
|
|
177
|
+
graph.HandleGraphQLRequest(masterDB, c)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
// 11. WebSockets
|
|
181
|
+
wsDispatcher := ws.NewDispatcher(masterDB)
|
|
182
|
+
r.GET("/ws", middleware.JWTMiddleware(), wsDispatcher.HandleConnection)
|
|
183
|
+
|
|
184
|
+
port := fmt.Sprintf(":%d", appConfig.GoDuck.Server.Port)
|
|
185
|
+
r.Run(port)
|
|
186
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
package graph
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"net/http"
|
|
6
|
+
"{{app_name}}/models"
|
|
7
|
+
|
|
8
|
+
"github.com/gin-gonic/gin"
|
|
9
|
+
"gorm.io/gorm"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// HandleGraphQLRequest is the main bridge for the Gin web framework.
|
|
13
|
+
// In a production app, we would use a more robust engine (like gqlgen).
|
|
14
|
+
func HandleGraphQLRequest(db *gorm.DB, c *gin.Context) {
|
|
15
|
+
var input struct {
|
|
16
|
+
Query string \`json:"query"\`
|
|
17
|
+
Variables map[string]interface{} \`json:"variables"\`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if err := c.BindJSON(&input); err != nil {
|
|
21
|
+
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid GraphQL request"})
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// We'll perform basic routing for the POC.
|
|
26
|
+
// For production, this should integrate with a real schema loader.
|
|
27
|
+
c.JSON(http.StatusOK, gin.H{
|
|
28
|
+
"data": "GraphQL Handler Integrated Successfully!",
|
|
29
|
+
"note": "Ready for schema execution.",
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Resolver for each entity
|
|
34
|
+
{{#each entities}}
|
|
35
|
+
func Resolve{{capitalize name}}(db *gorm.DB, id uint) (*models.{{capitalize name}}, error) {
|
|
36
|
+
var e models.{{capitalize name}}
|
|
37
|
+
if err := db.First(&e, id).Error; err != nil {
|
|
38
|
+
return nil, err
|
|
39
|
+
}
|
|
40
|
+
return &e, nil
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func ResolveAll{{capitalize name}}s(db *gorm.DB) ([]models.{{capitalize name}}, error) {
|
|
44
|
+
var list []models.{{capitalize name}}
|
|
45
|
+
if err := db.Find(&list).Error; err != nil {
|
|
46
|
+
return nil, err
|
|
47
|
+
}
|
|
48
|
+
return list, nil
|
|
49
|
+
}
|
|
50
|
+
{{/each}}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{{#each entities}}
|
|
2
|
+
type {{name}} {
|
|
3
|
+
id: ID!
|
|
4
|
+
{{#each fields}}
|
|
5
|
+
{{name}}: {{gql_type type}}{{#if required}}!{{/if}}
|
|
6
|
+
{{/each}}
|
|
7
|
+
{{#if (eq annotation "@Audited")}}
|
|
8
|
+
createdBy: String
|
|
9
|
+
createdDate: String
|
|
10
|
+
lastModifiedBy: String
|
|
11
|
+
lastModifiedDate: String
|
|
12
|
+
lastModifiedUserId: String
|
|
13
|
+
{{/if}}
|
|
14
|
+
{{#each relationships}}
|
|
15
|
+
{{#if (eq from.entity ../name)}}
|
|
16
|
+
{{from.field}}: [{{capitalize to.entity}}]
|
|
17
|
+
{{else}}
|
|
18
|
+
{{to.field}}: {{capitalize from.entity}}
|
|
19
|
+
{{/if}}
|
|
20
|
+
{{/each}}
|
|
21
|
+
createdAt: String
|
|
22
|
+
updatedAt: String
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
input Create{{name}}Input {
|
|
26
|
+
{{#each fields}}
|
|
27
|
+
{{name}}: {{gql_type type}}{{#if required}}!{{/if}}
|
|
28
|
+
{{/each}}
|
|
29
|
+
{{#each relationships}}
|
|
30
|
+
{{#if (eq to.entity ../name)}}
|
|
31
|
+
{{to.field}}Id: ID!
|
|
32
|
+
{{/if}}
|
|
33
|
+
{{/each}}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
input Update{{name}}Input {
|
|
37
|
+
{{#each fields}}
|
|
38
|
+
{{name}}: {{gql_type type}}
|
|
39
|
+
{{/each}}
|
|
40
|
+
}
|
|
41
|
+
{{/each}}
|
|
42
|
+
|
|
43
|
+
{{#each enums}}
|
|
44
|
+
enum {{name}} {
|
|
45
|
+
{{#each values}}
|
|
46
|
+
{{this}}
|
|
47
|
+
{{/each}}
|
|
48
|
+
}
|
|
49
|
+
{{/each}}
|
|
50
|
+
|
|
51
|
+
type Query {
|
|
52
|
+
{{#each entities}}
|
|
53
|
+
get{{capitalize name}}(id: ID!): {{name}}
|
|
54
|
+
list{{capitalize name}}s(page: Int, size: Int): [{{name}}]
|
|
55
|
+
{{/each}}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type Mutation {
|
|
59
|
+
{{#each entities}}
|
|
60
|
+
create{{capitalize name}}(input: Create{{name}}Input!): {{name}}
|
|
61
|
+
update{{capitalize name}}(id: ID!, input: Update{{name}}Input!): {{name}}
|
|
62
|
+
delete{{capitalize name}}(id: ID!): Boolean
|
|
63
|
+
{{/each}}
|
|
64
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
package service
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
pb "{{projectName}}/api/v1"
|
|
6
|
+
"{{projectName}}/internal/repository"
|
|
7
|
+
"{{projectName}}/models"
|
|
8
|
+
{{#if (hasJson fields)}}
|
|
9
|
+
"gorm.io/datatypes"
|
|
10
|
+
{{/if}}
|
|
11
|
+
"google.golang.org/protobuf/types/known/timestamppb"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
type {{capitalize name}}Service struct {
|
|
15
|
+
pb.Unimplemented{{capitalize name}}ServiceServer
|
|
16
|
+
repo *repository.Repository
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func New{{capitalize name}}Service(repo *repository.Repository) *{{capitalize name}}Service {
|
|
20
|
+
return &{{capitalize name}}Service{repo: repo}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func (s *{{capitalize name}}Service) Create{{capitalize name}}(ctx context.Context, req *pb.Create{{capitalize name}}Request) (*pb.{{capitalize name}}Reply, error) {
|
|
24
|
+
entity := &models.{{capitalize name}}{
|
|
25
|
+
{{#each fields}}
|
|
26
|
+
{{capitalize name}}: {{#if (isJson type)}}datatypes.JSON(req.{{capitalize name}}){{else}}{{#if (toGoCast type)}}{{toGoCast type}}(req.{{capitalize name}}){{else}}req.{{capitalize name}}{{/if}}{{/if}},
|
|
27
|
+
{{/each}}
|
|
28
|
+
}
|
|
29
|
+
if err := s.repo.DB.WithContext(ctx).Create(entity).Error; err != nil {
|
|
30
|
+
return nil, err
|
|
31
|
+
}
|
|
32
|
+
return &pb.{{capitalize name}}Reply{
|
|
33
|
+
Data: map{{capitalize name}}ToPb(entity),
|
|
34
|
+
}, nil
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func (s *{{capitalize name}}Service) Get{{capitalize name}}(ctx context.Context, req *pb.Get{{capitalize name}}Request) (*pb.{{capitalize name}}Reply, error) {
|
|
38
|
+
var entity models.{{capitalize name}}
|
|
39
|
+
if err := s.repo.DB.WithContext(ctx).First(&entity, req.Id).Error; err != nil {
|
|
40
|
+
return nil, err
|
|
41
|
+
}
|
|
42
|
+
return &pb.{{capitalize name}}Reply{
|
|
43
|
+
Data: map{{capitalize name}}ToPb(&entity),
|
|
44
|
+
}, nil
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func (s *{{capitalize name}}Service) Update{{capitalize name}}(ctx context.Context, req *pb.Update{{capitalize name}}Request) (*pb.{{capitalize name}}Reply, error) {
|
|
48
|
+
var entity models.{{capitalize name}}
|
|
49
|
+
if err := s.repo.DB.WithContext(ctx).First(&entity, req.Id).Error; err != nil {
|
|
50
|
+
return nil, err
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
{{#each fields}}
|
|
54
|
+
entity.{{capitalize name}} = {{#if (isJson type)}}datatypes.JSON(req.{{capitalize name}}){{else}}{{#if (toGoCast type)}}{{toGoCast type}}(req.{{capitalize name}}){{else}}req.{{capitalize name}}{{/if}}{{/if}}
|
|
55
|
+
{{/each}}
|
|
56
|
+
|
|
57
|
+
if err := s.repo.DB.WithContext(ctx).Save(&entity).Error; err != nil {
|
|
58
|
+
return nil, err
|
|
59
|
+
}
|
|
60
|
+
return &pb.{{capitalize name}}Reply{
|
|
61
|
+
Data: map{{capitalize name}}ToPb(&entity),
|
|
62
|
+
}, nil
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func (s *{{capitalize name}}Service) Delete{{capitalize name}}(ctx context.Context, req *pb.Delete{{capitalize name}}Request) (*pb.Delete{{capitalize name}}Reply, error) {
|
|
66
|
+
if err := s.repo.DB.WithContext(ctx).Delete(&models.{{capitalize name}}{}, req.Id).Error; err != nil {
|
|
67
|
+
return nil, err
|
|
68
|
+
}
|
|
69
|
+
return &pb.Delete{{capitalize name}}Reply{Message: "Success"}, nil
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func (s *{{capitalize name}}Service) List{{capitalize name}}(ctx context.Context, req *pb.List{{capitalize name}}Request) (*pb.List{{capitalize name}}Reply, error) {
|
|
73
|
+
var results []models.{{capitalize name}}
|
|
74
|
+
var total int64
|
|
75
|
+
|
|
76
|
+
db := s.repo.DB.WithContext(ctx).Model(&models.{{capitalize name}}{})
|
|
77
|
+
db.Count(&total)
|
|
78
|
+
|
|
79
|
+
offset := (req.Page - 1) * req.PageSize
|
|
80
|
+
if err := db.Limit(int(req.PageSize)).Offset(int(offset)).Find(&results).Error; err != nil {
|
|
81
|
+
return nil, err
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
pbResults := make([]*pb.{{capitalize name}}, len(results))
|
|
85
|
+
for i, r := range results {
|
|
86
|
+
pbResults[i] = map{{capitalize name}}ToPb(&r)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return &pb.List{{capitalize name}}Reply{
|
|
90
|
+
Results: pbResults,
|
|
91
|
+
Total: total,
|
|
92
|
+
}, nil
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
func map{{capitalize name}}ToPb(m *models.{{capitalize name}}) *pb.{{capitalize name}} {
|
|
96
|
+
return &pb.{{capitalize name}}{
|
|
97
|
+
Id: uint64(m.ID),
|
|
98
|
+
{{#each fields}}
|
|
99
|
+
{{capitalize name}}: {{#if (isJson type)}}string(m.{{capitalize name}}){{else}}{{#if (toProtoCast type)}}{{toProtoCast type}}(m.{{capitalize name}}){{else}}m.{{capitalize name}}{{/if}}{{/if}},
|
|
100
|
+
{{/each}}
|
|
101
|
+
CreatedAt: timestamppb.New(m.CreatedAt),
|
|
102
|
+
UpdatedAt: timestamppb.New(m.UpdatedAt),
|
|
103
|
+
}
|
|
104
|
+
}
|