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.
Files changed (49) hide show
  1. package/README.md +130 -0
  2. package/generators/cache.js +107 -0
  3. package/generators/config.js +173 -0
  4. package/generators/devops.js +212 -0
  5. package/generators/docs.js +74 -0
  6. package/generators/graphql.js +38 -0
  7. package/generators/kratos.js +157 -0
  8. package/generators/logger.js +68 -0
  9. package/generators/metering.js +143 -0
  10. package/generators/migrations.js +240 -0
  11. package/generators/mqtt.js +87 -0
  12. package/generators/multitenancy.js +130 -0
  13. package/generators/postgrest.js +115 -0
  14. package/generators/repository.js +28 -0
  15. package/generators/resilience.js +69 -0
  16. package/generators/security.js +168 -0
  17. package/generators/swagger.js +145 -0
  18. package/generators/telemetry.js +121 -0
  19. package/generators/websocket.js +162 -0
  20. package/index.js +592 -0
  21. package/package.json +23 -0
  22. package/parser/gdl.js +162 -0
  23. package/templates/application.yml.hbs +18 -0
  24. package/templates/docs/gin_bottle.png +0 -0
  25. package/templates/docs/index.html.hbs +226 -0
  26. package/templates/docs/intro.mp4 +0 -0
  27. package/templates/docs/kratos_mark.png +0 -0
  28. package/templates/docs/layout.hbs +106 -0
  29. package/templates/docs/logo.png +0 -0
  30. package/templates/docs/pages/audit.hbs +39 -0
  31. package/templates/docs/pages/cli.hbs +83 -0
  32. package/templates/docs/pages/gdl.hbs +223 -0
  33. package/templates/docs/pages/graphql.hbs +51 -0
  34. package/templates/docs/pages/grpc.hbs +100 -0
  35. package/templates/docs/pages/index.hbs +181 -0
  36. package/templates/docs/pages/integrations.hbs +83 -0
  37. package/templates/docs/pages/observability.hbs +34 -0
  38. package/templates/docs/pages/realtime.hbs +43 -0
  39. package/templates/docs/pages/rest.hbs +149 -0
  40. package/templates/docs/pages/security.hbs +31 -0
  41. package/templates/go/controller.go.hbs +236 -0
  42. package/templates/go/entity.go.hbs +34 -0
  43. package/templates/go/enum.go.hbs +7 -0
  44. package/templates/go/main.go.hbs +186 -0
  45. package/templates/graphql/resolver.go.hbs +50 -0
  46. package/templates/graphql/schema.graphql.hbs +64 -0
  47. package/templates/kratos/service.go.hbs +104 -0
  48. package/templates/proto/entity.proto.hbs +95 -0
  49. package/test_parser.js +9 -0
@@ -0,0 +1,87 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+
5
+ export const generateMQTTCode = async (config, outputDir) => {
6
+ const messagingDir = path.join(outputDir, 'messaging');
7
+ await fs.ensureDir(messagingDir);
8
+
9
+ const mqttClientGo = `
10
+ package messaging
11
+
12
+ import (
13
+ "encoding/json"
14
+ "fmt"
15
+ "log"
16
+ "time"
17
+
18
+ "{{app_name}}/config"
19
+ mq "github.com/eclipse/paho.mqtt.golang"
20
+ )
21
+
22
+ var MQTTClient mq.Client
23
+
24
+ type EventMessage struct {
25
+ Action string \`json:"action"\`
26
+ Entity string \`json:"entity"\`
27
+ EventTime time.Time \`json:"event_time"\`
28
+ Payload interface{} \`json:"payload"\`
29
+ PreviousValue interface{} \`json:"previous_value,omitempty"\`
30
+ }
31
+
32
+ func InitMQTT(cfg *config.Config) {
33
+ if !cfg.GoDuck.Messaging.MQTT.Enabled {
34
+ log.Println("MQTT Messaging is disabled.")
35
+ return
36
+ }
37
+
38
+ opts := mq.NewClientOptions()
39
+ opts.AddBroker(cfg.GoDuck.Messaging.MQTT.Broker)
40
+ opts.SetClientID(cfg.GoDuck.Messaging.MQTT.ClientID)
41
+
42
+ if cfg.GoDuck.Messaging.MQTT.Username != "" {
43
+ opts.SetUsername(cfg.GoDuck.Messaging.MQTT.Username)
44
+ opts.SetPassword(cfg.GoDuck.Messaging.MQTT.Password)
45
+ }
46
+
47
+ opts.OnConnect = func(c mq.Client) {
48
+ log.Printf("Connected to MQTT Broker: %s", cfg.GoDuck.Messaging.MQTT.Broker)
49
+ }
50
+
51
+ client := mq.NewClient(opts)
52
+ if token := client.Connect(); token.Wait() && token.Error() != nil {
53
+ log.Printf("Failed to connect to MQTT: %v", token.Error())
54
+ return
55
+ }
56
+
57
+ MQTTClient = client
58
+ }
59
+
60
+ func PublishEvent(topicPrefix string, action string, entity string, payload interface{}, prev interface{}) {
61
+ if MQTTClient == nil || !MQTTClient.IsConnected() {
62
+ return
63
+ }
64
+
65
+ msg := EventMessage{
66
+ Action: action,
67
+ Entity: entity,
68
+ EventTime: time.Now(),
69
+ Payload: payload,
70
+ PreviousValue: prev,
71
+ }
72
+
73
+ data, err := json.Marshal(msg)
74
+ if err != nil {
75
+ log.Printf("Error marshaling MQTT message: %v", err)
76
+ return
77
+ }
78
+
79
+ topic := fmt.Sprintf("%s/%s/%s", topicPrefix, entity, action)
80
+ token := MQTTClient.Publish(topic, 0, false, data)
81
+ token.Wait()
82
+ }
83
+ `;
84
+
85
+ await fs.writeFile(path.join(messagingDir, 'mqtt.go'), mqttClientGo.replace(/{{app_name}}/g, config.name));
86
+ console.log(chalk.gray(' Generated MQTT Messaging Package'));
87
+ };
@@ -0,0 +1,130 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+
5
+ export const generateMultitenancy = async (config, outputDir) => {
6
+
7
+ const multitenancyTemplate = `
8
+ package middleware
9
+
10
+ import (
11
+ "fmt"
12
+ "net/http"
13
+
14
+ "github.com/gin-gonic/gin"
15
+ "gorm.io/gorm"
16
+ )
17
+
18
+ func TenantMiddleware(db *gorm.DB) gin.HandlerFunc {
19
+ return func(c *gin.Context) {
20
+ // 1. Get roles from JWT (previously set by JWTMiddleware)
21
+ userRolesInterface, exists := c.Get("UserRoles")
22
+ if !exists {
23
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "No roles found in token"})
24
+ c.Abort()
25
+ return
26
+ }
27
+
28
+ roles, ok := userRolesInterface.([]interface{})
29
+ if !ok {
30
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid roles format"})
31
+ c.Abort()
32
+ return
33
+ }
34
+
35
+ // 2. Lookup DB name for the roles in tenant_roles table
36
+ var dbName string
37
+ err := db.Raw("SELECT db_name FROM tenant_roles WHERE role_name IN ? LIMIT 1", roles).Scan(&dbName).Error
38
+
39
+ if err != nil || dbName == "" {
40
+ // Fallback to default DB if no role match (optional behavior)
41
+ dbName = "go-duck"
42
+ }
43
+
44
+ // 3. Store tenant info for downstream use
45
+ c.Set("tenantDB", dbName)
46
+ c.Next()
47
+ }
48
+ }
49
+ `;
50
+
51
+ const dbApiTemplate = `
52
+ package management
53
+
54
+ import (
55
+ "fmt"
56
+ "net/http"
57
+ "os/exec"
58
+ "{{app_name}}/config"
59
+
60
+ "github.com/gin-gonic/gin"
61
+ "gorm.io/gorm"
62
+ )
63
+
64
+ type DatabaseRequest struct {
65
+ Role string \`json:"role" binding:"required"\`
66
+ DBName string \`json:"db_name" binding:"required"\`
67
+ }
68
+
69
+ func CreateDatabaseAndMigrate(masterDB *gorm.DB) gin.HandlerFunc {
70
+ return func(c *gin.Context) {
71
+ var req DatabaseRequest
72
+ if err := c.ShouldBindJSON(&req); err != nil {
73
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
74
+ return
75
+ }
76
+
77
+ // 1. CREATE DATABASE
78
+ // Note: CREATE DATABASE cannot be run in a transaction.
79
+ // We use the masterDB connection.
80
+ if err := masterDB.Exec(fmt.Sprintf("CREATE DATABASE %s", req.DBName)).Error; err != nil {
81
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create database: " + err.Error()})
82
+ return
83
+ }
84
+
85
+ // 2. Insert into roles mapping table
86
+ if err := masterDB.Exec("INSERT INTO tenant_roles (role_name, db_name) VALUES (?, ?)", req.Role, req.DBName).Error; err != nil {
87
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to map role: " + err.Error()})
88
+ return
89
+ }
90
+
91
+ // 3. Start Liquibase Migration for the new tenant
92
+ appConfig, _ := config.LoadConfig()
93
+ ds := appConfig.GoDuck.Datasource
94
+
95
+ // Construct JDBC URL for the new database
96
+ jdbcUrl := fmt.Sprintf("jdbc:postgresql://%s:%d/%s", ds.Host, ds.Port, req.DBName)
97
+
98
+ fmt.Printf("Migrating new tenant DB: %s\\n", req.DBName)
99
+
100
+ cmd := exec.Command("liquibase",
101
+ "--url=" + jdbcUrl,
102
+ "--username=" + ds.Username,
103
+ "--password=" + ds.Password,
104
+ "--changeLogFile=migrations/master.xml",
105
+ "update")
106
+
107
+ if err := cmd.Run(); err != nil {
108
+ fmt.Printf("Liquibase Error: %v\\n", err)
109
+ // We don't fail the whole request because the DB is created,
110
+ // but we warn the admin.
111
+ c.JSON(http.StatusOK, gin.H{"message": "Database created but migration failed to auto-start. Please run manually.", "error": err.Error()})
112
+ return
113
+ }
114
+
115
+ c.JSON(http.StatusOK, gin.H{"message": "Database created, role mapped, and migration completed for " + req.Role})
116
+ }
117
+ }
118
+ `;
119
+
120
+ const middlewarePath = path.join(outputDir, 'middleware/tenant_middleware.go');
121
+ const dbApiPath = path.join(outputDir, 'management/db_controller.go');
122
+
123
+ await fs.ensureDir(path.join(outputDir, 'middleware'));
124
+ await fs.ensureDir(path.join(outputDir, 'management'));
125
+
126
+ await fs.writeFile(middlewarePath, multitenancyTemplate);
127
+ await fs.writeFile(dbApiPath, dbApiTemplate.replace('{{app_name}}', config.name));
128
+
129
+ console.log(chalk.gray(' Generated Multitenancy Middleware & Management API'));
130
+ };
@@ -0,0 +1,115 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+
5
+ export const generatePostgRESTCode = async (config, outputDir) => {
6
+ const controllersDir = path.join(outputDir, 'controllers');
7
+ await fs.ensureDir(controllersDir);
8
+
9
+ const postgrestController = `
10
+ package controllers
11
+
12
+ import (
13
+ "fmt"
14
+ "net/http"
15
+ "strings"
16
+
17
+ "github.com/gin-gonic/gin"
18
+ "gorm.io/gorm"
19
+ )
20
+
21
+ type SearchController struct {
22
+ DB *gorm.DB
23
+ }
24
+
25
+ // GenericSearch handles PostgREST-like queries
26
+ // Syntax: /api/search/:table?age=gt.20&order=id.desc&limit=10&offset=0
27
+ func (sc *SearchController) GenericSearch(c *gin.Context) {
28
+ tableName := c.Param("table")
29
+ query := sc.DB.Table(tableName)
30
+
31
+ // Apply Filters
32
+ params := c.Request.URL.Query()
33
+ for key, values := range params {
34
+ if key == "order" || key == "limit" || key == "offset" || key == "select" {
35
+ continue
36
+ }
37
+
38
+ for _, val := range values {
39
+ parts := strings.SplitN(val, ".", 2)
40
+ if len(parts) < 2 {
41
+ // Default to equality
42
+ query = query.Where(fmt.Sprintf("%s = ?", key), val)
43
+ continue
44
+ }
45
+
46
+ op := parts[0]
47
+ target := parts[1]
48
+
49
+ switch op {
50
+ case "eq":
51
+ query = query.Where(fmt.Sprintf("%s = ?", key), target)
52
+ case "neq":
53
+ query = query.Where(fmt.Sprintf("%s <> ?", key), target)
54
+ case "gt":
55
+ query = query.Where(fmt.Sprintf("%s > ?", key), target)
56
+ case "gte":
57
+ query = query.Where(fmt.Sprintf("%s >= ?", key), target)
58
+ case "lt":
59
+ query = query.Where(fmt.Sprintf("%s < ?", key), target)
60
+ case "lte":
61
+ query = query.Where(fmt.Sprintf("%s <= ?", key), target)
62
+ case "like":
63
+ query = query.Where(fmt.Sprintf("%s LIKE ?", key), "%"+target+"%")
64
+ case "ilike":
65
+ query = query.Where(fmt.Sprintf("%s ILIKE ?", key), "%"+target+"%")
66
+ case "in":
67
+ list := strings.Split(target, ",")
68
+ query = query.Where(fmt.Sprintf("%s IN ?", key), list)
69
+ }
70
+ }
71
+ }
72
+
73
+ // Apply Sorting
74
+ if order := c.Query("order"); order != "" {
75
+ parts := strings.SplitN(order, ".", 2)
76
+ field := parts[0]
77
+ direction := "asc"
78
+ if len(parts) > 1 {
79
+ direction = parts[1]
80
+ }
81
+ query = query.Order(fmt.Sprintf("%s %s", field, direction))
82
+ }
83
+
84
+ // Apply Pagination
85
+ if limit := c.Query("limit"); limit != "" {
86
+ query = query.Limit(parseInt(limit, 10))
87
+ } else {
88
+ query = query.Limit(100) // Default limit
89
+ }
90
+
91
+ if offset := c.Query("offset"); offset != "" {
92
+ query = query.Offset(parseInt(offset, 0))
93
+ }
94
+
95
+ var results []map[string]interface{}
96
+ if err := query.Find(&results).Error; err != nil {
97
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Search failed: " + err.Error()})
98
+ return
99
+ }
100
+
101
+ c.JSON(http.StatusOK, results)
102
+ }
103
+
104
+ func parseInt(s string, def int) int {
105
+ var val int
106
+ if _, err := fmt.Sscanf(s, "%d", &val); err != nil {
107
+ return def
108
+ }
109
+ return val
110
+ }
111
+ `;
112
+
113
+ await fs.writeFile(path.join(controllersDir, 'search_controller.go'), postgrestController);
114
+ console.log(chalk.gray(' Generated PostgREST-like Search Controller'));
115
+ };
@@ -0,0 +1,28 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+
5
+ export const generateRepositoryCode = async (outputDir) => {
6
+ const repoDir = path.join(outputDir, 'internal', 'repository');
7
+ await fs.ensureDir(repoDir);
8
+
9
+ const repoGo = `package repository
10
+
11
+ import (
12
+ "gorm.io/gorm"
13
+ )
14
+
15
+ type Repository struct {
16
+ DB *gorm.DB
17
+ }
18
+
19
+ func NewRepository(db *gorm.DB) *Repository {
20
+ return &Repository{
21
+ DB: db,
22
+ }
23
+ }
24
+ `;
25
+
26
+ await fs.writeFile(path.join(repoDir, 'repository.go'), repoGo);
27
+ console.log(chalk.gray(' Generated Internal Repository Layer'));
28
+ };
@@ -0,0 +1,69 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+
5
+ export const generateResilienceCode = async (config, outputDir) => {
6
+ const resilienceDir = path.join(outputDir, 'resilience');
7
+ await fs.ensureDir(resilienceDir);
8
+
9
+ const circuitBreakerGo = `
10
+ package resilience
11
+
12
+ import (
13
+ "fmt"
14
+ "log"
15
+ "time"
16
+
17
+ "{{app_name}}/config"
18
+ "github.com/sony/gobreaker"
19
+ )
20
+
21
+ var (
22
+ CB *gobreaker.CircuitBreaker
23
+ )
24
+
25
+ // InitResilience initializes circuit breaker settings
26
+ func InitResilience(cfg *config.Config) {
27
+ if !cfg.GoDuck.Resilience.CircuitBreaker.Enabled {
28
+ log.Println("Circuit Breaker is disabled.")
29
+ return
30
+ }
31
+
32
+ st := gobreaker.Settings{
33
+ Name: "Global Breaker",
34
+ Interval: time.Minute,
35
+ Timeout: cfg.GoDuck.Resilience.CircuitBreaker.Timeout,
36
+ MaxRequests: cfg.GoDuck.Resilience.CircuitBreaker.SuccessThreshold,
37
+ ReadyToTrip: func(counts gobreaker.Counts) bool {
38
+ return counts.ConsecutiveFailures >= cfg.GoDuck.Resilience.CircuitBreaker.FailureThreshold
39
+ },
40
+ OnStateChange: func(name string, from gobreaker.State, to gobreaker.State) {
41
+ log.Printf("Circuit Breaker %s: State Changed from %s to %s", name, from.String(), to.String())
42
+ },
43
+ }
44
+
45
+ CB = gobreaker.NewCircuitBreaker(st)
46
+ log.Println("Circuit Breaker initialized.")
47
+ }
48
+
49
+ // Execute wraps any function call in a circuit breaker
50
+ func Execute(f func() (interface{}, error)) (interface{}, error) {
51
+ if CB == nil {
52
+ return f()
53
+ }
54
+
55
+ result, err := CB.Execute(f)
56
+ if err != nil {
57
+ if err == gobreaker.ErrOpenState {
58
+ return nil, fmt.Errorf("circuit breaker is OPEN")
59
+ }
60
+ return nil, err
61
+ }
62
+
63
+ return result, nil
64
+ }
65
+ `;
66
+
67
+ await fs.writeFile(path.join(resilienceDir, 'circuit_breaker.go'), circuitBreakerGo.replace(/{{app_name}}/g, config.name));
68
+ console.log(chalk.gray(' Generated Circuit Breaker Resilience Package'));
69
+ };
@@ -0,0 +1,168 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+
5
+ export const generateSecurityMiddleware = async (config, outputDir) => {
6
+ const middlewareDir = path.join(outputDir, 'middleware');
7
+ await fs.ensureDir(middlewareDir);
8
+
9
+ const jwtMiddleware = `
10
+ package middleware
11
+
12
+ import (
13
+ "net/http"
14
+ "strings"
15
+
16
+ "github.com/gin-gonic/gin"
17
+ "github.com/golang-jwt/jwt/v4"
18
+ )
19
+
20
+ // JWTMiddleware validates Keycloak JWTs
21
+ func JWTMiddleware() gin.HandlerFunc {
22
+ return func(c *gin.Context) {
23
+ authHeader := c.GetHeader("Authorization")
24
+ if authHeader == "" {
25
+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"})
26
+ return
27
+ }
28
+
29
+ parts := strings.Split(authHeader, " ")
30
+ if len(parts) != 2 || parts[0] != "Bearer" {
31
+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token format"})
32
+ return
33
+ }
34
+
35
+ tokenString := parts[1]
36
+
37
+ token, _, err := new(jwt.Parser).ParseUnverified(tokenString, jwt.MapClaims{})
38
+ if err != nil {
39
+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Could not parse token"})
40
+ return
41
+ }
42
+
43
+ if claims, ok := token.Claims.(jwt.MapClaims); ok {
44
+ c.Set("KeycloakID", claims["sub"])
45
+ c.Set("UserEmail", claims["email"])
46
+ if ra, ok := claims["realm_access"].(map[string]interface{}); ok {
47
+ c.Set("UserRoles", ra["roles"])
48
+ }
49
+ }
50
+
51
+ c.Next()
52
+ }
53
+ }
54
+
55
+ func GetUserID(c *gin.Context) string {
56
+ val, _ := c.Get("KeycloakID")
57
+ if str, ok := val.(string); ok {
58
+ return str
59
+ }
60
+ return "anonymous"
61
+ }
62
+ `;
63
+
64
+ const rateLimitMiddleware = `
65
+ package middleware
66
+
67
+ import (
68
+ "net/http"
69
+ "sync"
70
+ "{{app_name}}/config"
71
+
72
+ "github.com/gin-gonic/gin"
73
+ "golang.org/x/time/rate"
74
+ )
75
+
76
+ var (
77
+ limiters = make(map[string]*rate.Limiter)
78
+ mu sync.Mutex
79
+ )
80
+
81
+ // RateLimitMiddleware provides burst protection based on configuration
82
+ func RateLimitMiddleware(cfg *config.Config) gin.HandlerFunc {
83
+ return func(c *gin.Context) {
84
+ ip := c.ClientIP()
85
+
86
+ mu.Lock()
87
+ limiter, exists := limiters[ip]
88
+ if !exists {
89
+ rps := cfg.GoDuck.Security.RateLimit.RPS
90
+ burst := cfg.GoDuck.Security.RateLimit.Burst
91
+ limiter = rate.NewLimiter(rate.Limit(rps), burst)
92
+ limiters[ip] = limiter
93
+ }
94
+ mu.Unlock()
95
+
96
+ if !limiter.Allow() {
97
+ c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
98
+ "error": "Rate limit exceeded. Please try again later.",
99
+ })
100
+ return
101
+ }
102
+ c.Next()
103
+ }
104
+ }
105
+ `;
106
+
107
+ await fs.writeFile(path.join(middlewareDir, 'jwt_middleware.go'), jwtMiddleware);
108
+ await fs.writeFile(path.join(middlewareDir, 'rate_limit_middleware.go'), rateLimitMiddleware.replace('{{app_name}}', config.name));
109
+
110
+ const corsMiddleware = `
111
+ package middleware
112
+
113
+ import (
114
+ "{{app_name}}/config"
115
+ "github.com/gin-gonic/gin"
116
+ )
117
+
118
+ func CORSMiddleware(cfg *config.Config) gin.HandlerFunc {
119
+ return func(c *gin.Context) {
120
+ origins := cfg.GoDuck.Server.CORS.AllowOrigins
121
+ methods := cfg.GoDuck.Server.CORS.AllowMethods
122
+ headers := cfg.GoDuck.Server.CORS.AllowHeaders
123
+
124
+ origin := c.Request.Header.Get("Origin")
125
+ allowOrigin := ""
126
+ for _, o := range origins {
127
+ if o == "*" || o == origin {
128
+ allowOrigin = origin
129
+ if o == "*" {
130
+ allowOrigin = "*"
131
+ }
132
+ break
133
+ }
134
+ }
135
+
136
+ if allowOrigin != "" {
137
+ c.Writer.Header().Set("Access-Control-Allow-Origin", allowOrigin)
138
+ }
139
+
140
+ c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
141
+
142
+ headerString := ""
143
+ for i, h := range headers {
144
+ if i > 0 { headerString += ", " }
145
+ headerString += h
146
+ }
147
+ c.Writer.Header().Set("Access-Control-Allow-Headers", headerString)
148
+
149
+ methodString := ""
150
+ for i, m := range methods {
151
+ if i > 0 { methodString += ", " }
152
+ methodString += m
153
+ }
154
+ c.Writer.Header().Set("Access-Control-Allow-Methods", methodString)
155
+
156
+ if c.Request.Method == "OPTIONS" {
157
+ c.AbortWithStatus(204)
158
+ return
159
+ }
160
+
161
+ c.Next()
162
+ }
163
+ }
164
+ `;
165
+
166
+ await fs.writeFile(path.join(middlewareDir, 'cors_middleware.go'), corsMiddleware.replace('{{app_name}}', config.name));
167
+ console.log(chalk.gray(' Generated Advanced Security & CORS Middleware'));
168
+ };