go-duck-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -0
- package/generators/cache.js +107 -0
- package/generators/config.js +173 -0
- package/generators/devops.js +212 -0
- package/generators/docs.js +74 -0
- package/generators/graphql.js +38 -0
- package/generators/kratos.js +157 -0
- package/generators/logger.js +68 -0
- package/generators/metering.js +143 -0
- package/generators/migrations.js +240 -0
- package/generators/mqtt.js +87 -0
- package/generators/multitenancy.js +130 -0
- package/generators/postgrest.js +115 -0
- package/generators/repository.js +28 -0
- package/generators/resilience.js +69 -0
- package/generators/security.js +168 -0
- package/generators/swagger.js +145 -0
- package/generators/telemetry.js +121 -0
- package/generators/websocket.js +162 -0
- package/index.js +592 -0
- package/package.json +23 -0
- package/parser/gdl.js +162 -0
- package/templates/application.yml.hbs +18 -0
- package/templates/docs/gin_bottle.png +0 -0
- package/templates/docs/index.html.hbs +226 -0
- package/templates/docs/intro.mp4 +0 -0
- package/templates/docs/kratos_mark.png +0 -0
- package/templates/docs/layout.hbs +106 -0
- package/templates/docs/logo.png +0 -0
- package/templates/docs/pages/audit.hbs +39 -0
- package/templates/docs/pages/cli.hbs +83 -0
- package/templates/docs/pages/gdl.hbs +223 -0
- package/templates/docs/pages/graphql.hbs +51 -0
- package/templates/docs/pages/grpc.hbs +100 -0
- package/templates/docs/pages/index.hbs +181 -0
- package/templates/docs/pages/integrations.hbs +83 -0
- package/templates/docs/pages/observability.hbs +34 -0
- package/templates/docs/pages/realtime.hbs +43 -0
- package/templates/docs/pages/rest.hbs +149 -0
- package/templates/docs/pages/security.hbs +31 -0
- package/templates/go/controller.go.hbs +236 -0
- package/templates/go/entity.go.hbs +34 -0
- package/templates/go/enum.go.hbs +7 -0
- package/templates/go/main.go.hbs +186 -0
- package/templates/graphql/resolver.go.hbs +50 -0
- package/templates/graphql/schema.graphql.hbs +64 -0
- package/templates/kratos/service.go.hbs +104 -0
- package/templates/proto/entity.proto.hbs +95 -0
- package/test_parser.js +9 -0
package/index.js
ADDED
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GO-DUCK-CLI: A powerful Go code generator for microservices.
|
|
5
|
+
* Supports full project creation and incremental GDL imports.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
import fs from 'fs-extra';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import yaml from 'js-yaml';
|
|
12
|
+
import Handlebars from 'handlebars';
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
import { parseGDL } from './parser/gdl.js';
|
|
15
|
+
import { generateMultitenancy } from './generators/multitenancy.js';
|
|
16
|
+
import { generateLiquibaseChangelogs } from './generators/migrations.js';
|
|
17
|
+
import { generateMeteringCode } from './generators/metering.js';
|
|
18
|
+
import { generateGraphQLCode } from './generators/graphql.js';
|
|
19
|
+
import { generatePostgRESTCode } from './generators/postgrest.js';
|
|
20
|
+
import { generateSwaggerDocs } from './generators/swagger.js';
|
|
21
|
+
import { generateSecurityMiddleware } from './generators/security.js';
|
|
22
|
+
import { generateWebSocketCode } from './generators/websocket.js';
|
|
23
|
+
import { generateConfigLoader } from './generators/config.js';
|
|
24
|
+
import { generateLoggerCode } from './generators/logger.js';
|
|
25
|
+
import { generateMQTTCode } from './generators/mqtt.js';
|
|
26
|
+
import { generateCacheCode } from './generators/cache.js';
|
|
27
|
+
import { generateResilienceCode } from './generators/resilience.js';
|
|
28
|
+
import { generateTelemetryCode } from './generators/telemetry.js';
|
|
29
|
+
import { generateDeploymentArtifacts } from './generators/devops.js';
|
|
30
|
+
import { generateKratosCode } from './generators/kratos.js';
|
|
31
|
+
import { generateRepositoryCode } from './generators/repository.js';
|
|
32
|
+
import { generateDocumentation } from './generators/docs.js';
|
|
33
|
+
|
|
34
|
+
export const generateAuditCode = async (config, outputDir) => {
|
|
35
|
+
const middlewareDir = path.join(outputDir, 'middleware');
|
|
36
|
+
const modelsDir = path.join(outputDir, 'models');
|
|
37
|
+
const controllersDir = path.join(outputDir, 'controllers');
|
|
38
|
+
|
|
39
|
+
await fs.ensureDir(middlewareDir);
|
|
40
|
+
await fs.ensureDir(modelsDir);
|
|
41
|
+
await fs.ensureDir(controllersDir);
|
|
42
|
+
|
|
43
|
+
const auditModel = `
|
|
44
|
+
package models
|
|
45
|
+
|
|
46
|
+
import (
|
|
47
|
+
"time"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
type AuditLog struct {
|
|
51
|
+
ID uint \`gorm:"primaryKey" json:"id"\`
|
|
52
|
+
EntityName string \`json:"entityName"\`
|
|
53
|
+
EntityID string \`json:"entityId"\`
|
|
54
|
+
Action string \`json:"action"\` // CREATE, UPDATE, DELETE
|
|
55
|
+
PreviousValue string \`json:"previousValue" gorm:"type:text"\`
|
|
56
|
+
NewValue string \`json:"newValue" gorm:"type:text"\`
|
|
57
|
+
ModifiedBy string \`json:"modifiedBy"\`
|
|
58
|
+
KeycloakID string \`json:"keycloakId"\`
|
|
59
|
+
ModifiedAt time.Time \`json:"modifiedAt"\`
|
|
60
|
+
ClientIP string \`json:"clientIp"\`
|
|
61
|
+
}
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
const auditMiddleware = `
|
|
65
|
+
package middleware
|
|
66
|
+
|
|
67
|
+
import (
|
|
68
|
+
"bytes"
|
|
69
|
+
"io/ioutil"
|
|
70
|
+
"net/http"
|
|
71
|
+
"time"
|
|
72
|
+
|
|
73
|
+
"github.com/gin-gonic/gin"
|
|
74
|
+
"gorm.io/gorm"
|
|
75
|
+
"{{app_name}}/models"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
func AuditMiddleware(db *gorm.DB) gin.HandlerFunc {
|
|
79
|
+
return func(c *gin.Context) {
|
|
80
|
+
if c.Request.Method == http.MethodGet {
|
|
81
|
+
c.Next()
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Simplified auditing logic
|
|
86
|
+
method := c.Request.Method
|
|
87
|
+
path := c.Request.URL.Path
|
|
88
|
+
|
|
89
|
+
// Map method to action
|
|
90
|
+
action := "UPDATE"
|
|
91
|
+
if method == http.MethodPost { action = "CREATE" }
|
|
92
|
+
if method == http.MethodDelete { action = "DELETE" }
|
|
93
|
+
|
|
94
|
+
// Mock user and IP
|
|
95
|
+
userEmail := c.GetHeader("User-Email")
|
|
96
|
+
if userEmail == "" { userEmail = "anonymous" }
|
|
97
|
+
|
|
98
|
+
keycloakId := c.GetHeader("X-Keycloak-Id")
|
|
99
|
+
clientIP := c.ClientIP()
|
|
100
|
+
|
|
101
|
+
// Call next handlers
|
|
102
|
+
c.Next()
|
|
103
|
+
|
|
104
|
+
// Logic to capture entity ID and snapshot values would go here...
|
|
105
|
+
// For now, track the action
|
|
106
|
+
auditEntry := models.AuditLog{
|
|
107
|
+
EntityName: path,
|
|
108
|
+
Action: action,
|
|
109
|
+
ModifiedBy: userEmail,
|
|
110
|
+
KeycloakID: keycloakId,
|
|
111
|
+
ModifiedAt: time.Now(),
|
|
112
|
+
ClientIP: clientIP,
|
|
113
|
+
}
|
|
114
|
+
db.Create(&auditEntry)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
`;
|
|
118
|
+
|
|
119
|
+
const auditController = `
|
|
120
|
+
package controllers
|
|
121
|
+
|
|
122
|
+
import (
|
|
123
|
+
"net/http"
|
|
124
|
+
"{{app_name}}/models"
|
|
125
|
+
|
|
126
|
+
"github.com/gin-gonic/gin"
|
|
127
|
+
"gorm.io/gorm"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
type AuditController struct {
|
|
131
|
+
DB *gorm.DB
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func (ac *AuditController) GetLogs(c *gin.Context) {
|
|
135
|
+
var logs []models.AuditLog
|
|
136
|
+
ac.DB.Order("modified_at desc").Find(&logs)
|
|
137
|
+
c.JSON(http.StatusOK, logs)
|
|
138
|
+
}
|
|
139
|
+
`;
|
|
140
|
+
|
|
141
|
+
await fs.writeFile(path.join(modelsDir, 'audit_log.go'), auditModel);
|
|
142
|
+
await fs.writeFile(path.join(middlewareDir, 'audit_middleware.go'), auditMiddleware.replace('{{app_name}}', config.name));
|
|
143
|
+
await fs.writeFile(path.join(controllersDir, 'audit_controller.go'), auditController.replace('{{app_name}}', config.name));
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const program = new Command();
|
|
147
|
+
|
|
148
|
+
// Handlebars Helpers
|
|
149
|
+
Handlebars.registerHelper('capitalize', (str) => {
|
|
150
|
+
if (typeof str !== 'string') return '';
|
|
151
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
Handlebars.registerHelper('hasJson', (fields) => {
|
|
155
|
+
if (!fields || !Array.isArray(fields)) return false;
|
|
156
|
+
return fields.some(f => f.type === 'JSON' || f.type === 'JSONB');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
Handlebars.registerHelper('isJson', (type) => type === 'JSON' || type === 'JSONB');
|
|
160
|
+
|
|
161
|
+
Handlebars.registerHelper('toLowerCase', (str) => {
|
|
162
|
+
if (typeof str !== 'string') return '';
|
|
163
|
+
return str.toLowerCase();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
Handlebars.registerHelper('toGoType', (type, options) => {
|
|
167
|
+
const enums = options.data.root.enums || [];
|
|
168
|
+
const isEnum = enums.some(e => e.name === type);
|
|
169
|
+
if (isEnum) return type;
|
|
170
|
+
|
|
171
|
+
const types = {
|
|
172
|
+
'String': 'string',
|
|
173
|
+
'Text': 'string',
|
|
174
|
+
'Integer': 'int',
|
|
175
|
+
'Float': 'float64',
|
|
176
|
+
'Boolean': 'bool',
|
|
177
|
+
'Long': 'int64',
|
|
178
|
+
'BigDecimal': 'float64',
|
|
179
|
+
'LocalDate': 'time.Time',
|
|
180
|
+
'Instant': 'time.Time',
|
|
181
|
+
'JSON': 'datatypes.JSON',
|
|
182
|
+
'JSONB': 'datatypes.JSON'
|
|
183
|
+
};
|
|
184
|
+
return types[type] || 'interface{}';
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
Handlebars.registerHelper('gql_type', (type, options) => {
|
|
188
|
+
const enums = options.data.root.enums || [];
|
|
189
|
+
const isEnum = enums.some(e => e.name === type);
|
|
190
|
+
if (isEnum) return type;
|
|
191
|
+
|
|
192
|
+
const types = {
|
|
193
|
+
'String': 'String',
|
|
194
|
+
'Integer': 'Int',
|
|
195
|
+
'Float': 'Float',
|
|
196
|
+
'Boolean': 'Boolean',
|
|
197
|
+
'Long': 'ID',
|
|
198
|
+
'BigDecimal': 'Float',
|
|
199
|
+
'LocalDate': 'String',
|
|
200
|
+
'Instant': 'String',
|
|
201
|
+
'JSON': 'String',
|
|
202
|
+
'JSONB': 'String'
|
|
203
|
+
};
|
|
204
|
+
return types[type] || 'String';
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
Handlebars.registerHelper('eq', (a, b) => a === b);
|
|
208
|
+
|
|
209
|
+
program
|
|
210
|
+
.name('go-duck-cli')
|
|
211
|
+
.description('A powerful Go code generator for microservices')
|
|
212
|
+
.version('1.0.0');
|
|
213
|
+
|
|
214
|
+
// Helper to load configuration
|
|
215
|
+
const loadConfig = async (configPath) => {
|
|
216
|
+
try {
|
|
217
|
+
const fileContents = await fs.readFile(configPath, 'utf8');
|
|
218
|
+
const config = yaml.load(fileContents);
|
|
219
|
+
const appConfig = config.app || {};
|
|
220
|
+
return appConfig;
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.error(chalk.red(`Error loading config from ${configPath}:`), error.message);
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const saveEntitySnapshot = async (outputDir, entity) => {
|
|
228
|
+
const goDuckDir = path.join(outputDir, '.go-duck');
|
|
229
|
+
await fs.ensureDir(goDuckDir);
|
|
230
|
+
await fs.writeJson(path.join(goDuckDir, `${entity.name.toLowerCase()}.json`), entity, { spaces: 2 });
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const getPreviousEntities = async (outputDir) => {
|
|
234
|
+
const goDuckDir = path.join(outputDir, '.go-duck');
|
|
235
|
+
if (!await fs.pathExists(goDuckDir)) return [];
|
|
236
|
+
|
|
237
|
+
const files = await fs.readdir(goDuckDir);
|
|
238
|
+
const entities = [];
|
|
239
|
+
for (const file of files) {
|
|
240
|
+
if (file.endsWith('.json')) {
|
|
241
|
+
const content = await fs.readJson(path.join(goDuckDir, file));
|
|
242
|
+
entities.push(content);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return entities;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const generateEntities = async (gdlFilePath, outputDir, config) => {
|
|
249
|
+
if (!await fs.pathExists(gdlFilePath)) {
|
|
250
|
+
console.error(chalk.red(`❌ GDL file not found: ${gdlFilePath}`));
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const { entities, relationships, enums } = await parseGDL(gdlFilePath);
|
|
255
|
+
console.log(chalk.green(`✅ Parsed ${entities.length} entities, ${relationships.length} relationships, and ${enums.length} enums`));
|
|
256
|
+
|
|
257
|
+
const previousEntities = await getPreviousEntities(outputDir);
|
|
258
|
+
const delta = {
|
|
259
|
+
newEntities: [],
|
|
260
|
+
newFields: {},
|
|
261
|
+
newRelationships: []
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// Calculate Delta for Incremental Migrations
|
|
265
|
+
for (const entity of entities) {
|
|
266
|
+
const prev = previousEntities.find(e => e.name === entity.name);
|
|
267
|
+
if (!prev) {
|
|
268
|
+
delta.newEntities.push(entity);
|
|
269
|
+
} else {
|
|
270
|
+
// Check for new fields
|
|
271
|
+
const newFields = entity.fields.filter(f => !prev.fields.some(pf => pf.name === f.name));
|
|
272
|
+
if (newFields.length > 0) {
|
|
273
|
+
delta.newFields[entity.name] = newFields;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// New relationships
|
|
279
|
+
delta.newRelationships = relationships.filter(rel => {
|
|
280
|
+
const fromEntityCreated = delta.newEntities.some(e => e.name === rel.from.entity);
|
|
281
|
+
const toEntityCreated = delta.newEntities.some(e => e.name === rel.to.entity);
|
|
282
|
+
return fromEntityCreated || toEntityCreated;
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const entityTemplatePath = path.resolve(path.dirname(import.meta.url.replace('file://', '')), 'templates/go/entity.go.hbs');
|
|
286
|
+
const entityTemplateSource = await fs.readFile(entityTemplatePath, 'utf8');
|
|
287
|
+
const entityTemplate = Handlebars.compile(entityTemplateSource);
|
|
288
|
+
|
|
289
|
+
const controllerTemplatePath = path.resolve(path.dirname(import.meta.url.replace('file://', '')), 'templates/go/controller.go.hbs');
|
|
290
|
+
const controllerTemplateSource = await fs.readFile(controllerTemplatePath, 'utf8');
|
|
291
|
+
const controllerTemplate = Handlebars.compile(controllerTemplateSource);
|
|
292
|
+
|
|
293
|
+
const enumTemplatePath = path.resolve(path.dirname(import.meta.url.replace('file://', '')), 'templates/go/enum.go.hbs');
|
|
294
|
+
const enumTemplateSource = await fs.readFile(enumTemplatePath, 'utf8');
|
|
295
|
+
const enumTemplate = Handlebars.compile(enumTemplateSource);
|
|
296
|
+
|
|
297
|
+
await fs.ensureDir(path.join(outputDir, 'models'));
|
|
298
|
+
await fs.ensureDir(path.join(outputDir, 'controllers'));
|
|
299
|
+
|
|
300
|
+
// Generate Enums
|
|
301
|
+
if (enums.length > 0) {
|
|
302
|
+
let enumContent = 'package models\n\n';
|
|
303
|
+
for (const en of enums) {
|
|
304
|
+
enumContent += enumTemplate(en).trim() + '\n\n';
|
|
305
|
+
}
|
|
306
|
+
await fs.writeFile(path.join(outputDir, 'models', 'enums.go'), enumContent);
|
|
307
|
+
console.log(chalk.gray(` - Generated Enums: models/enums.go`));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
for (const entity of entities) {
|
|
311
|
+
entity.relationships = relationships.filter(r => r.from.entity === entity.name || r.to.entity === entity.name);
|
|
312
|
+
entity.app_name = config.name;
|
|
313
|
+
entity.enums = enums; // Pass enums context for helpers
|
|
314
|
+
|
|
315
|
+
// Generate Model
|
|
316
|
+
const entityContent = entityTemplate(entity);
|
|
317
|
+
await fs.writeFile(path.join(outputDir, 'models', `${entity.name.toLowerCase()}.go`), entityContent);
|
|
318
|
+
|
|
319
|
+
// Generate Controller
|
|
320
|
+
const controllerContent = controllerTemplate(entity);
|
|
321
|
+
await fs.writeFile(path.join(outputDir, 'controllers', `${entity.name.toLowerCase()}_controller.go`), controllerContent);
|
|
322
|
+
|
|
323
|
+
console.log(chalk.gray(` - Updated Entity & Controller: ${entity.name}`));
|
|
324
|
+
|
|
325
|
+
// Save Snapshot for next comparison
|
|
326
|
+
await saveEntitySnapshot(outputDir, entity);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Generate Incremental Changelogs!
|
|
330
|
+
await generateLiquibaseChangelogs(entities, relationships, outputDir, delta, enums);
|
|
331
|
+
console.log(chalk.green('✅ Liquibase incremental migrations updated!'));
|
|
332
|
+
|
|
333
|
+
return { entities, relationships, enums };
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
program
|
|
337
|
+
.command('create')
|
|
338
|
+
.description('Create a new base Go app from config.yaml and GDL')
|
|
339
|
+
.option('-c, --config <path>', 'Path to config.yaml', '../CONFIG/config.yaml')
|
|
340
|
+
.option('-o, --output <path>', 'Path to generate project', '.')
|
|
341
|
+
.option('-g, --gdl <path>', 'Path to GDL files directory', '../GDL')
|
|
342
|
+
.action(async (options) => {
|
|
343
|
+
const { config: configPath, output: outputDir, gdl: gdlDir } = options;
|
|
344
|
+
console.log(chalk.blue('🚀 Starting Go-Duck project generation...'));
|
|
345
|
+
|
|
346
|
+
const absoluteOutputDir = path.resolve(process.cwd(), outputDir);
|
|
347
|
+
await fs.ensureDir(absoluteOutputDir);
|
|
348
|
+
|
|
349
|
+
const config = await loadConfig(path.resolve(process.cwd(), configPath));
|
|
350
|
+
console.log(chalk.green(`✅ Config loaded for app: ${config.name}`));
|
|
351
|
+
|
|
352
|
+
await generateConfigLoader(absoluteOutputDir);
|
|
353
|
+
await generateLoggerCode(config, absoluteOutputDir);
|
|
354
|
+
await generateMQTTCode(config, absoluteOutputDir);
|
|
355
|
+
await generateCacheCode(config, absoluteOutputDir);
|
|
356
|
+
await generateResilienceCode(config, absoluteOutputDir);
|
|
357
|
+
await generateTelemetryCode(config, absoluteOutputDir);
|
|
358
|
+
await generateDeploymentArtifacts(config, absoluteOutputDir);
|
|
359
|
+
await generateYAMLConfigs(config, absoluteOutputDir);
|
|
360
|
+
const { entities, relationships, enums } = await generateEntities(path.join(path.resolve(process.cwd(), gdlDir), 'app.gdl'), absoluteOutputDir, config);
|
|
361
|
+
await generateKratosCode(entities, absoluteOutputDir, config.name, enums);
|
|
362
|
+
|
|
363
|
+
await generateRepositoryCode(absoluteOutputDir);
|
|
364
|
+
|
|
365
|
+
await generateGraphQLCode(config, entities, relationships, absoluteOutputDir, enums);
|
|
366
|
+
if (config.multitenancy?.enabled) await generateMultitenancy(config, absoluteOutputDir);
|
|
367
|
+
await generateAuditCode(config, absoluteOutputDir);
|
|
368
|
+
await generateMeteringCode(config, absoluteOutputDir);
|
|
369
|
+
await generateSecurityMiddleware(config, absoluteOutputDir);
|
|
370
|
+
await generateWebSocketCode(config, entities, absoluteOutputDir);
|
|
371
|
+
await generatePostgRESTCode(config, absoluteOutputDir);
|
|
372
|
+
console.log(chalk.green('✅ PostgREST-like search layer created!'));
|
|
373
|
+
|
|
374
|
+
// 8. Generate Swagger Docs
|
|
375
|
+
await generateSwaggerDocs(config, entities, absoluteOutputDir);
|
|
376
|
+
console.log(chalk.green('✅ Swagger API documentation generated!'));
|
|
377
|
+
|
|
378
|
+
// 8.5 Generate Web Docs App
|
|
379
|
+
await generateDocumentation(config, entities, absoluteOutputDir, enums);
|
|
380
|
+
console.log(chalk.green('✅ Web Documentation App generated!'));
|
|
381
|
+
|
|
382
|
+
// 9. Generate main.go
|
|
383
|
+
const mainTemplatePath = path.resolve(path.dirname(import.meta.url.replace('file://', '')), 'templates/go/main.go.hbs');
|
|
384
|
+
if (await fs.pathExists(mainTemplatePath)) {
|
|
385
|
+
const mainTemplateSource = await fs.readFile(mainTemplatePath, 'utf8');
|
|
386
|
+
const mainTemplate = Handlebars.compile(mainTemplateSource);
|
|
387
|
+
await fs.writeFile(path.join(absoluteOutputDir, 'main.go'), mainTemplate({ app_name: config.name, entities }));
|
|
388
|
+
console.log(chalk.green('✅ main.go entry point created!'));
|
|
389
|
+
}
|
|
390
|
+
console.log(chalk.bold.magenta('\n✨ Project created successfully!'));
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
program
|
|
394
|
+
.command('import-gdl <file>')
|
|
395
|
+
.description('Import entities from a GDL file into an existing app')
|
|
396
|
+
.option('-o, --output <path>', 'Path to the existing app root', '.')
|
|
397
|
+
.action(async (file, options) => {
|
|
398
|
+
const absoluteOutputDir = path.resolve(process.cwd(), options.output);
|
|
399
|
+
console.log(chalk.blue(`📥 Importing GDL from ${file}...`));
|
|
400
|
+
|
|
401
|
+
const config = await loadConfig(path.resolve(process.cwd(), '../CONFIG/config.yaml'));
|
|
402
|
+
await generateConfigLoader(absoluteOutputDir);
|
|
403
|
+
await generateLoggerCode(config, absoluteOutputDir);
|
|
404
|
+
await generateMQTTCode(config, absoluteOutputDir);
|
|
405
|
+
await generateCacheCode(config, absoluteOutputDir);
|
|
406
|
+
await generateResilienceCode(config, absoluteOutputDir);
|
|
407
|
+
await generateTelemetryCode(config, absoluteOutputDir);
|
|
408
|
+
await generateDeploymentArtifacts(config, absoluteOutputDir);
|
|
409
|
+
const { entities, relationships, enums } = await generateEntities(path.resolve(process.cwd(), file), absoluteOutputDir, config);
|
|
410
|
+
await generateKratosCode(entities, absoluteOutputDir, config.name, enums);
|
|
411
|
+
|
|
412
|
+
await generateRepositoryCode(absoluteOutputDir);
|
|
413
|
+
|
|
414
|
+
await generateGraphQLCode(config, entities, relationships, absoluteOutputDir, enums);
|
|
415
|
+
// Sync PostgREST search as well
|
|
416
|
+
await generatePostgRESTCode(config, absoluteOutputDir);
|
|
417
|
+
|
|
418
|
+
// Sync Security
|
|
419
|
+
await generateSecurityMiddleware(config, absoluteOutputDir);
|
|
420
|
+
|
|
421
|
+
// Sync WebSocket
|
|
422
|
+
await generateWebSocketCode(config, entities, absoluteOutputDir);
|
|
423
|
+
|
|
424
|
+
// Sync Swagger Docs
|
|
425
|
+
await generateSwaggerDocs(config, entities, absoluteOutputDir);
|
|
426
|
+
|
|
427
|
+
// Sync Web Docs App
|
|
428
|
+
await generateDocumentation(config, entities, absoluteOutputDir, enums);
|
|
429
|
+
|
|
430
|
+
// Regenerate main.go to include new routes if any (or just entities)
|
|
431
|
+
const mainTemplatePath = path.resolve(path.dirname(import.meta.url.replace('file://', '')), 'templates/go/main.go.hbs');
|
|
432
|
+
if (await fs.pathExists(mainTemplatePath)) {
|
|
433
|
+
const mainTemplate = Handlebars.compile(await fs.readFile(mainTemplatePath, 'utf8'));
|
|
434
|
+
await fs.writeFile(path.join(absoluteOutputDir, 'main.go'), mainTemplate({ app_name: config.name, entities }));
|
|
435
|
+
console.log(chalk.green('✅ Updated main.go to register new entity routes.'));
|
|
436
|
+
}
|
|
437
|
+
console.log(chalk.bold.magenta('\n✨ GDL Import Completed with Incremental Migrations! ✨'));
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
const generateYAMLConfigs = async (config, outputDir) => {
|
|
441
|
+
// Clean up input config - remove the static multitenancy-databases list if it exists
|
|
442
|
+
const cleanConfig = { ...config };
|
|
443
|
+
if (cleanConfig.datasource && cleanConfig.datasource['multitenancy-databases']) {
|
|
444
|
+
delete cleanConfig.datasource['multitenancy-databases'];
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const extendedConfig = {
|
|
448
|
+
...cleanConfig,
|
|
449
|
+
server: {
|
|
450
|
+
port: 8080,
|
|
451
|
+
'read-timeout': '30s',
|
|
452
|
+
'write-timeout': '30s',
|
|
453
|
+
grpc: {
|
|
454
|
+
addr: ':9000',
|
|
455
|
+
network: 'tcp',
|
|
456
|
+
timeout: '1s'
|
|
457
|
+
},
|
|
458
|
+
cors: {
|
|
459
|
+
'allow-origins': ['*'],
|
|
460
|
+
'allow-methods': ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
461
|
+
'allow-headers': ['Origin', 'Content-Type', 'Accept', 'Authorization']
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
datasource: {
|
|
465
|
+
...cleanConfig.datasource,
|
|
466
|
+
'max-open-conns': 25,
|
|
467
|
+
'max-idle-conns': 10,
|
|
468
|
+
'conn-max-lifetime': '5m'
|
|
469
|
+
},
|
|
470
|
+
security: {
|
|
471
|
+
...cleanConfig.security,
|
|
472
|
+
'rate-limit': {
|
|
473
|
+
rps: 100,
|
|
474
|
+
burst: 200
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
logging: {
|
|
478
|
+
datadog: {
|
|
479
|
+
enabled: false,
|
|
480
|
+
'api-key': 'YOUR_DATADOG_API_KEY',
|
|
481
|
+
site: 'datadoghq.com',
|
|
482
|
+
service: cleanConfig.name || 'go-duck-service'
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
messaging: {
|
|
486
|
+
mqtt: {
|
|
487
|
+
enabled: false,
|
|
488
|
+
broker: 'tcp://localhost:1883',
|
|
489
|
+
'client-id': (cleanConfig.name || 'go-duck') + '-dev',
|
|
490
|
+
username: 'dev_user',
|
|
491
|
+
password: 'dev_password',
|
|
492
|
+
'topic-prefix': 'go-duck/events'
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
cache: {
|
|
496
|
+
redis: {
|
|
497
|
+
enabled: false,
|
|
498
|
+
host: 'localhost:6379',
|
|
499
|
+
password: '',
|
|
500
|
+
db: 0,
|
|
501
|
+
ttl: '10m'
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
resilience: {
|
|
505
|
+
'circuit-breaker': {
|
|
506
|
+
enabled: true,
|
|
507
|
+
'failure-threshold': 5,
|
|
508
|
+
'success-threshold': 2,
|
|
509
|
+
timeout: '60s'
|
|
510
|
+
}
|
|
511
|
+
},
|
|
512
|
+
telemetry: {
|
|
513
|
+
otel: {
|
|
514
|
+
enabled: false,
|
|
515
|
+
endpoint: 'localhost:4317',
|
|
516
|
+
'sampler-ratio': 1.0
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const baseConfig = { 'go-duck': extendedConfig };
|
|
522
|
+
await fs.writeFile(path.join(outputDir, 'application.yml'), yaml.dump(baseConfig));
|
|
523
|
+
|
|
524
|
+
const devConfig = {
|
|
525
|
+
'go-duck': extendedConfig,
|
|
526
|
+
environment: { active_profile: 'dev' }
|
|
527
|
+
};
|
|
528
|
+
await fs.writeFile(path.join(outputDir, 'application-dev.yml'), yaml.dump(devConfig));
|
|
529
|
+
|
|
530
|
+
const prodConfig = {
|
|
531
|
+
'go-duck': {
|
|
532
|
+
...extendedConfig,
|
|
533
|
+
server: {
|
|
534
|
+
...extendedConfig.server,
|
|
535
|
+
cors: {
|
|
536
|
+
...extendedConfig.server.cors,
|
|
537
|
+
'allow-origins': ['https://your-domain.com']
|
|
538
|
+
}
|
|
539
|
+
},
|
|
540
|
+
logging: {
|
|
541
|
+
...extendedConfig.logging,
|
|
542
|
+
datadog: {
|
|
543
|
+
...extendedConfig.logging.datadog,
|
|
544
|
+
enabled: true
|
|
545
|
+
}
|
|
546
|
+
},
|
|
547
|
+
messaging: {
|
|
548
|
+
mqtt: {
|
|
549
|
+
enabled: true,
|
|
550
|
+
broker: 'tcp://mqtt.production.svc:1883',
|
|
551
|
+
'client-id': cleanConfig.name + '-prod',
|
|
552
|
+
username: 'prod_user',
|
|
553
|
+
password: 'prod_password',
|
|
554
|
+
'topic-prefix': 'go-duck/events'
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
cache: {
|
|
558
|
+
redis: {
|
|
559
|
+
enabled: true,
|
|
560
|
+
host: 'redis.production.svc:6379',
|
|
561
|
+
password: 'prod_redis_password',
|
|
562
|
+
db: 0,
|
|
563
|
+
ttl: '1h'
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
resilience: {
|
|
567
|
+
'circuit-breaker': {
|
|
568
|
+
enabled: true,
|
|
569
|
+
'failure-threshold': 3,
|
|
570
|
+
'success-threshold': 5,
|
|
571
|
+
timeout: '30s'
|
|
572
|
+
}
|
|
573
|
+
},
|
|
574
|
+
telemetry: {
|
|
575
|
+
otel: {
|
|
576
|
+
enabled: true,
|
|
577
|
+
endpoint: 'otel-collector.monitoring.svc:4317',
|
|
578
|
+
'sampler-ratio': 0.1
|
|
579
|
+
}
|
|
580
|
+
},
|
|
581
|
+
datasource: {
|
|
582
|
+
...extendedConfig.datasource,
|
|
583
|
+
'max-open-conns': 100,
|
|
584
|
+
'max-idle-conns': 50
|
|
585
|
+
}
|
|
586
|
+
},
|
|
587
|
+
environment: { active_profile: 'prod' }
|
|
588
|
+
};
|
|
589
|
+
await fs.writeFile(path.join(outputDir, 'application-prod.yml'), yaml.dump(prodConfig));
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
program.parse(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "go-duck-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "go function generator",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"go-duck": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
12
|
+
},
|
|
13
|
+
"author": "heavenscode",
|
|
14
|
+
"license": "ISC",
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"chalk": "^4.1.2",
|
|
17
|
+
"commander": "^14.0.3",
|
|
18
|
+
"fs-extra": "^11.3.4",
|
|
19
|
+
"handlebars": "^4.7.8",
|
|
20
|
+
"inquirer": "^8.2.7",
|
|
21
|
+
"js-yaml": "^4.1.1"
|
|
22
|
+
}
|
|
23
|
+
}
|