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.
- package/README.md +287 -0
- package/git-manager.sh +495 -0
- package/package.json +36 -0
- package/prompt.md +6 -0
- package/src/bin/index.js +410 -0
- package/src/lib/constants/index.js +24 -0
- package/src/lib/shares/createDir.js +22 -0
- package/src/lib/shares/createFile.js +23 -0
- package/src/lib/utils/add-auth-to-resource.js +225 -0
- package/src/lib/utils/create-auth.js +937 -0
- package/src/lib/utils/create-resource.js +1426 -0
- package/src/lib/utils/create-service.js +456 -0
- package/src/lib/utils/display.js +19 -0
- package/src/lib/utils/help.js +93 -0
- package/src/lib/utils/remove-module.js +146 -0
- package/src/lib/utils/setup.js +1626 -0
|
@@ -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;
|