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.
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 +481 -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
@@ -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
- {{#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"`
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
- {{capitalize to.field}} *{{capitalize from.entity}} `gorm:"foreignKey:{{capitalize to.field}}ID"
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
  }
@@ -1,189 +1,56 @@
1
1
  package main
2
2
 
3
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
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 & 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)
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
- port := fmt.Sprintf(":%d", appConfig.GoDuck.Server.Port)
188
- r.Run(port)
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
+ }