go-gin-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.
@@ -0,0 +1,1626 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs").promises;
4
+ const path = require("path");
5
+ const createFile = require("../shares/createFile");
6
+ const createDir = require("../shares/createDir");
7
+ const { COLORS } = require("../constants/index");
8
+
9
+ /**
10
+ * ProjectSetup - Generates Go Clean Architecture project structure with Gin-Gonic
11
+ * Matching NestJS Clean Architecture pattern:
12
+ *
13
+ * project/
14
+ * ├── src/
15
+ * │ ├── domain/
16
+ * │ │ ├── dtos/
17
+ * │ │ ├── logger/
18
+ * │ │ ├── repositories/
19
+ * │ │ └── models/
20
+ * │ ├── infrastructure/
21
+ * │ │ ├── common/
22
+ * │ │ ├── config/
23
+ * │ │ ├── controllers/
24
+ * │ │ ├── entities/
25
+ * │ │ ├── logger/
26
+ * │ │ ├── repositories/
27
+ * │ │ └── usecases-proxy/
28
+ * │ ├── usecases/
29
+ * │ ├── shared/utils/
30
+ * │ └── main.go
31
+ * ├── .env
32
+ * ├── go.mod
33
+ * ├── Makefile
34
+ * └── Dockerfile
35
+ */
36
+ class ProjectSetup {
37
+ constructor(projectName, moduleName) {
38
+ this.projectName = projectName;
39
+ this.moduleName = moduleName;
40
+ this.rootDir = projectName;
41
+ this.srcDir = path.join(projectName, "src");
42
+ this.domainDir = path.join(this.srcDir, "domain");
43
+ this.infrastructureDir = path.join(this.srcDir, "infrastructure");
44
+ }
45
+
46
+ async execute() {
47
+ try {
48
+ console.log(
49
+ `${COLORS.YELLOW}Starting Go Gin-Gonic project setup...${COLORS.NC}`,
50
+ );
51
+
52
+ await createDir(this.rootDir);
53
+ await createDir(this.srcDir);
54
+
55
+ await this.setupDirectories();
56
+ await this.setupDomain();
57
+ await this.setupInfrastructure();
58
+ await this.setupUsecases();
59
+ await this.setupShared();
60
+ await this.setupMain();
61
+ await this.setupGoMod();
62
+ await this.setupEnvFiles();
63
+ await this.setupMakefile();
64
+ await this.setupDockerfile();
65
+
66
+ console.log(
67
+ `${COLORS.GREEN}Project setup completed successfully!${COLORS.NC}`,
68
+ );
69
+ } catch (error) {
70
+ console.error(
71
+ `${COLORS.RED}Error during setup:${COLORS.NC} ${error.message}`,
72
+ );
73
+ throw error;
74
+ }
75
+ }
76
+
77
+ async setupDirectories() {
78
+ // Domain directories (matching NestJS structure)
79
+ const domainDirs = ["dtos", "logger", "repositories", "models"];
80
+
81
+ // Infrastructure directories (matching NestJS structure)
82
+ const infrastructureDirs = [
83
+ "common/middleware",
84
+ "common/response",
85
+ "config/environment",
86
+ "config/database",
87
+ "controllers",
88
+ "entities",
89
+ "logger",
90
+ "repositories",
91
+ "usecases-proxy",
92
+ ];
93
+
94
+ // Other directories
95
+ const otherDirs = ["usecases", "shared/utils"];
96
+
97
+ // Create domain directories
98
+ for (const dir of domainDirs) {
99
+ await createDir(path.join(this.domainDir, dir));
100
+ }
101
+
102
+ // Create infrastructure directories
103
+ for (const dir of infrastructureDirs) {
104
+ await createDir(path.join(this.infrastructureDir, dir));
105
+ }
106
+
107
+ // Create other directories
108
+ for (const dir of otherDirs) {
109
+ await createDir(path.join(this.srcDir, dir));
110
+ }
111
+ }
112
+
113
+ async setupDomain() {
114
+ // Logger interface
115
+ await createFile(
116
+ path.join(this.domainDir, "logger/logger.go"),
117
+ this.getLoggerInterfaceContent(),
118
+ );
119
+
120
+ // Base model
121
+ await createFile(
122
+ path.join(this.domainDir, "models/base.go"),
123
+ this.getBaseModelContent(),
124
+ );
125
+
126
+ // Query model
127
+ await createFile(
128
+ path.join(this.domainDir, "models/query.go"),
129
+ this.getQueryModelContent(),
130
+ );
131
+
132
+ // Base repository interface
133
+ await createFile(
134
+ path.join(this.domainDir, "repositories/base.go"),
135
+ this.getBaseRepositoryInterfaceContent(),
136
+ );
137
+ }
138
+
139
+ async setupInfrastructure() {
140
+ // Common - Middleware
141
+ await createFile(
142
+ path.join(this.infrastructureDir, "common/middleware/logger.go"),
143
+ this.getLoggerMiddlewareContent(),
144
+ );
145
+
146
+ await createFile(
147
+ path.join(this.infrastructureDir, "common/middleware/recovery.go"),
148
+ this.getRecoveryMiddlewareContent(),
149
+ );
150
+
151
+ await createFile(
152
+ path.join(this.infrastructureDir, "common/middleware/cors.go"),
153
+ this.getCorsMiddlewareContent(),
154
+ );
155
+
156
+ // Common - Response
157
+ await createFile(
158
+ path.join(this.infrastructureDir, "common/response/response.go"),
159
+ this.getResponseContent(),
160
+ );
161
+
162
+ // Config - Environment
163
+ await createFile(
164
+ path.join(this.infrastructureDir, "config/environment/config.go"),
165
+ this.getEnvironmentConfigContent(),
166
+ );
167
+
168
+ // Config - Database
169
+ await createFile(
170
+ path.join(this.infrastructureDir, "config/database/postgres.go"),
171
+ this.getDatabaseConfigContent(),
172
+ );
173
+
174
+ // Logger service
175
+ await createFile(
176
+ path.join(this.infrastructureDir, "logger/logger.go"),
177
+ this.getLoggerServiceContent(),
178
+ );
179
+
180
+ // Repositories module
181
+ await createFile(
182
+ path.join(this.infrastructureDir, "repositories/repositories.go"),
183
+ this.getRepositoriesModuleContent(),
184
+ );
185
+
186
+ // Usecases proxy
187
+ await createFile(
188
+ path.join(this.infrastructureDir, "usecases-proxy/usecases_proxy.go"),
189
+ this.getUsecasesProxyContent(),
190
+ );
191
+
192
+ // Router
193
+ await createFile(
194
+ path.join(this.infrastructureDir, "controllers/router.go"),
195
+ this.getRouterContent(),
196
+ );
197
+ }
198
+
199
+ async setupUsecases() {
200
+ await createFile(
201
+ path.join(this.srcDir, "usecases/base.go"),
202
+ this.getBaseUsecaseContent(),
203
+ );
204
+ }
205
+
206
+ async setupShared() {
207
+ await createFile(
208
+ path.join(this.srcDir, "shared/utils/query.go"),
209
+ this.getQueryUtilContent(),
210
+ );
211
+
212
+ await createFile(
213
+ path.join(this.srcDir, "shared/utils/validator.go"),
214
+ this.getValidatorUtilContent(),
215
+ );
216
+ }
217
+
218
+ async setupMain() {
219
+ await createFile(path.join(this.srcDir, "main.go"), this.getMainContent());
220
+
221
+ await createFile(
222
+ path.join(this.srcDir, "app.go"),
223
+ this.getAppModuleContent(),
224
+ );
225
+ }
226
+
227
+ async setupGoMod() {
228
+ await createFile(path.join(this.rootDir, "go.mod"), this.getGoModContent());
229
+ }
230
+
231
+ async setupEnvFiles() {
232
+ await createFile(path.join(this.rootDir, ".env"), this.getEnvContent());
233
+
234
+ await createFile(
235
+ path.join(this.rootDir, ".env.example"),
236
+ this.getEnvContent(),
237
+ );
238
+
239
+ await createFile(
240
+ path.join(this.rootDir, ".gitignore"),
241
+ this.getGitignoreContent(),
242
+ );
243
+ }
244
+
245
+ async setupMakefile() {
246
+ await createFile(
247
+ path.join(this.rootDir, "Makefile"),
248
+ this.getMakefileContent(),
249
+ );
250
+
251
+ // Air config for hot reload
252
+ await createFile(
253
+ path.join(this.rootDir, ".air.toml"),
254
+ this.getAirConfigContent(),
255
+ );
256
+ }
257
+
258
+ async setupDockerfile() {
259
+ await createFile(
260
+ path.join(this.rootDir, "Dockerfile"),
261
+ this.getDockerfileContent(),
262
+ );
263
+
264
+ await createFile(
265
+ path.join(this.rootDir, "docker-compose.yml"),
266
+ this.getDockerComposeContent(),
267
+ );
268
+ }
269
+
270
+ // ============================================
271
+ // Content Generators - Domain
272
+ // ============================================
273
+
274
+ getLoggerInterfaceContent() {
275
+ return `package logger
276
+
277
+ // Logger defines the application logger interface
278
+ type Logger interface {
279
+ Debug(context string, message string)
280
+ Info(context string, message string)
281
+ Warn(context string, message string)
282
+ Error(context string, message string, err error)
283
+ Fatal(context string, message string, err error)
284
+ }
285
+ `;
286
+ }
287
+
288
+ getBaseModelContent() {
289
+ return `package models
290
+
291
+ import (
292
+ "time"
293
+
294
+ "github.com/google/uuid"
295
+ )
296
+
297
+ // BaseModel contains common fields for all models
298
+ type BaseModel struct {
299
+ ID uuid.UUID \`json:"id"\`
300
+ CreatedAt time.Time \`json:"created_at"\`
301
+ UpdatedAt time.Time \`json:"updated_at"\`
302
+ DeletedAt *time.Time \`json:"deleted_at,omitempty"\`
303
+ IsActive bool \`json:"is_active"\`
304
+ }
305
+
306
+ // NewBaseModel creates a new base model with default values
307
+ func NewBaseModel() BaseModel {
308
+ return BaseModel{
309
+ ID: uuid.New(),
310
+ CreatedAt: time.Now(),
311
+ UpdatedAt: time.Now(),
312
+ IsActive: true,
313
+ }
314
+ }
315
+ `;
316
+ }
317
+
318
+ getQueryModelContent() {
319
+ return `package models
320
+
321
+ // QueryParams represents common query parameters
322
+ type QueryParams struct {
323
+ DateFilter *DateFilter \`json:"date_filter,omitempty"\`
324
+ Search *Search \`json:"search,omitempty"\`
325
+ Sort int \`json:"sort,omitempty"\`
326
+ Paginate *Paginate \`json:"paginate,omitempty"\`
327
+ Condition []Condition \`json:"condition,omitempty"\`
328
+ InNumber []InNumber \`json:"in_number,omitempty"\`
329
+ InString []InString \`json:"in_string,omitempty"\`
330
+ Joins []string \`json:"joins,omitempty"\`
331
+ Select []string \`json:"select,omitempty"\`
332
+ }
333
+
334
+ // DateFilter represents date range filter
335
+ type DateFilter struct {
336
+ StartDate string \`json:"start_date"\`
337
+ EndDate string \`json:"end_date"\`
338
+ }
339
+
340
+ // Search represents search parameters
341
+ type Search struct {
342
+ SearchField []string \`json:"search_field"\`
343
+ Q string \`json:"q"\`
344
+ }
345
+
346
+ // Paginate represents pagination parameters
347
+ type Paginate struct {
348
+ Limit int \`json:"limit"\`
349
+ Page int \`json:"page"\`
350
+ }
351
+
352
+ // Condition represents a single condition
353
+ type Condition struct {
354
+ Field string \`json:"field"\`
355
+ Value string \`json:"value"\`
356
+ }
357
+
358
+ // InNumber represents IN clause for numbers
359
+ type InNumber struct {
360
+ Field string \`json:"field"\`
361
+ Value []int \`json:"value"\`
362
+ }
363
+
364
+ // InString represents IN clause for strings
365
+ type InString struct {
366
+ Field string \`json:"field"\`
367
+ Value []string \`json:"value"\`
368
+ }
369
+
370
+ // GetOffset calculates offset for pagination
371
+ func (p *Paginate) GetOffset() int {
372
+ if p == nil || p.Page < 1 {
373
+ return 0
374
+ }
375
+ return (p.Page - 1) * p.GetLimit()
376
+ }
377
+
378
+ // GetLimit returns limit with default value
379
+ func (p *Paginate) GetLimit() int {
380
+ if p == nil || p.Limit < 1 {
381
+ return 10
382
+ }
383
+ if p.Limit > 100 {
384
+ return 100
385
+ }
386
+ return p.Limit
387
+ }
388
+ `;
389
+ }
390
+
391
+ getBaseRepositoryInterfaceContent() {
392
+ return `package repositories
393
+
394
+ import (
395
+ "context"
396
+
397
+ "${this.moduleName}/src/domain/models"
398
+ "github.com/google/uuid"
399
+ )
400
+
401
+ // BaseRepository defines common repository operations
402
+ type BaseRepository[T any] interface {
403
+ Create(ctx context.Context, entity *T) error
404
+ Update(ctx context.Context, entity *T) error
405
+ Delete(ctx context.Context, id uuid.UUID) error
406
+ FindByID(ctx context.Context, id uuid.UUID) (*T, error)
407
+ FindAll(ctx context.Context, params *models.QueryParams) ([]T, int64, error)
408
+ }
409
+ `;
410
+ }
411
+
412
+ // ============================================
413
+ // Content Generators - Infrastructure
414
+ // ============================================
415
+
416
+ getLoggerMiddlewareContent() {
417
+ return `package middleware
418
+
419
+ import (
420
+ "time"
421
+
422
+ "${this.moduleName}/src/domain/logger"
423
+
424
+ "github.com/gin-gonic/gin"
425
+ )
426
+
427
+ // LoggerMiddleware logs HTTP requests
428
+ func LoggerMiddleware(log logger.Logger) gin.HandlerFunc {
429
+ return func(c *gin.Context) {
430
+ start := time.Now()
431
+ path := c.Request.URL.Path
432
+ raw := c.Request.URL.RawQuery
433
+
434
+ c.Next()
435
+
436
+ latency := time.Since(start)
437
+ statusCode := c.Writer.Status()
438
+ clientIP := c.ClientIP()
439
+ method := c.Request.Method
440
+
441
+ if raw != "" {
442
+ path = path + "?" + raw
443
+ }
444
+
445
+ log.Info("HTTP Request",
446
+ "method="+method+
447
+ " path="+path+
448
+ " status="+string(rune(statusCode))+
449
+ " latency="+latency.String()+
450
+ " ip="+clientIP,
451
+ )
452
+ }
453
+ }
454
+ `;
455
+ }
456
+
457
+ getRecoveryMiddlewareContent() {
458
+ return `package middleware
459
+
460
+ import (
461
+ "net/http"
462
+ "runtime/debug"
463
+
464
+ "${this.moduleName}/src/domain/logger"
465
+ "${this.moduleName}/src/infrastructure/common/response"
466
+
467
+ "github.com/gin-gonic/gin"
468
+ )
469
+
470
+ // RecoveryMiddleware recovers from panics and returns 500 error
471
+ func RecoveryMiddleware(log logger.Logger) gin.HandlerFunc {
472
+ return func(c *gin.Context) {
473
+ defer func() {
474
+ if r := recover(); r != nil {
475
+ log.Error("Panic recovered", "error", nil)
476
+ log.Error("Stack trace", string(debug.Stack()), nil)
477
+ response.Error(c, http.StatusInternalServerError, "Internal server error")
478
+ c.Abort()
479
+ }
480
+ }()
481
+ c.Next()
482
+ }
483
+ }
484
+ `;
485
+ }
486
+
487
+ getCorsMiddlewareContent() {
488
+ return `package middleware
489
+
490
+ import (
491
+ "github.com/gin-contrib/cors"
492
+ "github.com/gin-gonic/gin"
493
+ )
494
+
495
+ // CorsMiddleware returns CORS middleware configuration
496
+ func CorsMiddleware() gin.HandlerFunc {
497
+ return cors.New(cors.Config{
498
+ AllowOrigins: []string{"*"},
499
+ AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
500
+ AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"},
501
+ ExposeHeaders: []string{"Content-Length"},
502
+ AllowCredentials: true,
503
+ })
504
+ }
505
+ `;
506
+ }
507
+
508
+ getResponseContent() {
509
+ return `package response
510
+
511
+ import (
512
+ "github.com/gin-gonic/gin"
513
+ )
514
+
515
+ // Response represents a standard API response
516
+ type Response struct {
517
+ Success bool \`json:"success"\`
518
+ Message string \`json:"message,omitempty"\`
519
+ Data interface{} \`json:"data,omitempty"\`
520
+ Meta *Meta \`json:"meta,omitempty"\`
521
+ }
522
+
523
+ // Meta represents pagination metadata
524
+ type Meta struct {
525
+ Page int \`json:"page"\`
526
+ PageSize int \`json:"page_size"\`
527
+ Total int64 \`json:"total"\`
528
+ TotalPage int \`json:"total_page"\`
529
+ }
530
+
531
+ // Success sends a success response
532
+ func Success(c *gin.Context, statusCode int, data interface{}) {
533
+ c.JSON(statusCode, Response{
534
+ Success: true,
535
+ Data: data,
536
+ })
537
+ }
538
+
539
+ // SuccessWithMeta sends a success response with pagination meta
540
+ func SuccessWithMeta(c *gin.Context, statusCode int, data interface{}, meta *Meta) {
541
+ c.JSON(statusCode, Response{
542
+ Success: true,
543
+ Data: data,
544
+ Meta: meta,
545
+ })
546
+ }
547
+
548
+ // SuccessWithMessage sends a success response with message
549
+ func SuccessWithMessage(c *gin.Context, statusCode int, message string) {
550
+ c.JSON(statusCode, Response{
551
+ Success: true,
552
+ Message: message,
553
+ })
554
+ }
555
+
556
+ // Error sends an error response
557
+ func Error(c *gin.Context, statusCode int, message string) {
558
+ c.JSON(statusCode, Response{
559
+ Success: false,
560
+ Message: message,
561
+ })
562
+ }
563
+
564
+ // ValidationError sends a validation error response
565
+ func ValidationError(c *gin.Context, errors interface{}) {
566
+ c.JSON(400, Response{
567
+ Success: false,
568
+ Message: "Validation failed",
569
+ Data: errors,
570
+ })
571
+ }
572
+ `;
573
+ }
574
+
575
+ getEnvironmentConfigContent() {
576
+ return `package environment
577
+
578
+ import (
579
+ "fmt"
580
+ "os"
581
+ "strconv"
582
+
583
+ "github.com/joho/godotenv"
584
+ )
585
+
586
+ // Config holds all configuration
587
+ type Config struct {
588
+ App AppConfig
589
+ Server ServerConfig
590
+ Database DatabaseConfig
591
+ }
592
+
593
+ // AppConfig holds application configuration
594
+ type AppConfig struct {
595
+ Name string
596
+ Environment string
597
+ Debug bool
598
+ }
599
+
600
+ // ServerConfig holds server configuration
601
+ type ServerConfig struct {
602
+ Host string
603
+ Port int
604
+ }
605
+
606
+ // DatabaseConfig holds database configuration
607
+ type DatabaseConfig struct {
608
+ Host string
609
+ Port int
610
+ User string
611
+ Password string
612
+ DBName string
613
+ SSLMode string
614
+ TimeZone string
615
+ }
616
+
617
+ // Load loads configuration from environment
618
+ func Load() (*Config, error) {
619
+ env := os.Getenv("APP_ENV")
620
+ if env == "" {
621
+ env = "development"
622
+ }
623
+
624
+ envFile := ".env"
625
+ if env != "production" {
626
+ envFile = fmt.Sprintf(".env.%s", env)
627
+ if _, err := os.Stat(envFile); os.IsNotExist(err) {
628
+ envFile = ".env"
629
+ }
630
+ }
631
+
632
+ if err := godotenv.Load(envFile); err != nil {
633
+ if env != "production" {
634
+ return nil, fmt.Errorf("error loading %s file: %w", envFile, err)
635
+ }
636
+ }
637
+
638
+ port, _ := strconv.Atoi(getEnv("SERVER_PORT", "8080"))
639
+ dbPort, _ := strconv.Atoi(getEnv("DB_PORT", "5432"))
640
+ debug, _ := strconv.ParseBool(getEnv("APP_DEBUG", "false"))
641
+
642
+ return &Config{
643
+ App: AppConfig{
644
+ Name: getEnv("APP_NAME", "${this.projectName}"),
645
+ Environment: env,
646
+ Debug: debug,
647
+ },
648
+ Server: ServerConfig{
649
+ Host: getEnv("SERVER_HOST", "0.0.0.0"),
650
+ Port: port,
651
+ },
652
+ Database: DatabaseConfig{
653
+ Host: getEnv("DB_HOST", "localhost"),
654
+ Port: dbPort,
655
+ User: getEnv("DB_USER", "postgres"),
656
+ Password: getEnv("DB_PASSWORD", ""),
657
+ DBName: getEnv("DB_NAME", "${this.projectName.replace(/-/g, "_")}"),
658
+ SSLMode: getEnv("DB_SSL_MODE", "disable"),
659
+ TimeZone: getEnv("DB_TIMEZONE", "UTC"),
660
+ },
661
+ }, nil
662
+ }
663
+
664
+ func getEnv(key, defaultValue string) string {
665
+ if value := os.Getenv(key); value != "" {
666
+ return value
667
+ }
668
+ return defaultValue
669
+ }
670
+ `;
671
+ }
672
+
673
+ getDatabaseConfigContent() {
674
+ return `package database
675
+
676
+ import (
677
+ "fmt"
678
+ "time"
679
+
680
+ "${this.moduleName}/src/infrastructure/config/environment"
681
+
682
+ "gorm.io/driver/postgres"
683
+ "gorm.io/gorm"
684
+ "gorm.io/gorm/logger"
685
+ )
686
+
687
+ // NewPostgresConnection creates a new PostgreSQL connection
688
+ func NewPostgresConnection(cfg environment.DatabaseConfig) (*gorm.DB, error) {
689
+ dsn := fmt.Sprintf(
690
+ "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s TimeZone=%s",
691
+ cfg.Host,
692
+ cfg.Port,
693
+ cfg.User,
694
+ cfg.Password,
695
+ cfg.DBName,
696
+ cfg.SSLMode,
697
+ cfg.TimeZone,
698
+ )
699
+
700
+ db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
701
+ Logger: logger.Default.LogMode(logger.Info),
702
+ })
703
+ if err != nil {
704
+ return nil, fmt.Errorf("failed to connect to database: %w", err)
705
+ }
706
+
707
+ sqlDB, err := db.DB()
708
+ if err != nil {
709
+ return nil, fmt.Errorf("failed to get database instance: %w", err)
710
+ }
711
+
712
+ // Connection pool settings
713
+ sqlDB.SetMaxIdleConns(10)
714
+ sqlDB.SetMaxOpenConns(100)
715
+ sqlDB.SetConnMaxLifetime(time.Hour)
716
+
717
+ return db, nil
718
+ }
719
+
720
+ // AutoMigrate runs auto migration for given models
721
+ func AutoMigrate(db *gorm.DB, models ...interface{}) error {
722
+ return db.AutoMigrate(models...)
723
+ }
724
+ `;
725
+ }
726
+
727
+ getLoggerServiceContent() {
728
+ return `package logger
729
+
730
+ import (
731
+ "os"
732
+
733
+ domainLogger "${this.moduleName}/src/domain/logger"
734
+
735
+ "go.uber.org/zap"
736
+ "go.uber.org/zap/zapcore"
737
+ )
738
+
739
+ // ZapLogger implements domain logger interface
740
+ type ZapLogger struct {
741
+ *zap.SugaredLogger
742
+ }
743
+
744
+ // NewLogger creates a new logger instance
745
+ func NewLogger(env string) *ZapLogger {
746
+ var config zap.Config
747
+
748
+ if env == "production" {
749
+ config = zap.NewProductionConfig()
750
+ config.EncoderConfig.TimeKey = "timestamp"
751
+ config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
752
+ } else {
753
+ config = zap.NewDevelopmentConfig()
754
+ config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
755
+ }
756
+
757
+ config.OutputPaths = []string{"stdout"}
758
+ config.ErrorOutputPaths = []string{"stderr"}
759
+
760
+ logger, err := config.Build()
761
+ if err != nil {
762
+ panic(err)
763
+ }
764
+
765
+ return &ZapLogger{logger.Sugar()}
766
+ }
767
+
768
+ // Ensure ZapLogger implements Logger interface
769
+ var _ domainLogger.Logger = (*ZapLogger)(nil)
770
+
771
+ func (l *ZapLogger) Debug(context string, message string) {
772
+ l.SugaredLogger.Debugw(message, "context", context)
773
+ }
774
+
775
+ func (l *ZapLogger) Info(context string, message string) {
776
+ l.SugaredLogger.Infow(message, "context", context)
777
+ }
778
+
779
+ func (l *ZapLogger) Warn(context string, message string) {
780
+ l.SugaredLogger.Warnw(message, "context", context)
781
+ }
782
+
783
+ func (l *ZapLogger) Error(context string, message string, err error) {
784
+ l.SugaredLogger.Errorw(message, "context", context, "error", err)
785
+ }
786
+
787
+ func (l *ZapLogger) Fatal(context string, message string, err error) {
788
+ l.SugaredLogger.Fatalw(message, "context", context, "error", err)
789
+ os.Exit(1)
790
+ }
791
+
792
+ func (l *ZapLogger) Sync() {
793
+ _ = l.SugaredLogger.Sync()
794
+ }
795
+ `;
796
+ }
797
+
798
+ getRepositoriesModuleContent() {
799
+ return `package repositories
800
+
801
+ import (
802
+ "gorm.io/gorm"
803
+ )
804
+
805
+ // RepositoriesModule holds all repositories
806
+ type RepositoriesModule struct {
807
+ db *gorm.DB
808
+ // Add your repositories here
809
+ }
810
+
811
+ // NewRepositoriesModule creates a new repositories module
812
+ func NewRepositoriesModule(db *gorm.DB) *RepositoriesModule {
813
+ return &RepositoriesModule{
814
+ db: db,
815
+ // Initialize your repositories here
816
+ }
817
+ }
818
+ `;
819
+ }
820
+
821
+ getUsecasesProxyContent() {
822
+ return `package usecasesproxy
823
+
824
+ import (
825
+ "${this.moduleName}/src/domain/logger"
826
+ "${this.moduleName}/src/infrastructure/repositories"
827
+ )
828
+
829
+ // UsecasesProxy holds all usecases
830
+ type UsecasesProxy struct {
831
+ Logger logger.Logger
832
+ Repositories *repositories.RepositoriesModule
833
+ // Add your usecases here
834
+ // UserUsecase *usecases.UserUsecase
835
+ }
836
+
837
+ // NewUsecasesProxy creates a new usecases proxy
838
+ func NewUsecasesProxy(logger logger.Logger, repos *repositories.RepositoriesModule) *UsecasesProxy {
839
+ return &UsecasesProxy{
840
+ Logger: logger,
841
+ Repositories: repos,
842
+ // Initialize your usecases here
843
+ // UserUsecase: usecases.NewUserUsecase(repos.UserRepository, logger),
844
+ }
845
+ }
846
+ `;
847
+ }
848
+
849
+ getRouterContent() {
850
+ return `package controllers
851
+
852
+ import (
853
+ "${this.moduleName}/src/domain/logger"
854
+ "${this.moduleName}/src/infrastructure/common/middleware"
855
+ usecasesproxy "${this.moduleName}/src/infrastructure/usecases-proxy"
856
+
857
+ "github.com/gin-gonic/gin"
858
+ )
859
+
860
+ // Router holds the gin engine and dependencies
861
+ type Router struct {
862
+ engine *gin.Engine
863
+ logger logger.Logger
864
+ usecasesProxy *usecasesproxy.UsecasesProxy
865
+ }
866
+
867
+ // NewRouter creates a new router instance
868
+ func NewRouter(logger logger.Logger, usecasesProxy *usecasesproxy.UsecasesProxy, debug bool) *Router {
869
+ if !debug {
870
+ gin.SetMode(gin.ReleaseMode)
871
+ }
872
+
873
+ engine := gin.New()
874
+
875
+ // Apply global middleware
876
+ engine.Use(middleware.CorsMiddleware())
877
+ engine.Use(middleware.RecoveryMiddleware(logger))
878
+ engine.Use(middleware.LoggerMiddleware(logger))
879
+
880
+ return &Router{
881
+ engine: engine,
882
+ logger: logger,
883
+ usecasesProxy: usecasesProxy,
884
+ }
885
+ }
886
+
887
+ // SetupRoutes configures all routes
888
+ func (r *Router) SetupRoutes() {
889
+ // Health check
890
+ r.engine.GET("/health", func(c *gin.Context) {
891
+ c.JSON(200, gin.H{"status": "ok"})
892
+ })
893
+
894
+ // API v1 group
895
+ v1 := r.engine.Group("/api/v1")
896
+ {
897
+ // Register your routes here
898
+ _ = v1 // Remove this line when adding routes
899
+ }
900
+ }
901
+
902
+ // GetEngine returns the gin engine
903
+ func (r *Router) GetEngine() *gin.Engine {
904
+ return r.engine
905
+ }
906
+ `;
907
+ }
908
+
909
+ // ============================================
910
+ // Content Generators - Usecases
911
+ // ============================================
912
+
913
+ getBaseUsecaseContent() {
914
+ return `package usecases
915
+
916
+ import (
917
+ "context"
918
+
919
+ "${this.moduleName}/src/domain/models"
920
+ "github.com/google/uuid"
921
+ )
922
+
923
+ // BaseUsecase defines common usecase operations
924
+ type BaseUsecase[T any, CreateReq any, UpdateReq any] interface {
925
+ Create(ctx context.Context, req *CreateReq) (*T, error)
926
+ Update(ctx context.Context, id uuid.UUID, req *UpdateReq) (*T, error)
927
+ Delete(ctx context.Context, id uuid.UUID) error
928
+ GetByID(ctx context.Context, id uuid.UUID) (*T, error)
929
+ GetAll(ctx context.Context, params *models.QueryParams) ([]T, int64, error)
930
+ }
931
+ `;
932
+ }
933
+
934
+ // ============================================
935
+ // Content Generators - Shared Utils
936
+ // ============================================
937
+
938
+ getQueryUtilContent() {
939
+ return `package utils
940
+
941
+ import (
942
+ "fmt"
943
+ "time"
944
+
945
+ "${this.moduleName}/src/domain/models"
946
+
947
+ "gorm.io/gorm"
948
+ )
949
+
950
+ // QueryResult represents query result with pagination
951
+ type QueryResult[T any] struct {
952
+ Data []T
953
+ Total int64
954
+ }
955
+
956
+ // BuildQuery builds a GORM query based on provided parameters
957
+ func BuildQuery[T any](db *gorm.DB, params *models.QueryParams) *gorm.DB {
958
+ query := db.Model(new(T)).Where("is_active = ?", true)
959
+
960
+ if params == nil {
961
+ return query.Order("created_at DESC")
962
+ }
963
+
964
+ // Handle date filtering
965
+ if params.DateFilter != nil && params.DateFilter.StartDate != "" && params.DateFilter.EndDate != "" {
966
+ startDate, err1 := time.Parse("2006-01-02", params.DateFilter.StartDate)
967
+ endDate, err2 := time.Parse("2006-01-02", params.DateFilter.EndDate)
968
+ if err1 == nil && err2 == nil {
969
+ startDate = time.Date(startDate.Year(), startDate.Month(), startDate.Day(), 0, 0, 0, 0, time.UTC)
970
+ endDate = time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 23, 59, 59, 999999999, time.UTC)
971
+ query = query.Where("created_at BETWEEN ? AND ?", startDate, endDate)
972
+ }
973
+ }
974
+
975
+ // Handle search
976
+ if params.Search != nil && len(params.Search.SearchField) > 0 && params.Search.Q != "" {
977
+ var conditions []string
978
+ for _, field := range params.Search.SearchField {
979
+ conditions = append(conditions, fmt.Sprintf("%s ILIKE ?", field))
980
+ }
981
+ searchTerm := "%" + params.Search.Q + "%"
982
+ args := make([]interface{}, len(conditions))
983
+ for i := range args {
984
+ args[i] = searchTerm
985
+ }
986
+ query = query.Where("("+joinOr(conditions)+")", args...)
987
+ }
988
+
989
+ // Handle conditions
990
+ if len(params.Condition) > 0 {
991
+ for _, cond := range params.Condition {
992
+ query = query.Where(fmt.Sprintf("%s = ?", cond.Field), cond.Value)
993
+ }
994
+ }
995
+
996
+ // Handle IN clauses for numbers
997
+ if len(params.InNumber) > 0 {
998
+ for _, in := range params.InNumber {
999
+ query = query.Where(fmt.Sprintf("%s IN ?", in.Field), in.Value)
1000
+ }
1001
+ }
1002
+
1003
+ // Handle IN clauses for strings
1004
+ if len(params.InString) > 0 {
1005
+ for _, in := range params.InString {
1006
+ query = query.Where(fmt.Sprintf("%s IN ?", in.Field), in.Value)
1007
+ }
1008
+ }
1009
+
1010
+ // Handle sorting
1011
+ if params.Sort != 0 {
1012
+ if params.Sort == 1 {
1013
+ query = query.Order("created_at ASC")
1014
+ } else {
1015
+ query = query.Order("created_at DESC")
1016
+ }
1017
+ } else {
1018
+ query = query.Order("created_at DESC")
1019
+ }
1020
+
1021
+ return query
1022
+ }
1023
+
1024
+ // ExecuteQuery executes query with pagination
1025
+ func ExecuteQuery[T any](db *gorm.DB, params *models.QueryParams) (*QueryResult[T], error) {
1026
+ var entities []T
1027
+ var total int64
1028
+
1029
+ query := BuildQuery[T](db, params)
1030
+
1031
+ // Count total
1032
+ if err := query.Count(&total).Error; err != nil {
1033
+ return nil, err
1034
+ }
1035
+
1036
+ // Apply pagination
1037
+ if params != nil && params.Paginate != nil {
1038
+ offset := params.Paginate.GetOffset()
1039
+ limit := params.Paginate.GetLimit()
1040
+ query = query.Offset(offset).Limit(limit)
1041
+ }
1042
+
1043
+ if err := query.Find(&entities).Error; err != nil {
1044
+ return nil, err
1045
+ }
1046
+
1047
+ return &QueryResult[T]{
1048
+ Data: entities,
1049
+ Total: total,
1050
+ }, nil
1051
+ }
1052
+
1053
+ func joinOr(conditions []string) string {
1054
+ result := ""
1055
+ for i, cond := range conditions {
1056
+ if i > 0 {
1057
+ result += " OR "
1058
+ }
1059
+ result += cond
1060
+ }
1061
+ return result
1062
+ }
1063
+ `;
1064
+ }
1065
+
1066
+ getValidatorUtilContent() {
1067
+ return `package utils
1068
+
1069
+ import (
1070
+ "errors"
1071
+ "fmt"
1072
+
1073
+ "github.com/go-playground/validator/v10"
1074
+ )
1075
+
1076
+ var validate *validator.Validate
1077
+
1078
+ func init() {
1079
+ validate = validator.New()
1080
+ }
1081
+
1082
+ // ValidationError represents a validation error
1083
+ type ValidationError struct {
1084
+ Field string \`json:"field"\`
1085
+ Message string \`json:"message"\`
1086
+ }
1087
+
1088
+ // ValidateStruct validates a struct using go-playground/validator
1089
+ func ValidateStruct(s interface{}) []ValidationError {
1090
+ var validationErrors []ValidationError
1091
+
1092
+ err := validate.Struct(s)
1093
+ if err != nil {
1094
+ for _, err := range err.(validator.ValidationErrors) {
1095
+ validationErrors = append(validationErrors, ValidationError{
1096
+ Field: err.Field(),
1097
+ Message: getErrorMessage(err),
1098
+ })
1099
+ }
1100
+ }
1101
+
1102
+ return validationErrors
1103
+ }
1104
+
1105
+ func getErrorMessage(err validator.FieldError) string {
1106
+ switch err.Tag() {
1107
+ case "required":
1108
+ return fmt.Sprintf("%s is required", err.Field())
1109
+ case "email":
1110
+ return fmt.Sprintf("%s must be a valid email", err.Field())
1111
+ case "min":
1112
+ return fmt.Sprintf("%s must be at least %s characters", err.Field(), err.Param())
1113
+ case "max":
1114
+ return fmt.Sprintf("%s must be at most %s characters", err.Field(), err.Param())
1115
+ case "uuid":
1116
+ return fmt.Sprintf("%s must be a valid UUID", err.Field())
1117
+ default:
1118
+ return fmt.Sprintf("%s is invalid", err.Field())
1119
+ }
1120
+ }
1121
+
1122
+ // AppError represents an application error
1123
+ type AppError struct {
1124
+ Code int
1125
+ Message string
1126
+ }
1127
+
1128
+ func (e *AppError) Error() string {
1129
+ return e.Message
1130
+ }
1131
+
1132
+ // NewAppError creates a new application error
1133
+ func NewAppError(code int, message string) *AppError {
1134
+ return &AppError{Code: code, Message: message}
1135
+ }
1136
+
1137
+ // NotFoundError returns a not found error
1138
+ func NotFoundError(message string) error {
1139
+ return NewAppError(404, message)
1140
+ }
1141
+
1142
+ // BadRequestError returns a bad request error
1143
+ func BadRequestError(message string) error {
1144
+ return NewAppError(400, message)
1145
+ }
1146
+
1147
+ // InternalError returns an internal server error
1148
+ func InternalError(message string) error {
1149
+ return NewAppError(500, message)
1150
+ }
1151
+
1152
+ // ConflictError returns a conflict error
1153
+ func ConflictError(message string) error {
1154
+ return NewAppError(409, message)
1155
+ }
1156
+
1157
+ // IsAppError checks if error is an AppError and returns it
1158
+ func IsAppError(err error) (*AppError, bool) {
1159
+ var appErr *AppError
1160
+ if errors.As(err, &appErr) {
1161
+ return appErr, true
1162
+ }
1163
+ return nil, false
1164
+ }
1165
+ `;
1166
+ }
1167
+
1168
+ // ============================================
1169
+ // Content Generators - Main & App
1170
+ // ============================================
1171
+
1172
+ getMainContent() {
1173
+ return `package main
1174
+
1175
+ import (
1176
+ "log"
1177
+
1178
+ "${this.moduleName}/src/infrastructure/config/database"
1179
+ "${this.moduleName}/src/infrastructure/config/environment"
1180
+ infraLogger "${this.moduleName}/src/infrastructure/logger"
1181
+ )
1182
+
1183
+ func main() {
1184
+ // Load configuration
1185
+ cfg, err := environment.Load()
1186
+ if err != nil {
1187
+ log.Fatalf("Failed to load config: %v", err)
1188
+ }
1189
+
1190
+ // Initialize logger
1191
+ logger := infraLogger.NewLogger(cfg.App.Environment)
1192
+ defer logger.Sync()
1193
+
1194
+ // Initialize database
1195
+ db, err := database.NewPostgresConnection(cfg.Database)
1196
+ if err != nil {
1197
+ logger.Fatal("main", "Failed to connect to database", err)
1198
+ }
1199
+
1200
+ // Initialize and run application
1201
+ app := NewApp(cfg, db, logger)
1202
+
1203
+ if err := app.Run(); err != nil {
1204
+ logger.Fatal("main", "Failed to start server", err)
1205
+ }
1206
+ }
1207
+ `;
1208
+ }
1209
+
1210
+ getAppModuleContent() {
1211
+ return `package main
1212
+
1213
+ import (
1214
+ "context"
1215
+ "fmt"
1216
+ "net/http"
1217
+ "os"
1218
+ "os/signal"
1219
+ "syscall"
1220
+ "time"
1221
+
1222
+ "${this.moduleName}/src/infrastructure/config/environment"
1223
+ "${this.moduleName}/src/infrastructure/controllers"
1224
+ infraLogger "${this.moduleName}/src/infrastructure/logger"
1225
+ "${this.moduleName}/src/infrastructure/repositories"
1226
+ usecasesproxy "${this.moduleName}/src/infrastructure/usecases-proxy"
1227
+
1228
+ "gorm.io/gorm"
1229
+ )
1230
+
1231
+ // App represents the application
1232
+ type App struct {
1233
+ cfg *environment.Config
1234
+ db *gorm.DB
1235
+ logger *infraLogger.ZapLogger
1236
+ router *controllers.Router
1237
+ server *http.Server
1238
+ }
1239
+
1240
+ // NewApp creates a new application instance
1241
+ func NewApp(cfg *environment.Config, db *gorm.DB, logger *infraLogger.ZapLogger) *App {
1242
+ // Initialize modules
1243
+ reposModule := repositories.NewRepositoriesModule(db)
1244
+ usecasesProxy := usecasesproxy.NewUsecasesProxy(logger, reposModule)
1245
+
1246
+ // Create router
1247
+ router := controllers.NewRouter(logger, usecasesProxy, cfg.App.Debug)
1248
+ router.SetupRoutes()
1249
+
1250
+ // Create HTTP server
1251
+ server := &http.Server{
1252
+ Addr: fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port),
1253
+ Handler: router.GetEngine(),
1254
+ ReadTimeout: 10 * time.Second,
1255
+ WriteTimeout: 10 * time.Second,
1256
+ }
1257
+
1258
+ return &App{
1259
+ cfg: cfg,
1260
+ db: db,
1261
+ logger: logger,
1262
+ router: router,
1263
+ server: server,
1264
+ }
1265
+ }
1266
+
1267
+ // Run starts the application
1268
+ func (a *App) Run() error {
1269
+ // Start HTTP server in goroutine
1270
+ errChan := make(chan error, 1)
1271
+ go func() {
1272
+ a.logger.Info("HTTP Server", fmt.Sprintf("Starting server on %s", a.server.Addr))
1273
+ if err := a.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
1274
+ errChan <- err
1275
+ }
1276
+ }()
1277
+
1278
+ // Wait for interrupt signal
1279
+ quit := make(chan os.Signal, 1)
1280
+ signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
1281
+
1282
+ select {
1283
+ case err := <-errChan:
1284
+ return err
1285
+ case <-quit:
1286
+ a.logger.Info("app", "Shutting down server...")
1287
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
1288
+ defer cancel()
1289
+ if err := a.server.Shutdown(ctx); err != nil {
1290
+ a.logger.Error("app", "Server forced to shutdown", err)
1291
+ return err
1292
+ }
1293
+ a.logger.Info("app", "Server exited properly")
1294
+ }
1295
+
1296
+ return nil
1297
+ }
1298
+ `;
1299
+ }
1300
+
1301
+ // ============================================
1302
+ // Content Generators - Config Files
1303
+ // ============================================
1304
+
1305
+ getGoModContent() {
1306
+ return `module ${this.moduleName}
1307
+
1308
+ go 1.21
1309
+
1310
+ require (
1311
+ github.com/gin-contrib/cors v1.5.0
1312
+ github.com/gin-gonic/gin v1.9.1
1313
+ github.com/go-playground/validator/v10 v10.16.0
1314
+ github.com/google/uuid v1.5.0
1315
+ github.com/joho/godotenv v1.5.1
1316
+ go.uber.org/zap v1.26.0
1317
+ gorm.io/driver/postgres v1.5.4
1318
+ gorm.io/gorm v1.25.5
1319
+ )
1320
+ `;
1321
+ }
1322
+
1323
+ getEnvContent() {
1324
+ return `# Application
1325
+ APP_NAME=${this.projectName}
1326
+ APP_ENV=development
1327
+ APP_DEBUG=true
1328
+
1329
+ # Server
1330
+ SERVER_HOST=0.0.0.0
1331
+ SERVER_PORT=8080
1332
+
1333
+ # Database
1334
+ DB_HOST=localhost
1335
+ DB_PORT=5432
1336
+ DB_USER=postgres
1337
+ DB_PASSWORD=postgres
1338
+ DB_NAME=${this.projectName.replace(/-/g, "_")}
1339
+ DB_SSL_MODE=disable
1340
+ DB_TIMEZONE=UTC
1341
+ `;
1342
+ }
1343
+
1344
+ getGitignoreContent() {
1345
+ return `# Binaries
1346
+ *.exe
1347
+ *.exe~
1348
+ *.dll
1349
+ *.so
1350
+ *.dylib
1351
+ bin/
1352
+ build/
1353
+
1354
+ # Test binary
1355
+ *.test
1356
+
1357
+ # Output of the go coverage tool
1358
+ *.out
1359
+ coverage.html
1360
+
1361
+ # Dependency directories
1362
+ vendor/
1363
+
1364
+ # IDE
1365
+ .idea/
1366
+ .vscode/
1367
+ *.swp
1368
+ *.swo
1369
+
1370
+ # Environment files
1371
+ .env
1372
+ .env.local
1373
+ .env.*.local
1374
+
1375
+ # OS files
1376
+ .DS_Store
1377
+ Thumbs.db
1378
+
1379
+ # Logs
1380
+ *.log
1381
+ logs/
1382
+
1383
+ # Temporary files
1384
+ tmp/
1385
+ temp/
1386
+
1387
+ # Generated proto files
1388
+ *.pb.go
1389
+ `;
1390
+ }
1391
+
1392
+ getMakefileContent() {
1393
+ return `# Variables
1394
+ APP_NAME := ${this.projectName}
1395
+ BUILD_DIR := build
1396
+ MAIN_PATH := src/main.go src/app.go
1397
+
1398
+ # Go commands
1399
+ GOCMD := go
1400
+ GOBUILD := $(GOCMD) build
1401
+ GORUN := $(GOCMD) run
1402
+ GOTEST := $(GOCMD) test
1403
+ GOMOD := $(GOCMD) mod
1404
+ GOFMT := gofmt
1405
+
1406
+ .PHONY: all build run test clean fmt lint tidy help
1407
+
1408
+ all: build
1409
+
1410
+ ## build: Build the application
1411
+ build:
1412
+ @echo "Building..."
1413
+ @mkdir -p $(BUILD_DIR)
1414
+ $(GOBUILD) -o $(BUILD_DIR)/$(APP_NAME) $(MAIN_PATH)
1415
+
1416
+ ## run: Run the application
1417
+ run:
1418
+ @echo "Running..."
1419
+ $(GORUN) $(MAIN_PATH)
1420
+
1421
+ ## dev: Run with hot reload (requires air: go install github.com/air-verse/air@latest)
1422
+ dev:
1423
+ @echo "Starting development server with hot reload..."
1424
+ @if command -v air > /dev/null 2>&1; then \
1425
+ air; \
1426
+ elif [ -f "$$(go env GOPATH)/bin/air" ]; then \
1427
+ $$(go env GOPATH)/bin/air; \
1428
+ else \
1429
+ echo "Air not found. Installing..."; \
1430
+ go install github.com/air-verse/air@latest; \
1431
+ $$(go env GOPATH)/bin/air; \
1432
+ fi
1433
+
1434
+ ## test: Run tests
1435
+ test:
1436
+ @echo "Testing..."
1437
+ $(GOTEST) -v ./...
1438
+
1439
+ ## test-coverage: Run tests with coverage
1440
+ test-coverage:
1441
+ @echo "Testing with coverage..."
1442
+ $(GOTEST) -coverprofile=coverage.out ./...
1443
+ $(GOCMD) tool cover -html=coverage.out -o coverage.html
1444
+
1445
+ ## clean: Clean build files
1446
+ clean:
1447
+ @echo "Cleaning..."
1448
+ @rm -rf $(BUILD_DIR)
1449
+ @rm -f coverage.out coverage.html
1450
+
1451
+ ## fmt: Format code
1452
+ fmt:
1453
+ @echo "Formatting..."
1454
+ $(GOFMT) -s -w .
1455
+
1456
+ ## lint: Run linter
1457
+ lint:
1458
+ @echo "Linting..."
1459
+ golangci-lint run
1460
+
1461
+ ## tidy: Tidy dependencies
1462
+ tidy:
1463
+ @echo "Tidying dependencies..."
1464
+ $(GOMOD) tidy
1465
+
1466
+ ## docker-build: Build Docker image
1467
+ docker-build:
1468
+ @echo "Building Docker image..."
1469
+ docker build -t $(APP_NAME):latest .
1470
+
1471
+ ## docker-run: Run Docker container
1472
+ docker-run:
1473
+ @echo "Running Docker container..."
1474
+ docker-compose up -d
1475
+
1476
+ ## docker-stop: Stop Docker container
1477
+ docker-stop:
1478
+ @echo "Stopping Docker container..."
1479
+ docker-compose down
1480
+
1481
+ ## help: Show this help
1482
+ help:
1483
+ @echo "Usage: make [target]"
1484
+ @echo ""
1485
+ @echo "Targets:"
1486
+ @sed -n 's/^##//p' \${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /'
1487
+ `;
1488
+ }
1489
+
1490
+ getAirConfigContent() {
1491
+ return `# Air configuration for hot reload
1492
+ # Documentation: https://github.com/air-verse/air
1493
+
1494
+ root = "."
1495
+ testdata_dir = "testdata"
1496
+ tmp_dir = "tmp"
1497
+
1498
+ [build]
1499
+ args_bin = []
1500
+ bin = "./tmp/main"
1501
+ cmd = "go build -o ./tmp/main ./src/main.go ./src/app.go"
1502
+ delay = 1000
1503
+ exclude_dir = ["assets", "tmp", "vendor", "testdata", "node_modules"]
1504
+ exclude_file = []
1505
+ exclude_regex = ["_test.go", ".*_templ.go"]
1506
+ exclude_unchanged = false
1507
+ follow_symlink = false
1508
+ full_bin = ""
1509
+ include_dir = []
1510
+ include_ext = ["go", "tpl", "tmpl", "html"]
1511
+ include_file = []
1512
+ kill_delay = "2s"
1513
+ log = "build-errors.log"
1514
+ poll = false
1515
+ poll_interval = 0
1516
+ post_cmd = []
1517
+ pre_cmd = []
1518
+ rerun = false
1519
+ rerun_delay = 500
1520
+ send_interrupt = false
1521
+ stop_on_error = false
1522
+
1523
+ [color]
1524
+ app = ""
1525
+ build = "yellow"
1526
+ main = "magenta"
1527
+ runner = "green"
1528
+ watcher = "cyan"
1529
+
1530
+ [log]
1531
+ main_only = false
1532
+ time = false
1533
+
1534
+ [misc]
1535
+ clean_on_exit = false
1536
+
1537
+ [screen]
1538
+ clear_on_rebuild = false
1539
+ keep_scroll = true
1540
+ `;
1541
+ }
1542
+
1543
+ getDockerfileContent() {
1544
+ return `# Build stage
1545
+ FROM golang:1.21-alpine AS builder
1546
+
1547
+ WORKDIR /app
1548
+
1549
+ # Install dependencies
1550
+ RUN apk add --no-cache git
1551
+
1552
+ # Copy go mod files
1553
+ COPY go.mod go.sum ./
1554
+ RUN go mod download
1555
+
1556
+ # Copy source code
1557
+ COPY . .
1558
+
1559
+ # Build the application
1560
+ RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main src/main.go src/app.go
1561
+
1562
+ # Final stage
1563
+ FROM alpine:latest
1564
+
1565
+ WORKDIR /app
1566
+
1567
+ # Install ca-certificates for HTTPS
1568
+ RUN apk --no-cache add ca-certificates tzdata
1569
+
1570
+ # Copy binary from builder
1571
+ COPY --from=builder /app/main .
1572
+ COPY --from=builder /app/.env.example .env
1573
+
1574
+ # Expose HTTP port
1575
+ EXPOSE 8080
1576
+
1577
+ # Run the application
1578
+ CMD ["./main"]
1579
+ `;
1580
+ }
1581
+
1582
+ getDockerComposeContent() {
1583
+ return `version: '3.8'
1584
+
1585
+ services:
1586
+ app:
1587
+ build: .
1588
+ ports:
1589
+ - "8080:8080"
1590
+ environment:
1591
+ - APP_ENV=production
1592
+ - SERVER_PORT=8080
1593
+ - DB_HOST=postgres
1594
+ - DB_PORT=5432
1595
+ - DB_USER=postgres
1596
+ - DB_PASSWORD=postgres
1597
+ - DB_NAME=${this.projectName.replace(/-/g, "_")}
1598
+ depends_on:
1599
+ - postgres
1600
+ networks:
1601
+ - app-network
1602
+
1603
+ postgres:
1604
+ image: postgres:15-alpine
1605
+ ports:
1606
+ - "5432:5432"
1607
+ environment:
1608
+ - POSTGRES_USER=postgres
1609
+ - POSTGRES_PASSWORD=postgres
1610
+ - POSTGRES_DB=${this.projectName.replace(/-/g, "_")}
1611
+ volumes:
1612
+ - postgres-data:/var/lib/postgresql/data
1613
+ networks:
1614
+ - app-network
1615
+
1616
+ volumes:
1617
+ postgres-data:
1618
+
1619
+ networks:
1620
+ app-network:
1621
+ driver: bridge
1622
+ `;
1623
+ }
1624
+ }
1625
+
1626
+ module.exports = ProjectSetup;