go-duck-cli 1.0.8 → 1.1.1
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 +30 -15
- package/generators/ai_docs.js +130 -0
- package/generators/broker.js +63 -0
- package/generators/config.js +149 -7
- package/generators/devops.js +210 -43
- package/generators/docs.js +23 -4
- package/generators/elasticsearch.js +263 -0
- package/generators/kratos.js +229 -41
- package/generators/metering.js +280 -48
- package/generators/migrations.js +92 -198
- package/generators/mqtt.js +2 -39
- package/generators/multitenancy.js +274 -71
- package/generators/nats.js +39 -0
- package/generators/outbox.js +171 -0
- package/generators/postgrest.js +7 -3
- package/generators/postman.js +405 -0
- package/generators/repository.js +27 -0
- package/generators/router.js +27 -0
- package/generators/security.js +95 -14
- package/generators/serverless.js +147 -0
- package/generators/storage.js +589 -0
- package/generators/swagger.js +84 -60
- package/generators/telemetry.js +23 -32
- package/generators/websocket.js +55 -21
- package/index.js +481 -116
- package/package.json +6 -4
- package/parser/gdl.js +163 -24
- package/templates/docs/index.html.hbs +5 -5
- package/templates/docs/layout.hbs +221 -62
- package/templates/docs/pages/audit.hbs +83 -35
- package/templates/docs/pages/cli.hbs +18 -0
- package/templates/docs/pages/configuration.hbs +241 -0
- package/templates/docs/pages/datadog.hbs +46 -0
- package/templates/docs/pages/elasticsearch.hbs +121 -0
- package/templates/docs/pages/federation.hbs +241 -0
- package/templates/docs/pages/gdl-advanced.hbs +91 -0
- package/templates/docs/pages/gdl-annotations.hbs +137 -0
- package/templates/docs/pages/gdl-entities.hbs +134 -0
- package/templates/docs/pages/gdl-relationships.hbs +80 -0
- package/templates/docs/pages/gdl.hbs +60 -204
- package/templates/docs/pages/graphql.hbs +58 -44
- package/templates/docs/pages/grpc.hbs +53 -90
- package/templates/docs/pages/hybrid-store.hbs +127 -0
- package/templates/docs/pages/index.hbs +418 -149
- package/templates/docs/pages/keycloak.hbs +43 -0
- package/templates/docs/pages/legend.hbs +116 -0
- package/templates/docs/pages/mosquitto.hbs +39 -0
- package/templates/docs/pages/multitenancy.hbs +139 -71
- package/templates/docs/pages/otel.hbs +40 -0
- package/templates/docs/pages/realtime.hbs +38 -12
- package/templates/docs/pages/redis.hbs +40 -0
- package/templates/docs/pages/rest.hbs +120 -202
- package/templates/docs/pages/saga.hbs +94 -0
- package/templates/docs/pages/security.hbs +150 -44
- package/templates/docs/pages/serverless.hbs +157 -0
- package/templates/docs/pages/storage.hbs +127 -0
- package/templates/docs/pages/wizard.hbs +683 -0
- package/templates/docs/triple_identity_registry.png +0 -0
- package/templates/go/controller.go.hbs +287 -283
- package/templates/go/entity.go.hbs +17 -15
- package/templates/go/main.go.hbs +47 -180
- package/templates/go/migrator.go.hbs +65 -0
- package/templates/go/router.go.hbs +272 -0
- package/templates/graphql/resolver.go.hbs +53 -34
- package/templates/graphql/schema.graphql.hbs +17 -5
- package/templates/kratos/service.go.hbs +169 -34
- package/templates/proto/entity.proto.hbs +10 -14
- package/test_nested.gdl +21 -0
- package/templates/docs/intro.mp4 +0 -0
- package/test_parser.js +0 -9
|
@@ -8,27 +8,29 @@ import (
|
|
|
8
8
|
)
|
|
9
9
|
|
|
10
10
|
type {{name}} struct {
|
|
11
|
+
{{#if isDocument}}
|
|
12
|
+
ID string `json:"id" bson:"_id,omitempty"`
|
|
13
|
+
{{else}}
|
|
11
14
|
ID uint `gorm:"primaryKey" json:"id"`
|
|
12
|
-
{{
|
|
13
|
-
{{
|
|
14
|
-
{{
|
|
15
|
-
{{#if
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
LastModifiedDate time.Time `gorm:"column:last_modified_date" json:"lastModifiedDate"`
|
|
20
|
-
LastModifiedUserID string `gorm:"column:last_modified_user_id" json:"lastModifiedUserId"`
|
|
15
|
+
{{/if}}
|
|
16
|
+
{{> renderFields fields=fields isDocument=isDocument}}
|
|
17
|
+
{{#if isAudited}}
|
|
18
|
+
CreatedBy string `{{#if isDocument}}bson:"created_by"{{else}}gorm:"column:created_by"{{/if}} json:"createdBy"`
|
|
19
|
+
CreatedDate time.Time `{{#if isDocument}}bson:"created_date"{{else}}gorm:"column:created_date"{{/if}} json:"createdDate"`
|
|
20
|
+
LastModifiedBy string `{{#if isDocument}}bson:"last_modified_by"{{else}}gorm:"column:last_modified_by"{{/if}} json:"lastModifiedBy"`
|
|
21
|
+
LastModifiedDate time.Time `{{#if isDocument}}bson:"last_modified_date"{{else}}gorm:"column:last_modified_date"{{/if}} json:"lastModifiedDate"`
|
|
21
22
|
{{else}}
|
|
22
|
-
CreatedAt time.Time `gorm:"autoCreateTime" json:"createdAt"`
|
|
23
|
-
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updatedAt"`
|
|
23
|
+
CreatedAt time.Time `{{#if isDocument}}bson:"created_at"{{else}}gorm:"autoCreateTime"{{/if}} json:"createdAt"`
|
|
24
|
+
UpdatedAt time.Time `{{#if isDocument}}bson:"updated_at"{{else}}gorm:"autoUpdateTime"{{/if}} json:"updatedAt"`
|
|
24
25
|
{{/if}}
|
|
25
26
|
{{#each relationships}}
|
|
26
27
|
{{#if (eq from.entity ../name)}}
|
|
27
|
-
{{capitalize from.field}} []{{capitalize to.entity}} `gorm:"foreignKey:{{capitalize to.field}}ID" json:"{{from.field}}"`
|
|
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
29
|
{{else}}
|
|
29
|
-
{{capitalize to.field}}ID *uint `gorm:"column:{{to.field}}_id;index" json:"{{to.field}}Id"`
|
|
30
|
-
{{
|
|
31
|
-
json:"{{to.field}},omitempty"`
|
|
30
|
+
{{capitalize to.field}}ID {{#if ../isDocument}}string{{else}}{{#if fromIsDocument}}string{{else}}*uint{{/if}}{{/if}} `{{#if ../isDocument}}bson:"{{toLowerCase to.field}}_id"{{else}}gorm:"column:{{to.field}}_id;index"{{/if}} json:"{{to.field}}Id"`
|
|
31
|
+
{{#unless ../isDocument}}
|
|
32
|
+
{{capitalize to.field}} *{{capitalize from.entity}} `{{#unless fromIsDocument}}gorm:"foreignKey:{{capitalize to.field}}ID"{{else}}gorm:"-"{{/unless}} json:"{{to.field}},omitempty"`
|
|
33
|
+
{{/unless}}
|
|
32
34
|
{{/if}}
|
|
33
35
|
{{/each}}
|
|
34
36
|
}
|
package/templates/go/main.go.hbs
CHANGED
|
@@ -1,189 +1,56 @@
|
|
|
1
1
|
package main
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
-
"context"
|
|
5
|
-
"fmt"
|
|
6
|
-
"log"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
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
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"log"
|
|
7
|
+
|
|
8
|
+
"{{app_name}}/config"
|
|
9
|
+
"{{app_name}}/logger"
|
|
10
|
+
"{{app_name}}/router"
|
|
11
|
+
"{{app_name}}/internal/server"
|
|
12
|
+
"{{app_name}}/internal/worker"
|
|
13
|
+
"{{app_name}}/internal/repository"
|
|
14
|
+
"gorm.io/driver/postgres"
|
|
15
|
+
"gorm.io/gorm"
|
|
29
16
|
)
|
|
30
17
|
|
|
31
18
|
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
|
|
39
|
-
logger.InitLogger(appConfig)
|
|
40
|
-
logger.Info("Starting %s version %s...", appConfig.GoDuck.Name, appConfig.GoDuck.Version)
|
|
41
|
-
|
|
42
|
-
// 3.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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)
|
|
19
|
+
// 1. Load Configuration
|
|
20
|
+
appConfig, err := config.LoadConfig()
|
|
21
|
+
if err != nil {
|
|
22
|
+
log.Fatalf("Failed to load configuration: %v", err)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 2. Initialize Logging
|
|
26
|
+
logger.InitLogger(appConfig)
|
|
27
|
+
logger.Info("Starting %s version %s...", appConfig.GoDuck.Name, appConfig.GoDuck.Version)
|
|
28
|
+
|
|
29
|
+
// 3. Setup Reusable Router (Initializes DB, Cache, etc.)
|
|
30
|
+
r := router.SetupRouter(appConfig)
|
|
31
|
+
|
|
32
|
+
// 4. Background Services (Only for standard binary deployment)
|
|
33
|
+
// Initialize repository for Kratos
|
|
34
|
+
masterDB, err := gorm.Open(postgres.Open(appConfig.GetDSN()), &gorm.Config{})
|
|
35
|
+
if err == nil {
|
|
36
|
+
repo := repository.NewRepository(masterDB)
|
|
37
|
+
|
|
38
|
+
// Start Kratos gRPC Server
|
|
39
|
+
go func() {
|
|
40
|
+
grpcSrv := server.NewGRPCServer(appConfig, repo)
|
|
41
|
+
logger.Info("Starting Kratos gRPC server on %s", appConfig.GoDuck.Server.GRPC.Addr)
|
|
42
|
+
if err := grpcSrv.Start(context.Background()); err != nil {
|
|
43
|
+
logger.Error("Failed to start Kratos gRPC server: %v", err)
|
|
44
|
+
}
|
|
45
|
+
}()
|
|
46
|
+
|
|
47
|
+
// Start Distributed Outbox Worker
|
|
48
|
+
outboxWorker := worker.NewOutboxWorker(masterDB, appConfig)
|
|
49
|
+
go outboxWorker.Start(context.Background())
|
|
85
50
|
}
|
|
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, appConfig))
|
|
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.POST("/{{toLowerCase name}}s/bulk", {{toLowerCase name}}Ctrl.BulkCreate)
|
|
167
|
-
api.GET("/{{toLowerCase name}}s", {{toLowerCase name}}Ctrl.GetAll)
|
|
168
|
-
api.GET("/{{toLowerCase name}}s/:id", {{toLowerCase name}}Ctrl.GetByID)
|
|
169
|
-
api.PUT("/{{toLowerCase name}}s/:id", {{toLowerCase name}}Ctrl.Update)
|
|
170
|
-
api.PUT("/{{toLowerCase name}}s/bulk", {{toLowerCase name}}Ctrl.BulkUpdate)
|
|
171
|
-
api.PATCH("/{{toLowerCase name}}s/:id", {{toLowerCase name}}Ctrl.Patch)
|
|
172
|
-
api.PATCH("/{{toLowerCase name}}s/bulk", {{toLowerCase name}}Ctrl.BulkPatch)
|
|
173
|
-
api.DELETE("/{{toLowerCase name}}s/:id", {{toLowerCase name}}Ctrl.Delete)
|
|
174
|
-
{{/each}}
|
|
175
|
-
// go-duck-needle-add-route
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// 10. GraphQL
|
|
179
|
-
r.POST("/graphql", func(c *gin.Context) {
|
|
180
|
-
graph.HandleGraphQLRequest(masterDB, c)
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
// 11. WebSockets
|
|
184
|
-
wsDispatcher := ws.NewDispatcher(masterDB)
|
|
185
|
-
r.GET("/ws", middleware.JWTMiddleware(), wsDispatcher.HandleConnection)
|
|
186
51
|
|
|
187
|
-
|
|
188
|
-
|
|
52
|
+
// 5. Start HTTP Server
|
|
53
|
+
port := fmt.Sprintf(":%d", appConfig.GoDuck.Server.Port)
|
|
54
|
+
logger.Info("Starting HTTP server on %s", port)
|
|
55
|
+
r.Run(port)
|
|
189
56
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
package migrations
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"embed"
|
|
5
|
+
"fmt"
|
|
6
|
+
|
|
7
|
+
"github.com/pressly/goose/v3"
|
|
8
|
+
"gorm.io/gorm"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// Global migrations embed filesystem
|
|
12
|
+
//go:embed sql/*.sql
|
|
13
|
+
var embedMigrations embed.FS
|
|
14
|
+
|
|
15
|
+
// RunGoNativeMigrations performs idempotent schema updates using Goose and maintains a migration version table
|
|
16
|
+
func RunGoNativeMigrations(db *gorm.DB) error {
|
|
17
|
+
fmt.Printf("Checking embedded migrations with Goose...\n")
|
|
18
|
+
|
|
19
|
+
// Get native SQL DB from GORM
|
|
20
|
+
sqlDB, err := db.DB()
|
|
21
|
+
if err != nil {
|
|
22
|
+
return fmt.Errorf("failed to get sql.DB: %v", err)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 1. Setup Goose
|
|
26
|
+
// Goose natively supports versioning and history tracking via its version table.
|
|
27
|
+
// We rename it to 'database_changelog' for the user.
|
|
28
|
+
goose.SetTableName("database_changelog")
|
|
29
|
+
if err := goose.SetDialect("postgres"); err != nil {
|
|
30
|
+
return fmt.Errorf("failed to set goose dialect: %v", err)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 2. Use embedded FS (migrator.go is in 'migrations/' and SQL files are in 'migrations/sql/')
|
|
34
|
+
// So the path relative to Go source file is 'sql'
|
|
35
|
+
goose.SetBaseFS(embedMigrations)
|
|
36
|
+
|
|
37
|
+
// 3. Run Migrations
|
|
38
|
+
if err := goose.Up(sqlDB, "sql"); err != nil {
|
|
39
|
+
return fmt.Errorf("goose up failed: %v", err)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fmt.Println("All Goose migrations completed successfully.")
|
|
43
|
+
return nil
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// RunGoNativeMigrationsForTenant runs migrations for a specific tenant DB
|
|
47
|
+
func RunGoNativeMigrationsForTenant(db *gorm.DB) error {
|
|
48
|
+
sqlDB, err := db.DB()
|
|
49
|
+
if err != nil {
|
|
50
|
+
return fmt.Errorf("failed to get sql.DB: %v", err)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
goose.SetTableName("database_changelog")
|
|
54
|
+
if err := goose.SetDialect("postgres"); err != nil {
|
|
55
|
+
return fmt.Errorf("failed to set goose dialect: %v", err)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
goose.SetBaseFS(embedMigrations)
|
|
59
|
+
|
|
60
|
+
if err := goose.Up(sqlDB, "sql"); err != nil {
|
|
61
|
+
return fmt.Errorf("goose up failed for tenant: %v", err)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return nil
|
|
65
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
package router
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"log"
|
|
6
|
+
"net/http"
|
|
7
|
+
"time"
|
|
8
|
+
|
|
9
|
+
"github.com/gin-gonic/gin"
|
|
10
|
+
"go.mongodb.org/mongo-driver/mongo"
|
|
11
|
+
"go.mongodb.org/mongo-driver/mongo/options"
|
|
12
|
+
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
|
|
13
|
+
"gorm.io/driver/postgres"
|
|
14
|
+
"gorm.io/gorm"
|
|
15
|
+
"gorm.io/plugin/opentelemetry/tracing"
|
|
16
|
+
"{{app_name}}/management"
|
|
17
|
+
"{{app_name}}/middleware"
|
|
18
|
+
"{{app_name}}/controllers"
|
|
19
|
+
"{{app_name}}/models"
|
|
20
|
+
"{{app_name}}/migrations"
|
|
21
|
+
"{{app_name}}/graph"
|
|
22
|
+
"{{app_name}}/ws"
|
|
23
|
+
"{{app_name}}/config"
|
|
24
|
+
"{{app_name}}/logger"
|
|
25
|
+
"{{app_name}}/messaging"
|
|
26
|
+
"{{app_name}}/cache"
|
|
27
|
+
"{{app_name}}/resilience"
|
|
28
|
+
"{{app_name}}/internal/storage"
|
|
29
|
+
"{{app_name}}/internal/telemetry"
|
|
30
|
+
"{{app_name}}/internal/search"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
func SetupRouter(appConfig *config.Config) *gin.Engine {
|
|
34
|
+
// 1. Initialize Logging & Observability
|
|
35
|
+
logger.InitLogger(appConfig)
|
|
36
|
+
|
|
37
|
+
// 2. Initialize OpenTelemetry Tracing
|
|
38
|
+
_, err := telemetry.InitTelemetry(appConfig)
|
|
39
|
+
if err != nil {
|
|
40
|
+
log.Printf("Warning: Failed to initialize OpenTelemetry: %v", err)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 3. Initialize Resilience Layer (Circuit Breaker)
|
|
44
|
+
resilience.InitResilience(appConfig)
|
|
45
|
+
|
|
46
|
+
// 4. Initialize Messaging (Dual-Broker: MQTT + NATS - Async)
|
|
47
|
+
messaging.InitMQTT(appConfig)
|
|
48
|
+
messaging.InitNATS(appConfig)
|
|
49
|
+
|
|
50
|
+
// 5. Initialize Distributed Caching (Redis)
|
|
51
|
+
cache.InitCache(appConfig)
|
|
52
|
+
|
|
53
|
+
// 6. Initialize Elasticsearch Engine (Spring-style Search)
|
|
54
|
+
search.InitElasticsearch(appConfig)
|
|
55
|
+
|
|
56
|
+
// 7. Initialize Storage Providers & Bootstrap Credentials
|
|
57
|
+
if err := storage.InitStorage(appConfig); err != nil {
|
|
58
|
+
log.Printf("Warning: Failed to bootstrap storage credentials: %v", err)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 8. Initialize Hybrid-Store connections with Pool Tuning & Tracing
|
|
62
|
+
var masterDB *gorm.DB
|
|
63
|
+
if appConfig.GoDuck.Datasource.Host != "" {
|
|
64
|
+
db, err := gorm.Open(postgres.Open(appConfig.GetDSN()), &gorm.Config{})
|
|
65
|
+
if err != nil {
|
|
66
|
+
log.Fatalf("Failed to connect to master database: %v", err)
|
|
67
|
+
}
|
|
68
|
+
masterDB = db
|
|
69
|
+
|
|
70
|
+
// Inject GORM OTel Plugin
|
|
71
|
+
if err := masterDB.Use(tracing.NewPlugin()); err != nil {
|
|
72
|
+
log.Printf("Warning: Failed to inject GORM OTel plugin: %v", err)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
sqlDB, _ := masterDB.DB()
|
|
76
|
+
sqlDB.SetMaxOpenConns(appConfig.GoDuck.Datasource.MaxOpenConns)
|
|
77
|
+
sqlDB.SetMaxIdleConns(appConfig.GoDuck.Datasource.MaxIdleConns)
|
|
78
|
+
sqlDB.SetConnMaxLifetime(appConfig.GoDuck.Datasource.ConnMaxLifetime)
|
|
79
|
+
|
|
80
|
+
// 9. Auto-Migrate Master System Registry (Infrastructure)
|
|
81
|
+
masterDB.AutoMigrate(&models.TenantRole{}, &models.APIUsage{}, &models.APIUsageHistory{}, &models.AuditLog{}, &models.DistributedOutbox{})
|
|
82
|
+
|
|
83
|
+
// 10. Run Goose Migrations (The Modern Go-Native Way)
|
|
84
|
+
if err := migrations.RunGoNativeMigrations(masterDB); err != nil {
|
|
85
|
+
logger.Error("Goose Migration failed: %v", err)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
var masterMongo *mongo.Client
|
|
90
|
+
_ = masterMongo // Skip unused variable error if no MongoDB entities exist
|
|
91
|
+
|
|
92
|
+
if appConfig.GoDuck.Datasource.MongoDB.Enabled {
|
|
93
|
+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
94
|
+
defer cancel()
|
|
95
|
+
client, err := mongo.Connect(ctx, options.Client().ApplyURI(appConfig.GetMongoURI()))
|
|
96
|
+
if err != nil {
|
|
97
|
+
log.Fatalf("Failed to connect to master MongoDB: %v", err)
|
|
98
|
+
}
|
|
99
|
+
masterMongo = client
|
|
100
|
+
log.Println("✅ Connected to master MongoDB")
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
r := gin.Default()
|
|
104
|
+
|
|
105
|
+
// 11. Global Middleware (OTel, Rate Limit & CORS)
|
|
106
|
+
if appConfig.GoDuck.Telemetry.OTel.Enabled {
|
|
107
|
+
r.Use(otelgin.Middleware(appConfig.GoDuck.Name))
|
|
108
|
+
}
|
|
109
|
+
r.Use(middleware.RateLimitMiddleware(appConfig))
|
|
110
|
+
r.Use(middleware.CORSMiddleware(appConfig))
|
|
111
|
+
|
|
112
|
+
// Health Check
|
|
113
|
+
r.GET("/health", func(c *gin.Context) {
|
|
114
|
+
c.JSON(http.StatusOK, gin.H{"status": "UP"})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// Swagger Docs & UI
|
|
118
|
+
r.StaticFile("/swagger.json", "./docs/swagger.json")
|
|
119
|
+
r.GET("/swagger", func(c *gin.Context) {
|
|
120
|
+
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(`<!DOCTYPE html>
|
|
121
|
+
<html lang="en">
|
|
122
|
+
<head>
|
|
123
|
+
<meta charset="UTF-8">
|
|
124
|
+
<title>Swagger UI</title>
|
|
125
|
+
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui.css" />
|
|
126
|
+
<style>
|
|
127
|
+
html { box-sizing: border-box; overflow: -moz-scrollbars-vertical; overflow-y: scroll; }
|
|
128
|
+
*, *:before, *:after { box-sizing: inherit; }
|
|
129
|
+
body { margin:0; background: #fafafa; }
|
|
130
|
+
</style>
|
|
131
|
+
</head>
|
|
132
|
+
<body>
|
|
133
|
+
<div id="swagger-ui"></div>
|
|
134
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui-bundle.js"></script>
|
|
135
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui-standalone-preset.js"></script>
|
|
136
|
+
<script>
|
|
137
|
+
window.onload = function() {
|
|
138
|
+
const ui = SwaggerUIBundle({
|
|
139
|
+
url: "/swagger.json",
|
|
140
|
+
dom_id: '#swagger-ui',
|
|
141
|
+
deepLinking: true,
|
|
142
|
+
presets: [
|
|
143
|
+
SwaggerUIBundle.presets.apis,
|
|
144
|
+
SwaggerUIStandalonePreset
|
|
145
|
+
],
|
|
146
|
+
layout: "StandaloneLayout"
|
|
147
|
+
});
|
|
148
|
+
window.ui = ui;
|
|
149
|
+
};
|
|
150
|
+
</script>
|
|
151
|
+
</body>
|
|
152
|
+
</html>`))
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
// Management APIs (Run-time DB onboarding)
|
|
156
|
+
mgmt := r.Group("/management")
|
|
157
|
+
mgmt.Use(middleware.JWTMiddleware())
|
|
158
|
+
mgmt.Use(middleware.SuperAdminRoleMiddleware(appConfig))
|
|
159
|
+
{
|
|
160
|
+
mgmt.POST("/tenant/assign", management.CreateDatabaseAndMigrate(masterDB))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Open Application APIs (No Auth)
|
|
164
|
+
openApi := r.Group("/open/api")
|
|
165
|
+
if appConfig.GoDuck.Multitenancy.Enabled {
|
|
166
|
+
openApi.Use(middleware.PublicTenantMiddleware(masterDB, appConfig))
|
|
167
|
+
}
|
|
168
|
+
{
|
|
169
|
+
{{#each entities}}
|
|
170
|
+
// {{name}} Public Routes
|
|
171
|
+
{{#if (isAnyOperationOpen name ../openEntities)}}
|
|
172
|
+
{{toLowerCase name}}OpenCtrl := controllers.{{capitalize name}}Controller{
|
|
173
|
+
{{#if isDocument}}MongoClient: masterMongo,{{else}}DB: masterDB,{{/if}}
|
|
174
|
+
Config: appConfig,
|
|
175
|
+
}
|
|
176
|
+
{{#if (isOpen name ../openEntities 'create')}}
|
|
177
|
+
openApi.POST("/{{toLowerCase name}}s", {{toLowerCase name}}OpenCtrl.Create)
|
|
178
|
+
openApi.POST("/{{toLowerCase name}}s/bulk", {{toLowerCase name}}OpenCtrl.BulkCreate)
|
|
179
|
+
{{/if}}
|
|
180
|
+
{{#if (isOpen name ../openEntities 'read')}}
|
|
181
|
+
openApi.GET("/{{toLowerCase name}}s", {{toLowerCase name}}OpenCtrl.GetAll)
|
|
182
|
+
openApi.GET("/{{toLowerCase name}}s/:id", {{toLowerCase name}}OpenCtrl.GetByID)
|
|
183
|
+
{{/if}}
|
|
184
|
+
{{#if (isOpen name ../openEntities 'update')}}
|
|
185
|
+
openApi.PUT("/{{toLowerCase name}}s/:id", {{toLowerCase name}}OpenCtrl.Update)
|
|
186
|
+
openApi.PUT("/{{toLowerCase name}}s/bulk", {{toLowerCase name}}OpenCtrl.BulkUpdate)
|
|
187
|
+
{{/if}}
|
|
188
|
+
{{#if (isOpen name ../openEntities 'delete')}}
|
|
189
|
+
openApi.DELETE("/{{toLowerCase name}}s/:id", {{toLowerCase name}}OpenCtrl.Delete)
|
|
190
|
+
{{/if}}
|
|
191
|
+
{{#if (isOpen name ../openEntities 'update')}}
|
|
192
|
+
openApi.PATCH("/{{toLowerCase name}}s/:id", {{toLowerCase name}}OpenCtrl.Patch)
|
|
193
|
+
openApi.PATCH("/{{toLowerCase name}}s/bulk", {{toLowerCase name}}OpenCtrl.BulkPatch)
|
|
194
|
+
{{/if}}
|
|
195
|
+
{{/if}}
|
|
196
|
+
{{/each}}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Secured Application APIs
|
|
200
|
+
api := r.Group("/api")
|
|
201
|
+
api.Use(middleware.JWTMiddleware())
|
|
202
|
+
api.Use(middleware.TenantMiddleware(masterDB, appConfig))
|
|
203
|
+
api.Use(middleware.AuditMiddleware(masterDB))
|
|
204
|
+
api.Use(middleware.MeteringMiddleware(masterDB))
|
|
205
|
+
{
|
|
206
|
+
// Silo Portfolio
|
|
207
|
+
api.GET("/silos/me", management.GetMySilos(masterDB))
|
|
208
|
+
|
|
209
|
+
// Business Reporting APIs
|
|
210
|
+
meteringCtrl := controllers.MeteringController{DB: masterDB}
|
|
211
|
+
api.GET("/metering/usage", meteringCtrl.GetUsage)
|
|
212
|
+
api.GET("/metering/history", meteringCtrl.GetHistory)
|
|
213
|
+
|
|
214
|
+
// --- Infrastructure/Confidential Layer ---
|
|
215
|
+
admin := r.Group("/api/admin")
|
|
216
|
+
admin.Use(middleware.JWTMiddleware())
|
|
217
|
+
admin.Use(middleware.SuperAdminRoleMiddleware(appConfig))
|
|
218
|
+
{
|
|
219
|
+
auditCtrl := controllers.AuditController{DB: masterDB}
|
|
220
|
+
admin.GET("/audit", auditCtrl.GetLogs)
|
|
221
|
+
admin.POST("/metering/limit", meteringCtrl.SetLimit)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Search
|
|
225
|
+
searchCtrl := controllers.SearchController{DB: masterDB}
|
|
226
|
+
api.GET("/rpc/:table", searchCtrl.GenericSearch)
|
|
227
|
+
|
|
228
|
+
// Elasticsearch Global Search
|
|
229
|
+
if appConfig.GoDuck.Elasticsearch.Enabled {
|
|
230
|
+
esCtrl := controllers.NewESSearchController(appConfig)
|
|
231
|
+
api.GET("/search/:entity", esCtrl.GlobalSearch)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Global Storage Endpoints
|
|
235
|
+
storageCtrl := controllers.StorageController{}
|
|
236
|
+
api.POST("/storage/upload", storageCtrl.Upload)
|
|
237
|
+
|
|
238
|
+
// Exact Retrieval (Requires ?provider=)
|
|
239
|
+
api.GET("/storage/download/*key", storageCtrl.Download)
|
|
240
|
+
|
|
241
|
+
// Distributed Cross-Scan Retrieval (No provider required)
|
|
242
|
+
api.GET("/storage/scan/*key", storageCtrl.CrossScanDownload)
|
|
243
|
+
|
|
244
|
+
{{#each entities}}
|
|
245
|
+
// {{name}} Routes
|
|
246
|
+
{{toLowerCase name}}Ctrl := controllers.{{capitalize name}}Controller{
|
|
247
|
+
{{#if isDocument}}MongoClient: masterMongo,{{else}}DB: masterDB,{{/if}}
|
|
248
|
+
Config: appConfig,
|
|
249
|
+
}
|
|
250
|
+
api.POST("/{{toLowerCase name}}s", {{toLowerCase name}}Ctrl.Create)
|
|
251
|
+
api.POST("/{{toLowerCase name}}s/bulk", {{toLowerCase name}}Ctrl.BulkCreate)
|
|
252
|
+
api.GET("/{{toLowerCase name}}s", {{toLowerCase name}}Ctrl.GetAll)
|
|
253
|
+
api.GET("/{{toLowerCase name}}s/:id", {{toLowerCase name}}Ctrl.GetByID)
|
|
254
|
+
api.PUT("/{{toLowerCase name}}s/:id", {{toLowerCase name}}Ctrl.Update)
|
|
255
|
+
api.PUT("/{{toLowerCase name}}s/bulk", {{toLowerCase name}}Ctrl.BulkUpdate)
|
|
256
|
+
api.PATCH("/{{toLowerCase name}}s/:id", {{toLowerCase name}}Ctrl.Patch)
|
|
257
|
+
api.PATCH("/{{toLowerCase name}}s/bulk", {{toLowerCase name}}Ctrl.BulkPatch)
|
|
258
|
+
api.DELETE("/{{toLowerCase name}}s/:id", {{toLowerCase name}}Ctrl.Delete)
|
|
259
|
+
{{/each}}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// GraphQL
|
|
263
|
+
r.POST("/graphql", middleware.JWTMiddleware(), middleware.TenantMiddleware(masterDB, appConfig), func(c *gin.Context) {
|
|
264
|
+
graph.HandleGraphQLRequest(c)
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
// WebSockets
|
|
268
|
+
wsDispatcher := ws.NewDispatcher(masterDB)
|
|
269
|
+
r.GET("/ws", middleware.JWTMiddleware(), middleware.TenantMiddleware(masterDB, appConfig), wsDispatcher.HandleConnection)
|
|
270
|
+
|
|
271
|
+
return r
|
|
272
|
+
}
|