go-duck-cli 1.0.9 ā 1.1.12
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 +30 -15
- package/generators/ai_docs.js +130 -0
- package/generators/broker.js +63 -0
- package/generators/config.js +149 -7
- package/generators/devops.js +210 -43
- package/generators/docs.js +23 -4
- package/generators/elasticsearch.js +263 -0
- package/generators/kratos.js +229 -41
- package/generators/metering.js +280 -48
- package/generators/migrations.js +92 -198
- package/generators/mqtt.js +2 -39
- package/generators/multitenancy.js +274 -71
- package/generators/nats.js +39 -0
- package/generators/outbox.js +171 -0
- package/generators/postgrest.js +7 -3
- package/generators/postman.js +405 -0
- package/generators/repository.js +27 -0
- package/generators/router.js +27 -0
- package/generators/security.js +95 -14
- package/generators/serverless.js +147 -0
- package/generators/storage.js +589 -0
- package/generators/swagger.js +84 -60
- package/generators/telemetry.js +23 -32
- package/generators/websocket.js +55 -21
- package/index.js +493 -116
- package/package.json +6 -4
- package/parser/gdl.js +163 -24
- package/templates/docs/index.html.hbs +5 -5
- package/templates/docs/layout.hbs +221 -62
- package/templates/docs/pages/audit.hbs +83 -35
- package/templates/docs/pages/cli.hbs +18 -0
- package/templates/docs/pages/configuration.hbs +241 -0
- package/templates/docs/pages/datadog.hbs +46 -0
- package/templates/docs/pages/elasticsearch.hbs +121 -0
- package/templates/docs/pages/federation.hbs +241 -0
- package/templates/docs/pages/gdl-advanced.hbs +91 -0
- package/templates/docs/pages/gdl-annotations.hbs +137 -0
- package/templates/docs/pages/gdl-entities.hbs +134 -0
- package/templates/docs/pages/gdl-relationships.hbs +80 -0
- package/templates/docs/pages/gdl.hbs +60 -204
- package/templates/docs/pages/graphql.hbs +58 -44
- package/templates/docs/pages/grpc.hbs +53 -90
- package/templates/docs/pages/hybrid-store.hbs +127 -0
- package/templates/docs/pages/index.hbs +418 -149
- package/templates/docs/pages/keycloak.hbs +43 -0
- package/templates/docs/pages/legend.hbs +116 -0
- package/templates/docs/pages/mosquitto.hbs +39 -0
- package/templates/docs/pages/multitenancy.hbs +139 -71
- package/templates/docs/pages/otel.hbs +40 -0
- package/templates/docs/pages/realtime.hbs +38 -12
- package/templates/docs/pages/redis.hbs +40 -0
- package/templates/docs/pages/rest.hbs +120 -202
- package/templates/docs/pages/saga.hbs +94 -0
- package/templates/docs/pages/security.hbs +150 -44
- package/templates/docs/pages/serverless.hbs +157 -0
- package/templates/docs/pages/storage.hbs +127 -0
- package/templates/docs/pages/wizard.hbs +683 -0
- package/templates/docs/triple_identity_registry.png +0 -0
- package/templates/go/controller.go.hbs +287 -283
- package/templates/go/entity.go.hbs +17 -15
- package/templates/go/main.go.hbs +47 -180
- package/templates/go/migrator.go.hbs +65 -0
- package/templates/go/router.go.hbs +272 -0
- package/templates/graphql/resolver.go.hbs +53 -34
- package/templates/graphql/schema.graphql.hbs +17 -5
- package/templates/kratos/service.go.hbs +169 -34
- package/templates/proto/entity.proto.hbs +10 -14
- package/test_nested.gdl +21 -0
- package/templates/docs/intro.mp4 +0 -0
- package/test_parser.js +0 -9
package/index.js
CHANGED
|
@@ -11,6 +11,9 @@ import path from 'path';
|
|
|
11
11
|
import yaml from 'js-yaml';
|
|
12
12
|
import Handlebars from 'handlebars';
|
|
13
13
|
import chalk from 'chalk';
|
|
14
|
+
import { execSync } from 'child_process';
|
|
15
|
+
import express from 'express';
|
|
16
|
+
import open from 'open';
|
|
14
17
|
import { parseGDL } from './parser/gdl.js';
|
|
15
18
|
import { generateMultitenancy } from './generators/multitenancy.js';
|
|
16
19
|
import { generateLiquibaseChangelogs } from './generators/migrations.js';
|
|
@@ -18,6 +21,7 @@ import { generateMeteringCode } from './generators/metering.js';
|
|
|
18
21
|
import { generateGraphQLCode } from './generators/graphql.js';
|
|
19
22
|
import { generatePostgRESTCode } from './generators/postgrest.js';
|
|
20
23
|
import { generateSwaggerDocs } from './generators/swagger.js';
|
|
24
|
+
import { generatePostmanCollection } from './generators/postman.js';
|
|
21
25
|
import { generateSecurityMiddleware } from './generators/security.js';
|
|
22
26
|
import { generateWebSocketCode } from './generators/websocket.js';
|
|
23
27
|
import { generateConfigLoader } from './generators/config.js';
|
|
@@ -30,6 +34,14 @@ import { generateDeploymentArtifacts } from './generators/devops.js';
|
|
|
30
34
|
import { generateKratosCode } from './generators/kratos.js';
|
|
31
35
|
import { generateRepositoryCode } from './generators/repository.js';
|
|
32
36
|
import { generateDocumentation } from './generators/docs.js';
|
|
37
|
+
import { generateOutbox } from './generators/outbox.js';
|
|
38
|
+
import { generateNATSCode } from './generators/nats.js';
|
|
39
|
+
import { generateBrokerCode } from './generators/broker.js';
|
|
40
|
+
import { generateStorageCode } from './generators/storage.js';
|
|
41
|
+
import { generateElasticsearchLayer } from './generators/elasticsearch.js';
|
|
42
|
+
import { generateServerlessHandler } from './generators/serverless.js';
|
|
43
|
+
import { generateRouterCode } from './generators/router.js';
|
|
44
|
+
import { generateAIDocs } from './generators/ai_docs.js';
|
|
33
45
|
|
|
34
46
|
export const generateAuditCode = async (config, outputDir) => {
|
|
35
47
|
const middlewareDir = path.join(outputDir, 'middleware');
|
|
@@ -50,6 +62,7 @@ import (
|
|
|
50
62
|
type AuditLog struct {
|
|
51
63
|
ID uint \`gorm:"primaryKey" json:"id"\`
|
|
52
64
|
EntityName string \`json:"entityName"\`
|
|
65
|
+
TenantDB string \`json:"tenantDb"\`
|
|
53
66
|
EntityID string \`json:"entityId"\`
|
|
54
67
|
Action string \`json:"action"\` // CREATE, UPDATE, DELETE
|
|
55
68
|
PreviousValue string \`json:"previousValue" gorm:"type:text"\`
|
|
@@ -65,8 +78,7 @@ type AuditLog struct {
|
|
|
65
78
|
package middleware
|
|
66
79
|
|
|
67
80
|
import (
|
|
68
|
-
"
|
|
69
|
-
"io/ioutil"
|
|
81
|
+
"fmt"
|
|
70
82
|
"net/http"
|
|
71
83
|
"time"
|
|
72
84
|
|
|
@@ -77,7 +89,7 @@ import (
|
|
|
77
89
|
|
|
78
90
|
func AuditMiddleware(db *gorm.DB) gin.HandlerFunc {
|
|
79
91
|
return func(c *gin.Context) {
|
|
80
|
-
if c.Request.Method == http.MethodGet {
|
|
92
|
+
if db == nil || c.Request.Method == http.MethodGet {
|
|
81
93
|
c.Next()
|
|
82
94
|
return
|
|
83
95
|
}
|
|
@@ -91,23 +103,30 @@ func AuditMiddleware(db *gorm.DB) gin.HandlerFunc {
|
|
|
91
103
|
if method == http.MethodPost { action = "CREATE" }
|
|
92
104
|
if method == http.MethodDelete { action = "DELETE" }
|
|
93
105
|
|
|
94
|
-
//
|
|
95
|
-
userEmail := c.
|
|
96
|
-
|
|
106
|
+
// Extract Identity from context
|
|
107
|
+
userEmail, _ := c.Get("UserEmail")
|
|
108
|
+
emailStr := "anonymous"
|
|
109
|
+
if email, ok := userEmail.(string); ok { emailStr = email }
|
|
110
|
+
|
|
111
|
+
keycloakId, _ := c.Get("KeycloakID")
|
|
112
|
+
kidStr := ""
|
|
113
|
+
if kid, ok := keycloakId.(string); ok { kidStr = kid }
|
|
97
114
|
|
|
98
|
-
keycloakId := c.GetHeader("X-Keycloak-Id")
|
|
99
115
|
clientIP := c.ClientIP()
|
|
100
116
|
|
|
117
|
+
tenant, _ := c.Get("tenantDB")
|
|
118
|
+
tenantStr := fmt.Sprintf("%v", tenant)
|
|
119
|
+
|
|
101
120
|
// Call next handlers
|
|
102
121
|
c.Next()
|
|
103
122
|
|
|
104
123
|
// Logic to capture entity ID and snapshot values would go here...
|
|
105
|
-
// For now, track the action
|
|
106
124
|
auditEntry := models.AuditLog{
|
|
107
125
|
EntityName: path,
|
|
126
|
+
TenantDB: tenantStr,
|
|
108
127
|
Action: action,
|
|
109
|
-
ModifiedBy:
|
|
110
|
-
KeycloakID:
|
|
128
|
+
ModifiedBy: emailStr,
|
|
129
|
+
KeycloakID: kidStr,
|
|
111
130
|
ModifiedAt: time.Now(),
|
|
112
131
|
ClientIP: clientIP,
|
|
113
132
|
}
|
|
@@ -148,15 +167,26 @@ const program = new Command();
|
|
|
148
167
|
// Handlebars Helpers
|
|
149
168
|
Handlebars.registerHelper('capitalize', (str) => {
|
|
150
169
|
if (typeof str !== 'string') return '';
|
|
151
|
-
|
|
170
|
+
// Handle snake_case by splitting and joining title-cased parts
|
|
171
|
+
return str.replace(/(?:^|_| )(\w)/g, (match, p1) => p1.toUpperCase());
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
Handlebars.registerHelper('toProtoFieldName', (str) => {
|
|
175
|
+
if (typeof str !== 'string') return '';
|
|
176
|
+
// Protoc-gen-go converts snake_case to TitleCase (e.g. site_id -> SiteId)
|
|
177
|
+
// But it ALSO removes underscores. So we split and capitalize.
|
|
178
|
+
return str.split('_').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join('');
|
|
152
179
|
});
|
|
153
180
|
|
|
154
181
|
Handlebars.registerHelper('hasJson', (fields) => {
|
|
155
182
|
if (!fields || !Array.isArray(fields)) return false;
|
|
156
|
-
return fields.some(f => f.type === '
|
|
183
|
+
return fields.some(f => (f.type || '').toLowerCase() === 'json' || (f.type || '').toLowerCase() === 'jsonb');
|
|
157
184
|
});
|
|
158
185
|
|
|
159
|
-
Handlebars.registerHelper('isJson', (type) =>
|
|
186
|
+
Handlebars.registerHelper('isJson', (type) => {
|
|
187
|
+
const t = (type || '').toLowerCase();
|
|
188
|
+
return t === 'json' || t === 'jsonb';
|
|
189
|
+
});
|
|
160
190
|
|
|
161
191
|
Handlebars.registerHelper('toLowerCase', (str) => {
|
|
162
192
|
if (typeof str !== 'string') return '';
|
|
@@ -165,23 +195,30 @@ Handlebars.registerHelper('toLowerCase', (str) => {
|
|
|
165
195
|
|
|
166
196
|
Handlebars.registerHelper('toGoType', (type, options) => {
|
|
167
197
|
const enums = options.data.root.enums || [];
|
|
168
|
-
const isEnum = enums.some(e => e.name === type);
|
|
169
|
-
if (isEnum)
|
|
198
|
+
const isEnum = enums.some(e => e.name.toLowerCase() === type.toLowerCase());
|
|
199
|
+
if (isEnum) {
|
|
200
|
+
const en = enums.find(e => e.name.toLowerCase() === type.toLowerCase());
|
|
201
|
+
return en.name;
|
|
202
|
+
}
|
|
170
203
|
|
|
204
|
+
const t = type.toLowerCase();
|
|
171
205
|
const types = {
|
|
172
|
-
'
|
|
173
|
-
'
|
|
174
|
-
'
|
|
175
|
-
'
|
|
176
|
-
'
|
|
177
|
-
'
|
|
178
|
-
'
|
|
179
|
-
'
|
|
180
|
-
'
|
|
181
|
-
'
|
|
182
|
-
'
|
|
206
|
+
'string': 'string',
|
|
207
|
+
'text': 'string',
|
|
208
|
+
'integer': 'int',
|
|
209
|
+
'int': 'int',
|
|
210
|
+
'float': 'float64',
|
|
211
|
+
'boolean': 'bool',
|
|
212
|
+
'bool': 'bool',
|
|
213
|
+
'long': 'int64',
|
|
214
|
+
'bigdecimal': 'float64',
|
|
215
|
+
'localdate': 'time.Time',
|
|
216
|
+
'instant': 'time.Time',
|
|
217
|
+
'datetime': 'time.Time',
|
|
218
|
+
'json': 'datatypes.JSON',
|
|
219
|
+
'jsonb': 'datatypes.JSON'
|
|
183
220
|
};
|
|
184
|
-
return types[
|
|
221
|
+
return types[t] || 'interface{}';
|
|
185
222
|
});
|
|
186
223
|
|
|
187
224
|
Handlebars.registerHelper('gql_type', (type, options) => {
|
|
@@ -204,8 +241,53 @@ Handlebars.registerHelper('gql_type', (type, options) => {
|
|
|
204
241
|
return types[type] || 'String';
|
|
205
242
|
});
|
|
206
243
|
|
|
244
|
+
Handlebars.registerHelper('isAnyOperationOpen', (entityName, openEntities) => {
|
|
245
|
+
if (!openEntities || !Array.isArray(openEntities)) return false;
|
|
246
|
+
const wildcard = openEntities.find(e => e.name === '*');
|
|
247
|
+
if (wildcard && wildcard.actions && wildcard.actions.some(a => a !== 'none' && a !== '')) return true;
|
|
248
|
+
const entry = openEntities.find(e => e.name.toLowerCase() === entityName.toLowerCase());
|
|
249
|
+
return !!(entry && entry.actions && entry.actions.some(a => a !== 'none' && a !== ''));
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
Handlebars.registerHelper('isOpen', (entityName, openEntities, action) => {
|
|
253
|
+
if (!openEntities || !Array.isArray(openEntities)) return false;
|
|
254
|
+
|
|
255
|
+
// Check wildcard first
|
|
256
|
+
const wildcard = openEntities.find(e => e.name === '*');
|
|
257
|
+
if (wildcard) {
|
|
258
|
+
if (typeof action !== 'string') return true;
|
|
259
|
+
if (wildcard.actions.includes(action.toLowerCase())) return true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const entry = openEntities.find(e => e.name.toLowerCase() === entityName.toLowerCase());
|
|
263
|
+
if (entry) {
|
|
264
|
+
if (typeof action !== 'string') return true;
|
|
265
|
+
if (entry.actions.includes(action.toLowerCase())) return true;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return false;
|
|
269
|
+
});
|
|
270
|
+
|
|
207
271
|
Handlebars.registerHelper('eq', (a, b) => a === b);
|
|
208
272
|
|
|
273
|
+
Handlebars.registerHelper('or', function() {
|
|
274
|
+
const args = Array.prototype.slice.call(arguments, 0, -1);
|
|
275
|
+
return args.some(Boolean);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Recursive field rendering for multi-level JSON
|
|
279
|
+
Handlebars.registerPartial('renderFields', `
|
|
280
|
+
{{#each fields}}
|
|
281
|
+
{{#if isNested}}
|
|
282
|
+
{{capitalize name}} struct {
|
|
283
|
+
{{> renderFields fields=children isDocument=../isDocument}}
|
|
284
|
+
} \`json:"{{name}}"{{#if ../isDocument}} bson:"{{name}}"{{/if}}\`
|
|
285
|
+
{{else}}
|
|
286
|
+
{{capitalize name}} {{toGoType type}} \`{{#if ../isDocument}}bson:"{{#if (eq name "id")}}_id{{else}}{{name}}{{/if}}{{#if (eq name "id")}},omitempty{{/if}}"{{else}}{{#if (eq name "id")}}gorm:"primaryKey"{{else}}{{#if unique}}gorm:"uniqueIndex"{{/if}}{{/if}}{{/if}} {{#if (isJson type)}}gorm:"type:{{toLowerCase type}};serializer:json"{{/if}} json:"{{name}}"{{#if required}} binding:"required"{{/if}}\`
|
|
287
|
+
{{/if}}
|
|
288
|
+
{{/each}}
|
|
289
|
+
`);
|
|
290
|
+
|
|
209
291
|
program
|
|
210
292
|
.name('go-duck-cli')
|
|
211
293
|
.description('A powerful Go code generator for microservices')
|
|
@@ -216,7 +298,7 @@ const loadConfig = async (configPath) => {
|
|
|
216
298
|
try {
|
|
217
299
|
const fileContents = await fs.readFile(configPath, 'utf8');
|
|
218
300
|
const config = yaml.load(fileContents);
|
|
219
|
-
const appConfig = config.app || {};
|
|
301
|
+
const appConfig = config['go-duck'] || config.app || config || {};
|
|
220
302
|
return appConfig;
|
|
221
303
|
} catch (error) {
|
|
222
304
|
console.error(chalk.red(`Error loading config from ${configPath}:`), error.message);
|
|
@@ -245,14 +327,46 @@ const getPreviousEntities = async (outputDir) => {
|
|
|
245
327
|
return entities;
|
|
246
328
|
};
|
|
247
329
|
|
|
248
|
-
const generateEntities = async (
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
330
|
+
const generateEntities = async (gdlPath, outputDir, config) => {
|
|
331
|
+
let entities = [];
|
|
332
|
+
let relationships = [];
|
|
333
|
+
let enums = [];
|
|
334
|
+
let openEntities = [];
|
|
335
|
+
|
|
336
|
+
if (!await fs.pathExists(gdlPath)) {
|
|
337
|
+
console.log(chalk.yellow(`ā ļø GDL path not found: ${gdlPath} - Executing Zero-Entity Infrastructure Scaffold.`));
|
|
338
|
+
const delta = { newEntities: [], newFields: {}, newRelationships: [] };
|
|
339
|
+
await generateLiquibaseChangelogs(entities, relationships, outputDir, delta, enums);
|
|
340
|
+
return { entities, relationships, enums, openEntities };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const stats = await fs.stat(gdlPath);
|
|
344
|
+
const files = stats.isDirectory()
|
|
345
|
+
? (await fs.readdir(gdlPath)).filter(f => f.endsWith('.gdl')).map(f => path.join(gdlPath, f))
|
|
346
|
+
: [gdlPath];
|
|
347
|
+
|
|
348
|
+
if (files.length === 0) {
|
|
349
|
+
console.log(chalk.yellow(`ā ļø No .gdl files found in: ${gdlPath} - Executing Zero-Entity Infrastructure Scaffold.`));
|
|
350
|
+
const delta = { newEntities: [], newFields: {}, newRelationships: [] };
|
|
351
|
+
await generateLiquibaseChangelogs(entities, relationships, outputDir, delta, enums);
|
|
352
|
+
return { entities, relationships, enums, openEntities };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
console.log(chalk.blue(`š Loading ${files.length} GDL file(s)...`));
|
|
356
|
+
|
|
357
|
+
for (const file of files) {
|
|
358
|
+
const result = await parseGDL(file);
|
|
359
|
+
entities = [...entities, ...result.entities];
|
|
360
|
+
relationships = [...relationships, ...result.relationships];
|
|
361
|
+
enums = [...enums, ...result.enums];
|
|
362
|
+
openEntities = [...openEntities, ...result.openEntities];
|
|
252
363
|
}
|
|
253
364
|
|
|
254
|
-
|
|
255
|
-
|
|
365
|
+
// Deduplicate entities and enums by name
|
|
366
|
+
entities = Array.from(new Map(entities.map(e => [e.name, e])).values());
|
|
367
|
+
enums = Array.from(new Map(enums.map(e => [e.name, e])).values());
|
|
368
|
+
|
|
369
|
+
console.log(chalk.green(`ā
Parsed ${entities.length} entities, ${relationships.length} relationships, ${enums.length} enums, and detected ${openEntities.length} open rules`));
|
|
256
370
|
|
|
257
371
|
const previousEntities = await getPreviousEntities(outputDir);
|
|
258
372
|
const delta = {
|
|
@@ -308,9 +422,20 @@ const generateEntities = async (gdlFilePath, outputDir, config) => {
|
|
|
308
422
|
}
|
|
309
423
|
|
|
310
424
|
for (const entity of entities) {
|
|
311
|
-
entity.relationships = relationships.filter(r => r.from.entity === entity.name || r.to.entity === entity.name)
|
|
425
|
+
entity.relationships = relationships.filter(r => r.from.entity === entity.name || r.to.entity === entity.name).map(r => {
|
|
426
|
+
const fromEnt = entities.find(e => e.name === r.from.entity);
|
|
427
|
+
const toEnt = entities.find(e => e.name === r.to.entity);
|
|
428
|
+
return {
|
|
429
|
+
...r,
|
|
430
|
+
fromIsDocument: fromEnt ? fromEnt.isDocument : false,
|
|
431
|
+
toIsDocument: toEnt ? toEnt.isDocument : false
|
|
432
|
+
};
|
|
433
|
+
});
|
|
312
434
|
entity.app_name = config.name;
|
|
313
|
-
entity.enums = enums;
|
|
435
|
+
entity.enums = enums;
|
|
436
|
+
entity.openEntities = openEntities; // Pass open entities to template
|
|
437
|
+
// Use already parsed flags from gdl.js
|
|
438
|
+
// entity.isSearchable, entity.isFederated, entity.isAudited, entity.isDocument are already set correctly.
|
|
314
439
|
|
|
315
440
|
// Generate Model
|
|
316
441
|
const entityContent = entityTemplate(entity);
|
|
@@ -320,7 +445,7 @@ const generateEntities = async (gdlFilePath, outputDir, config) => {
|
|
|
320
445
|
const controllerContent = controllerTemplate(entity);
|
|
321
446
|
await fs.writeFile(path.join(outputDir, 'controllers', `${entity.name.toLowerCase()}_controller.go`), controllerContent);
|
|
322
447
|
|
|
323
|
-
console.log(chalk.gray(` - Updated Entity & Controller: ${entity.name}`));
|
|
448
|
+
console.log(chalk.gray(` - Updated Entity & Controller: ${entity.name} (Mongo: ${entity.isDocument || false})`));
|
|
324
449
|
|
|
325
450
|
// Save Snapshot for next comparison
|
|
326
451
|
await saveEntitySnapshot(outputDir, entity);
|
|
@@ -328,9 +453,9 @@ const generateEntities = async (gdlFilePath, outputDir, config) => {
|
|
|
328
453
|
|
|
329
454
|
// Generate Incremental Changelogs!
|
|
330
455
|
await generateLiquibaseChangelogs(entities, relationships, outputDir, delta, enums);
|
|
331
|
-
console.log(chalk.green('ā
|
|
456
|
+
console.log(chalk.green('ā
Goose SQL incremental migrations updated!'));
|
|
332
457
|
|
|
333
|
-
return { entities, relationships, enums };
|
|
458
|
+
return { entities, relationships, enums, openEntities };
|
|
334
459
|
};
|
|
335
460
|
|
|
336
461
|
program
|
|
@@ -349,6 +474,27 @@ program
|
|
|
349
474
|
const config = await loadConfig(path.resolve(process.cwd(), configPath));
|
|
350
475
|
console.log(chalk.green(`ā
Config loaded for app: ${config.name}`));
|
|
351
476
|
|
|
477
|
+
// Copy configuration file to the generated project .go-duck folder
|
|
478
|
+
const sourceConfigResolved = path.resolve(process.cwd(), configPath);
|
|
479
|
+
const targetConfigResolved = path.resolve(absoluteOutputDir, '.go-duck/config.yaml');
|
|
480
|
+
if (sourceConfigResolved !== targetConfigResolved) {
|
|
481
|
+
await fs.ensureDir(path.dirname(targetConfigResolved));
|
|
482
|
+
await fs.copy(sourceConfigResolved, targetConfigResolved);
|
|
483
|
+
console.log(chalk.green(`š Config file copied to: ${path.relative(process.cwd(), targetConfigResolved)}`));
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Cleanup legacy files if they exist in the root
|
|
487
|
+
const legacyFiles = ['Dockerfile', 'docker-compose.yml'];
|
|
488
|
+
const legacyDirs = ['k8s', 'realm-config'];
|
|
489
|
+
for (const f of legacyFiles) {
|
|
490
|
+
const p = path.join(absoluteOutputDir, f);
|
|
491
|
+
if (await fs.pathExists(p)) await fs.remove(p);
|
|
492
|
+
}
|
|
493
|
+
for (const d of legacyDirs) {
|
|
494
|
+
const p = path.join(absoluteOutputDir, d);
|
|
495
|
+
if (await fs.pathExists(p)) await fs.remove(p);
|
|
496
|
+
}
|
|
497
|
+
|
|
352
498
|
await generateConfigLoader(absoluteOutputDir);
|
|
353
499
|
await generateLoggerCode(config, absoluteOutputDir);
|
|
354
500
|
await generateMQTTCode(config, absoluteOutputDir);
|
|
@@ -357,48 +503,138 @@ program
|
|
|
357
503
|
await generateTelemetryCode(config, absoluteOutputDir);
|
|
358
504
|
await generateDeploymentArtifacts(config, absoluteOutputDir);
|
|
359
505
|
await generateYAMLConfigs(config, absoluteOutputDir);
|
|
360
|
-
const { entities, relationships, enums } = await generateEntities(path.
|
|
506
|
+
const { entities, relationships, enums, openEntities } = await generateEntities(path.resolve(process.cwd(), gdlDir), absoluteOutputDir, config);
|
|
361
507
|
await generateKratosCode(entities, absoluteOutputDir, config.name, enums);
|
|
362
508
|
|
|
363
509
|
await generateRepositoryCode(absoluteOutputDir);
|
|
510
|
+
|
|
511
|
+
{
|
|
512
|
+
const migratorTemplatePath = path.resolve(path.dirname(import.meta.url.replace('file://', '')), 'templates/go/migrator.go.hbs');
|
|
513
|
+
if (await fs.pathExists(migratorTemplatePath)) {
|
|
514
|
+
const migratorTemplate = Handlebars.compile(await fs.readFile(migratorTemplatePath, 'utf8'));
|
|
515
|
+
await fs.writeFile(path.join(absoluteOutputDir, 'migrations/migrations.go'), migratorTemplate({ app_name: config.name }));
|
|
516
|
+
}
|
|
517
|
+
}
|
|
364
518
|
|
|
365
519
|
await generateGraphQLCode(config, entities, relationships, absoluteOutputDir, enums);
|
|
366
|
-
|
|
520
|
+
await generatePostgRESTCode(config, absoluteOutputDir);
|
|
521
|
+
if (config.multitenancy?.enabled) await generateMultitenancy(config, absoluteOutputDir, entities);
|
|
367
522
|
await generateAuditCode(config, absoluteOutputDir);
|
|
368
523
|
await generateMeteringCode(config, absoluteOutputDir);
|
|
369
524
|
await generateSecurityMiddleware(config, absoluteOutputDir);
|
|
370
525
|
await generateWebSocketCode(config, entities, absoluteOutputDir);
|
|
371
|
-
await
|
|
372
|
-
|
|
526
|
+
await generateOutbox(absoluteOutputDir, config);
|
|
527
|
+
await generateNATSCode(config, absoluteOutputDir);
|
|
528
|
+
await generateBrokerCode(config, absoluteOutputDir);
|
|
529
|
+
await generateStorageCode(config, absoluteOutputDir, path.dirname(path.resolve(process.cwd(), configPath)));
|
|
530
|
+
await generateRouterCode(absoluteOutputDir, config, entities, openEntities);
|
|
531
|
+
await generateElasticsearchLayer(config, entities, absoluteOutputDir);
|
|
532
|
+
await generateServerlessHandler(absoluteOutputDir, config);
|
|
533
|
+
|
|
534
|
+
console.log(chalk.green('\nš Go-Duck project generated successfully!'));
|
|
373
535
|
|
|
374
536
|
// 8. Generate Swagger Docs
|
|
375
|
-
await generateSwaggerDocs(config, entities, absoluteOutputDir);
|
|
376
|
-
|
|
537
|
+
await generateSwaggerDocs(config, entities, absoluteOutputDir, openEntities);
|
|
538
|
+
await generatePostmanCollection(config, entities, absoluteOutputDir, openEntities);
|
|
539
|
+
console.log(chalk.green('ā
Swagger API and Postman Collection generated!'));
|
|
377
540
|
|
|
378
541
|
// 8.5 Generate Web Docs App
|
|
379
|
-
await generateDocumentation(config, entities, absoluteOutputDir, enums);
|
|
542
|
+
await generateDocumentation(config, entities, absoluteOutputDir, enums, openEntities);
|
|
380
543
|
console.log(chalk.green('ā
Web Documentation App generated!'));
|
|
381
544
|
|
|
545
|
+
// 8.6 Generate AI/LLM Blueprint Documentation
|
|
546
|
+
await generateAIDocs(config, entities, absoluteOutputDir, enums, openEntities);
|
|
547
|
+
console.log(chalk.green('š¤ System-level AI Blueprint Documentation generated!'));
|
|
548
|
+
|
|
382
549
|
// 9. Generate main.go
|
|
383
550
|
const mainTemplatePath = path.resolve(path.dirname(import.meta.url.replace('file://', '')), 'templates/go/main.go.hbs');
|
|
384
551
|
if (await fs.pathExists(mainTemplatePath)) {
|
|
385
552
|
const mainTemplateSource = await fs.readFile(mainTemplatePath, 'utf8');
|
|
386
553
|
const mainTemplate = Handlebars.compile(mainTemplateSource);
|
|
387
|
-
await fs.writeFile(path.join(absoluteOutputDir, 'main.go'), mainTemplate({ app_name: config.name, entities }));
|
|
554
|
+
await fs.writeFile(path.join(absoluteOutputDir, 'main.go'), mainTemplate({ app_name: config.name, entities, openEntities }));
|
|
388
555
|
console.log(chalk.green('ā
main.go entry point created!'));
|
|
389
556
|
}
|
|
557
|
+
|
|
558
|
+
// 10. Generate go.mod
|
|
559
|
+
const goModContent = `module ${config.name}
|
|
560
|
+
|
|
561
|
+
go 1.24
|
|
562
|
+
|
|
563
|
+
require (
|
|
564
|
+
github.com/gin-gonic/gin v1.10.0
|
|
565
|
+
github.com/go-kratos/kratos/v2 v2.8.2
|
|
566
|
+
github.com/google/uuid v1.6.0
|
|
567
|
+
github.com/spf13/viper v1.19.0
|
|
568
|
+
go.opentelemetry.io/otel v1.26.0
|
|
569
|
+
go.opentelemetry.io/otel/trace v1.26.0
|
|
570
|
+
go.opentelemetry.io/otel/sdk v1.26.0
|
|
571
|
+
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.51.0
|
|
572
|
+
google.golang.org/grpc v1.64.0
|
|
573
|
+
gorm.io/datatypes v1.2.0
|
|
574
|
+
gorm.io/driver/postgres v1.5.7
|
|
575
|
+
gorm.io/gorm v1.25.10
|
|
576
|
+
gorm.io/plugin/opentelemetry v0.1.5
|
|
577
|
+
github.com/golang-jwt/jwt/v5 v5.2.1
|
|
578
|
+
github.com/eclipse/paho.mqtt.golang v1.4.3
|
|
579
|
+
github.com/sony/gobreaker v0.5.0
|
|
580
|
+
github.com/redis/go-redis/v9 v9.5.1
|
|
581
|
+
github.com/gorilla/websocket v1.5.1
|
|
582
|
+
github.com/99designs/gqlgen v0.17.44
|
|
583
|
+
github.com/vektah/gqlparser/v2 v2.5.11
|
|
584
|
+
github.com/aws/aws-lambda-go v1.47.0
|
|
585
|
+
github.com/awslabs/aws-lambda-go-api-proxy v0.16.1
|
|
586
|
+
github.com/GoogleCloudPlatform/functions-framework-go v1.8.1
|
|
587
|
+
go.mongodb.org/mongo-driver v1.17.1
|
|
588
|
+
github.com/gin-contrib/cors v1.7.1
|
|
589
|
+
github.com/go-resty/resty/v2 v2.16.0
|
|
590
|
+
github.com/olivere/elastic/v7 v7.0.32
|
|
591
|
+
github.com/nats-io/nats.go v1.38.0
|
|
592
|
+
)
|
|
593
|
+
`;
|
|
594
|
+
await fs.writeFile(path.join(absoluteOutputDir, 'go.mod'), goModContent);
|
|
595
|
+
console.log(chalk.green('ā
go.mod file created!'));
|
|
596
|
+
|
|
597
|
+
// Post-generation: Run go mod tidy
|
|
598
|
+
try {
|
|
599
|
+
console.log(chalk.blue('Cleaning up Go modules (go mod tidy)...'));
|
|
600
|
+
execSync('go mod tidy', { cwd: absoluteOutputDir, stdio: 'inherit' });
|
|
601
|
+
console.log(chalk.green('ā
Go modules tidied!'));
|
|
602
|
+
} catch (error) {
|
|
603
|
+
console.warn(chalk.yellow('ā ļø Could not run go mod tidy. Please run it manually.'));
|
|
604
|
+
}
|
|
605
|
+
|
|
390
606
|
console.log(chalk.bold.magenta('\n⨠Project created successfully!'));
|
|
391
607
|
});
|
|
392
608
|
|
|
393
609
|
program
|
|
394
|
-
.command('import-gdl <
|
|
395
|
-
.description('Import entities from a GDL file into an existing app')
|
|
610
|
+
.command('import-gdl <path>')
|
|
611
|
+
.description('Import entities from a GDL file or directory into an existing app')
|
|
396
612
|
.option('-o, --output <path>', 'Path to the existing app root', '.')
|
|
397
|
-
.action(async (
|
|
613
|
+
.action(async (gdlPath, options) => {
|
|
398
614
|
const absoluteOutputDir = path.resolve(process.cwd(), options.output);
|
|
399
|
-
console.log(chalk.blue(`š„ Importing GDL from ${
|
|
615
|
+
console.log(chalk.blue(`š„ Importing GDL from ${gdlPath}...`));
|
|
616
|
+
|
|
617
|
+
let configPath = path.resolve(process.cwd(), './.go-duck/config.yaml');
|
|
618
|
+
if (!await fs.pathExists(configPath)) {
|
|
619
|
+
configPath = path.resolve(process.cwd(), './CONFIG/config.yaml');
|
|
620
|
+
}
|
|
621
|
+
if (!await fs.pathExists(configPath)) {
|
|
622
|
+
configPath = path.resolve(process.cwd(), '../CONFIG/config.yaml');
|
|
623
|
+
}
|
|
624
|
+
const config = await loadConfig(configPath);
|
|
625
|
+
|
|
626
|
+
// Cleanup legacy files if they exist in the root
|
|
627
|
+
const legacyFiles = ['Dockerfile', 'docker-compose.yml'];
|
|
628
|
+
const legacyDirs = ['k8s', 'realm-config'];
|
|
629
|
+
for (const f of legacyFiles) {
|
|
630
|
+
const p = path.join(absoluteOutputDir, f);
|
|
631
|
+
if (await fs.pathExists(p)) await fs.remove(p);
|
|
632
|
+
}
|
|
633
|
+
for (const d of legacyDirs) {
|
|
634
|
+
const p = path.join(absoluteOutputDir, d);
|
|
635
|
+
if (await fs.pathExists(p)) await fs.remove(p);
|
|
636
|
+
}
|
|
400
637
|
|
|
401
|
-
const config = await loadConfig(path.resolve(process.cwd(), '../CONFIG/config.yaml'));
|
|
402
638
|
await generateConfigLoader(absoluteOutputDir);
|
|
403
639
|
await generateLoggerCode(config, absoluteOutputDir);
|
|
404
640
|
await generateMQTTCode(config, absoluteOutputDir);
|
|
@@ -406,34 +642,52 @@ program
|
|
|
406
642
|
await generateResilienceCode(config, absoluteOutputDir);
|
|
407
643
|
await generateTelemetryCode(config, absoluteOutputDir);
|
|
408
644
|
await generateDeploymentArtifacts(config, absoluteOutputDir);
|
|
409
|
-
const { entities, relationships, enums } = await generateEntities(
|
|
645
|
+
const { entities, relationships, enums, openEntities } = await generateEntities(gdlPath, absoluteOutputDir, config);
|
|
410
646
|
await generateKratosCode(entities, absoluteOutputDir, config.name, enums);
|
|
411
647
|
|
|
412
648
|
await generateRepositoryCode(absoluteOutputDir);
|
|
413
649
|
|
|
414
650
|
await generateGraphQLCode(config, entities, relationships, absoluteOutputDir, enums);
|
|
415
|
-
// Sync PostgREST search as well
|
|
416
651
|
await generatePostgRESTCode(config, absoluteOutputDir);
|
|
417
652
|
|
|
418
|
-
// Sync Security
|
|
653
|
+
// Sync Search & Security
|
|
654
|
+
await generateElasticsearchLayer(config, entities, absoluteOutputDir);
|
|
419
655
|
await generateSecurityMiddleware(config, absoluteOutputDir);
|
|
420
656
|
|
|
421
657
|
// Sync WebSocket
|
|
422
658
|
await generateWebSocketCode(config, entities, absoluteOutputDir);
|
|
423
659
|
|
|
424
660
|
// Sync Swagger Docs
|
|
425
|
-
await generateSwaggerDocs(config, entities, absoluteOutputDir);
|
|
661
|
+
await generateSwaggerDocs(config, entities, absoluteOutputDir, openEntities);
|
|
662
|
+
await generatePostmanCollection(config, entities, absoluteOutputDir, openEntities);
|
|
426
663
|
|
|
427
664
|
// Sync Web Docs App
|
|
428
|
-
await generateDocumentation(config, entities, absoluteOutputDir, enums);
|
|
665
|
+
await generateDocumentation(config, entities, absoluteOutputDir, enums, openEntities);
|
|
666
|
+
|
|
667
|
+
// Sync AI Blueprints
|
|
668
|
+
await generateAIDocs(config, entities, absoluteOutputDir, enums, openEntities);
|
|
669
|
+
// Sync Serverless & Router
|
|
670
|
+
await generateRouterCode(absoluteOutputDir, config, entities, openEntities);
|
|
671
|
+
await generateServerlessHandler(absoluteOutputDir, config);
|
|
429
672
|
|
|
430
673
|
// Regenerate main.go to include new routes if any (or just entities)
|
|
674
|
+
const timestamp = new Date().toISOString().replace(/[-:T]/g, '').split('.')[0];
|
|
431
675
|
const mainTemplatePath = path.resolve(path.dirname(import.meta.url.replace('file://', '')), 'templates/go/main.go.hbs');
|
|
432
676
|
if (await fs.pathExists(mainTemplatePath)) {
|
|
433
677
|
const mainTemplate = Handlebars.compile(await fs.readFile(mainTemplatePath, 'utf8'));
|
|
434
|
-
await fs.writeFile(path.join(absoluteOutputDir, 'main.go'), mainTemplate({ app_name: config.name, entities }));
|
|
678
|
+
await fs.writeFile(path.join(absoluteOutputDir, 'main.go'), mainTemplate({ app_name: config.name, entities, openEntities, timestamp }));
|
|
435
679
|
console.log(chalk.green('ā
Updated main.go to register new entity routes.'));
|
|
436
680
|
}
|
|
681
|
+
|
|
682
|
+
// Run go mod tidy after import as well
|
|
683
|
+
try {
|
|
684
|
+
console.log(chalk.blue('Cleaning up Go modules (go mod tidy)...'));
|
|
685
|
+
execSync('go mod tidy', { cwd: absoluteOutputDir, stdio: 'inherit' });
|
|
686
|
+
console.log(chalk.green('ā
Go modules tidied!'));
|
|
687
|
+
} catch (error) {
|
|
688
|
+
console.warn(chalk.yellow('ā ļø Could not run go mod tidy. Please run it manually.'));
|
|
689
|
+
}
|
|
690
|
+
|
|
437
691
|
console.log(chalk.bold.magenta('\n⨠GDL Import Completed with Incremental Migrations! āØ'));
|
|
438
692
|
});
|
|
439
693
|
|
|
@@ -447,74 +701,159 @@ const generateYAMLConfigs = async (config, outputDir) => {
|
|
|
447
701
|
const extendedConfig = {
|
|
448
702
|
...cleanConfig,
|
|
449
703
|
server: {
|
|
450
|
-
port: 8080,
|
|
451
|
-
'read-timeout': '30s',
|
|
452
|
-
'write-timeout': '30s',
|
|
704
|
+
port: cleanConfig.server?.port || 8080,
|
|
705
|
+
'read-timeout': cleanConfig.server?.['read-timeout'] || '30s',
|
|
706
|
+
'write-timeout': cleanConfig.server?.['write-timeout'] || '30s',
|
|
453
707
|
grpc: {
|
|
454
|
-
addr: ':9000',
|
|
455
|
-
network: 'tcp',
|
|
456
|
-
timeout: '1s'
|
|
708
|
+
addr: cleanConfig.server?.grpc?.addr || ':9000',
|
|
709
|
+
network: cleanConfig.server?.grpc?.network || 'tcp',
|
|
710
|
+
timeout: cleanConfig.server?.grpc?.timeout || '1s'
|
|
457
711
|
},
|
|
458
712
|
cors: {
|
|
459
|
-
'allow-origins': ['*'],
|
|
460
|
-
'allow-methods': ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
461
|
-
'allow-headers': ['Origin', 'Content-Type', 'Accept', 'Authorization']
|
|
713
|
+
'allow-origins': cleanConfig.server?.cors?.['allow-origins'] || ['*'],
|
|
714
|
+
'allow-methods': cleanConfig.server?.cors?.['allow-methods'] || ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
715
|
+
'allow-headers': cleanConfig.server?.cors?.['allow-headers'] || ['Origin', 'Content-Type', 'Accept', 'Authorization', 'X-Tenant-ID']
|
|
462
716
|
}
|
|
463
717
|
},
|
|
464
718
|
datasource: {
|
|
465
719
|
...cleanConfig.datasource,
|
|
466
|
-
'
|
|
467
|
-
'max-
|
|
468
|
-
'
|
|
720
|
+
'ssl-mode': cleanConfig.datasource?.['ssl-mode'] || 'disable',
|
|
721
|
+
'max-open-conns': cleanConfig.datasource?.['max-open-conns'] || 25,
|
|
722
|
+
'max-idle-conns': cleanConfig.datasource?.['max-idle-conns'] || 10,
|
|
723
|
+
'conn-max-lifetime': cleanConfig.datasource?.['conn-max-lifetime'] || '5m',
|
|
724
|
+
mongodb: {
|
|
725
|
+
enabled: cleanConfig.datasource?.mongodb?.enabled || false,
|
|
726
|
+
uri: cleanConfig.datasource?.mongodb?.uri || 'mongodb://localhost:27017',
|
|
727
|
+
database: cleanConfig.datasource?.mongodb?.database || cleanConfig.name || 'go_duck_mongo'
|
|
728
|
+
}
|
|
469
729
|
},
|
|
470
730
|
security: {
|
|
471
731
|
...cleanConfig.security,
|
|
732
|
+
'keycloak-app-client-id': cleanConfig.security?.['keycloak-app-client-id'] || `${cleanConfig.name || 'go-duck'}-app`,
|
|
733
|
+
'keycloak-app-client-secret': cleanConfig.security?.['keycloak-app-client-secret'] || 'app-secret-123',
|
|
734
|
+
'keycloak-service-client-id': cleanConfig.security?.['keycloak-service-client-id'] || `${cleanConfig.name || 'go-duck'}-service`,
|
|
735
|
+
'keycloak-service-secret': cleanConfig.security?.['keycloak-service-secret'] || 'service-secret-123',
|
|
736
|
+
'keycloak-admin-client-id': cleanConfig.security?.['keycloak-admin-client-id'] || 'admin-cli',
|
|
737
|
+
'keycloak-admin-secret': cleanConfig.security?.['keycloak-admin-secret'] || 'admin-secret-123',
|
|
738
|
+
'super-admin-role': cleanConfig.security?.['super-admin-role'] || 'admin',
|
|
739
|
+
'confidential-mode': cleanConfig.security?.['confidential-mode'] ?? true,
|
|
472
740
|
'rate-limit': {
|
|
473
|
-
rps: 100,
|
|
474
|
-
burst: 200
|
|
741
|
+
rps: cleanConfig.security?.['rate-limit']?.rps || 100,
|
|
742
|
+
burst: cleanConfig.security?.['rate-limit']?.burst || 200
|
|
475
743
|
}
|
|
476
744
|
},
|
|
477
745
|
logging: {
|
|
478
746
|
datadog: {
|
|
479
|
-
enabled: false,
|
|
480
|
-
'api-key': 'YOUR_DATADOG_API_KEY',
|
|
481
|
-
site: 'datadoghq.com',
|
|
482
|
-
service: cleanConfig.name || 'go-duck-service'
|
|
747
|
+
enabled: cleanConfig.logging?.datadog?.enabled || false,
|
|
748
|
+
'api-key': cleanConfig.logging?.datadog?.['api-key'] || 'YOUR_DATADOG_API_KEY',
|
|
749
|
+
site: cleanConfig.logging?.datadog?.site || 'datadoghq.com',
|
|
750
|
+
service: cleanConfig.logging?.datadog?.service || cleanConfig.name || 'go-duck-service'
|
|
483
751
|
}
|
|
484
752
|
},
|
|
485
753
|
messaging: {
|
|
486
754
|
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'
|
|
755
|
+
enabled: cleanConfig.messaging?.mqtt?.enabled || false,
|
|
756
|
+
broker: cleanConfig.messaging?.mqtt?.broker || 'tcp://localhost:1883',
|
|
757
|
+
'client-id': cleanConfig.messaging?.mqtt?.['client-id'] || (cleanConfig.name || 'go-duck') + '-dev',
|
|
758
|
+
username: cleanConfig.messaging?.mqtt?.username || 'dev_user',
|
|
759
|
+
password: cleanConfig.messaging?.mqtt?.password || 'dev_password',
|
|
760
|
+
'topic-prefix': cleanConfig.messaging?.mqtt?.['topic-prefix'] || 'go-duck/events'
|
|
493
761
|
}
|
|
494
762
|
},
|
|
495
763
|
cache: {
|
|
496
764
|
redis: {
|
|
497
|
-
enabled: false,
|
|
498
|
-
host: 'localhost:6379',
|
|
499
|
-
password: '',
|
|
500
|
-
db: 0,
|
|
501
|
-
ttl: '10m'
|
|
765
|
+
enabled: cleanConfig.cache?.redis?.enabled || false,
|
|
766
|
+
host: cleanConfig.cache?.redis?.host || 'localhost:6379',
|
|
767
|
+
password: cleanConfig.cache?.redis?.password || '',
|
|
768
|
+
db: cleanConfig.cache?.redis?.db || 0,
|
|
769
|
+
ttl: cleanConfig.cache?.redis?.ttl || '10m'
|
|
502
770
|
}
|
|
503
771
|
},
|
|
504
772
|
resilience: {
|
|
505
773
|
'circuit-breaker': {
|
|
506
|
-
enabled: true,
|
|
507
|
-
'failure-threshold': 5,
|
|
508
|
-
'success-threshold': 2,
|
|
509
|
-
timeout: '60s'
|
|
774
|
+
enabled: cleanConfig.resilience?.['circuit-breaker']?.enabled ?? true,
|
|
775
|
+
'failure-threshold': cleanConfig.resilience?.['circuit-breaker']?.['failure-threshold'] || 5,
|
|
776
|
+
'success-threshold': cleanConfig.resilience?.['circuit-breaker']?.['success-threshold'] || 2,
|
|
777
|
+
timeout: cleanConfig.resilience?.['circuit-breaker']?.timeout || '60s'
|
|
510
778
|
}
|
|
511
779
|
},
|
|
512
780
|
telemetry: {
|
|
513
781
|
otel: {
|
|
514
|
-
enabled: false,
|
|
515
|
-
endpoint: 'localhost:4317',
|
|
516
|
-
'sampler-ratio': 1.0
|
|
782
|
+
enabled: cleanConfig.telemetry?.otel?.enabled || false,
|
|
783
|
+
endpoint: cleanConfig.telemetry?.otel?.endpoint || 'localhost:4317',
|
|
784
|
+
'sampler-ratio': cleanConfig.telemetry?.otel?.['sampler-ratio'] || 1.0
|
|
785
|
+
}
|
|
786
|
+
},
|
|
787
|
+
storage: {
|
|
788
|
+
s3: {
|
|
789
|
+
enabled: cleanConfig.storage?.s3?.enabled || false,
|
|
790
|
+
bucket: cleanConfig.storage?.s3?.bucket || 'YOUR_S3_BUCKET',
|
|
791
|
+
region: cleanConfig.storage?.s3?.region || 'us-east-1',
|
|
792
|
+
'access-key': cleanConfig.storage?.s3?.['access-key'] || 'YOUR_S3_ACCESS_KEY',
|
|
793
|
+
'secret-key': cleanConfig.storage?.s3?.['secret-key'] || 'YOUR_S3_SECRET_KEY',
|
|
794
|
+
endpoint: cleanConfig.storage?.s3?.endpoint || ''
|
|
795
|
+
},
|
|
796
|
+
gcs: {
|
|
797
|
+
enabled: cleanConfig.storage?.gcs?.enabled || false,
|
|
798
|
+
bucket: cleanConfig.storage?.gcs?.bucket || 'YOUR_GCS_BUCKET',
|
|
799
|
+
'credentials-file': cleanConfig.storage?.gcs?.['credentials-file'] || 'gcs-service-account.json'
|
|
800
|
+
},
|
|
801
|
+
minio: {
|
|
802
|
+
enabled: cleanConfig.storage?.minio?.enabled || false,
|
|
803
|
+
bucket: cleanConfig.storage?.minio?.bucket || 'YOUR_MINIO_BUCKET',
|
|
804
|
+
endpoint: cleanConfig.storage?.minio?.endpoint || 'http://localhost:9000',
|
|
805
|
+
'access-key': cleanConfig.storage?.minio?.['access-key'] || 'YOUR_MINIO_ACCESS_KEY',
|
|
806
|
+
'secret-key': cleanConfig.storage?.minio?.['secret-key'] || 'YOUR_MINIO_SECRET_KEY',
|
|
807
|
+
'use-ssl': cleanConfig.storage?.minio?.['use-ssl'] || false
|
|
808
|
+
},
|
|
809
|
+
r2: {
|
|
810
|
+
enabled: cleanConfig.storage?.r2?.enabled || false,
|
|
811
|
+
bucket: cleanConfig.storage?.r2?.bucket || 'YOUR_R2_BUCKET',
|
|
812
|
+
'account-id': cleanConfig.storage?.r2?.['account-id'] || 'YOUR_R2_ACCOUNT_ID',
|
|
813
|
+
'access-key': cleanConfig.storage?.r2?.['access-key'] || 'YOUR_R2_ACCESS_KEY',
|
|
814
|
+
'secret-key': cleanConfig.storage?.r2?.['secret-key'] || 'YOUR_R2_SECRET_KEY'
|
|
815
|
+
},
|
|
816
|
+
generic: {
|
|
817
|
+
enabled: cleanConfig.storage?.generic?.enabled || false,
|
|
818
|
+
provider: cleanConfig.storage?.generic?.provider || 'S3',
|
|
819
|
+
'access-key': cleanConfig.storage?.generic?.['access-key'] || 'YOUR_ACCESS_KEY',
|
|
820
|
+
'secret-key': cleanConfig.storage?.generic?.['secret-key'] || 'YOUR_SECRET_KEY'
|
|
821
|
+
},
|
|
822
|
+
sftp: {
|
|
823
|
+
enabled: cleanConfig.storage?.sftp?.enabled || false,
|
|
824
|
+
host: cleanConfig.storage?.sftp?.host || 'sftp.example.com',
|
|
825
|
+
port: cleanConfig.storage?.sftp?.port || 22,
|
|
826
|
+
username: cleanConfig.storage?.sftp?.username || 'sftp_user',
|
|
827
|
+
password: cleanConfig.storage?.sftp?.password || 'sftp_password',
|
|
828
|
+
'key-file': cleanConfig.storage?.sftp?.['key-file'] || 'id_rsa',
|
|
829
|
+
'remote-path': cleanConfig.storage?.sftp?.['remote-path'] || '/uploads'
|
|
830
|
+
},
|
|
831
|
+
github: {
|
|
832
|
+
enabled: cleanConfig.storage?.github?.enabled || false,
|
|
833
|
+
owner: cleanConfig.storage?.github?.owner || 'YOUR_ORG',
|
|
834
|
+
repo: cleanConfig.storage?.github?.repo || 'YOUR_REPO',
|
|
835
|
+
branch: cleanConfig.storage?.github?.branch || 'main',
|
|
836
|
+
path: cleanConfig.storage?.github?.path || '/',
|
|
837
|
+
token: cleanConfig.storage?.github?.token || 'YOUR_GITHUB_TOKEN',
|
|
838
|
+
username: cleanConfig.storage?.github?.username || '',
|
|
839
|
+
password: cleanConfig.storage?.github?.password || ''
|
|
517
840
|
}
|
|
841
|
+
},
|
|
842
|
+
serverless: {
|
|
843
|
+
enabled: cleanConfig.serverless?.enabled || false,
|
|
844
|
+
provider: cleanConfig.serverless?.provider || 'aws'
|
|
845
|
+
},
|
|
846
|
+
multitenancy: {
|
|
847
|
+
enabled: cleanConfig.multitenancy?.enabled || false,
|
|
848
|
+
'hide-silo-names': cleanConfig.multitenancy?.['hide-silo-names'] || false
|
|
849
|
+
},
|
|
850
|
+
elasticsearch: {
|
|
851
|
+
enabled: cleanConfig.elasticsearch?.enabled || false,
|
|
852
|
+
addresses: cleanConfig.elasticsearch?.addresses || ['http://localhost:9200'],
|
|
853
|
+
username: cleanConfig.elasticsearch?.username || '',
|
|
854
|
+
password: cleanConfig.elasticsearch?.password || '',
|
|
855
|
+
auto_sync: cleanConfig.elasticsearch?.auto_sync ?? true,
|
|
856
|
+
index_prefix: cleanConfig.elasticsearch?.index_prefix || 'go_duck_'
|
|
518
857
|
}
|
|
519
858
|
};
|
|
520
859
|
|
|
@@ -534,54 +873,55 @@ const generateYAMLConfigs = async (config, outputDir) => {
|
|
|
534
873
|
...extendedConfig.server,
|
|
535
874
|
cors: {
|
|
536
875
|
...extendedConfig.server.cors,
|
|
537
|
-
'allow-origins': ['https://your-domain.com']
|
|
876
|
+
'allow-origins': cleanConfig.server?.cors?.['allow-origins'] || ['https://your-domain.com']
|
|
538
877
|
}
|
|
539
878
|
},
|
|
540
879
|
logging: {
|
|
541
880
|
...extendedConfig.logging,
|
|
542
881
|
datadog: {
|
|
543
882
|
...extendedConfig.logging.datadog,
|
|
544
|
-
enabled: true
|
|
883
|
+
enabled: cleanConfig.logging?.datadog?.enabled ?? true
|
|
545
884
|
}
|
|
546
885
|
},
|
|
547
886
|
messaging: {
|
|
548
887
|
mqtt: {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
password: 'prod_password',
|
|
554
|
-
'topic-prefix': 'go-duck/events'
|
|
888
|
+
...extendedConfig.messaging.mqtt,
|
|
889
|
+
enabled: cleanConfig.messaging?.mqtt?.enabled ?? true,
|
|
890
|
+
broker: cleanConfig.messaging?.mqtt?.broker || 'tcp://mqtt.production.svc:1883',
|
|
891
|
+
'client-id': cleanConfig.messaging?.mqtt?.['client-id'] || cleanConfig.name + '-prod'
|
|
555
892
|
}
|
|
556
893
|
},
|
|
557
894
|
cache: {
|
|
558
895
|
redis: {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
ttl: '1h'
|
|
896
|
+
...extendedConfig.cache.redis,
|
|
897
|
+
enabled: cleanConfig.cache?.redis?.enabled ?? true,
|
|
898
|
+
host: cleanConfig.cache?.redis?.host || 'redis.production.svc:6379',
|
|
899
|
+
ttl: cleanConfig.cache?.redis?.ttl || '1h'
|
|
564
900
|
}
|
|
565
901
|
},
|
|
566
902
|
resilience: {
|
|
567
903
|
'circuit-breaker': {
|
|
568
|
-
|
|
569
|
-
'
|
|
570
|
-
'
|
|
571
|
-
|
|
904
|
+
...extendedConfig.resilience['circuit-breaker'],
|
|
905
|
+
enabled: cleanConfig.resilience?.['circuit-breaker']?.enabled ?? true,
|
|
906
|
+
'failure-threshold': cleanConfig.resilience?.['circuit-breaker']?.['failure-threshold'] || 3,
|
|
907
|
+
'success-threshold': cleanConfig.resilience?.['circuit-breaker']?.['success-threshold'] || 5
|
|
572
908
|
}
|
|
573
909
|
},
|
|
574
910
|
telemetry: {
|
|
575
911
|
otel: {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
'
|
|
912
|
+
...extendedConfig.telemetry.otel,
|
|
913
|
+
enabled: cleanConfig.telemetry?.otel?.enabled ?? true,
|
|
914
|
+
endpoint: cleanConfig.telemetry?.otel?.endpoint || 'otel-collector.monitoring.svc:4317',
|
|
915
|
+
'sampler-ratio': cleanConfig.telemetry?.otel?.['sampler-ratio'] || 0.1
|
|
579
916
|
}
|
|
580
917
|
},
|
|
581
918
|
datasource: {
|
|
582
919
|
...extendedConfig.datasource,
|
|
583
|
-
'
|
|
584
|
-
'
|
|
920
|
+
host: cleanConfig.datasource?.host || 'db.production.svc',
|
|
921
|
+
username: cleanConfig.datasource?.username || 'go_duck_user',
|
|
922
|
+
password: cleanConfig.datasource?.password || 'go_duck_pass',
|
|
923
|
+
'max-open-conns': cleanConfig.datasource?.['max-open-conns'] || 100,
|
|
924
|
+
'max-idle-conns': cleanConfig.datasource?.['max-idle-conns'] || 50
|
|
585
925
|
}
|
|
586
926
|
},
|
|
587
927
|
environment: { active_profile: 'prod' }
|
|
@@ -589,4 +929,41 @@ const generateYAMLConfigs = async (config, outputDir) => {
|
|
|
589
929
|
await fs.writeFile(path.join(outputDir, 'application-prod.yml'), yaml.dump(prodConfig));
|
|
590
930
|
};
|
|
591
931
|
|
|
932
|
+
program
|
|
933
|
+
.command('serve')
|
|
934
|
+
.description('Birth of GO-DUCK: Start the interactive GUI and Documentation server on Port 2026')
|
|
935
|
+
.action(async () => {
|
|
936
|
+
const port = 2026;
|
|
937
|
+
const app = express();
|
|
938
|
+
const tempDir = path.join(process.cwd(), '.go_duck_birth');
|
|
939
|
+
|
|
940
|
+
console.log(chalk.blue('š¦ Initializing Birth of GO-DUCK (Industrial GUI)...'));
|
|
941
|
+
|
|
942
|
+
// Load default config and dummy data for documentation preview
|
|
943
|
+
const config = { name: 'go-duck-preview' };
|
|
944
|
+
const entities = [];
|
|
945
|
+
const enums = [];
|
|
946
|
+
const openEntities = [];
|
|
947
|
+
|
|
948
|
+
await fs.ensureDir(tempDir);
|
|
949
|
+
await generateDocumentation(config, entities, tempDir, enums, openEntities);
|
|
950
|
+
|
|
951
|
+
const docsPath = path.join(tempDir, 'docs', 'web');
|
|
952
|
+
app.use(express.static(docsPath));
|
|
953
|
+
|
|
954
|
+
app.listen(port, async () => {
|
|
955
|
+
console.log(chalk.bold.green(`\nš Birth of GO-DUCK Server is ALIVE!`));
|
|
956
|
+
console.log(chalk.cyan(`š Documentation & Config Wizard: http://localhost:${port}`));
|
|
957
|
+
console.log(chalk.gray(`Press Ctrl+C to terminate the server.\n`));
|
|
958
|
+
|
|
959
|
+
await open(`http://localhost:${port}/wizard.html`);
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
// Cleanup temp files on exit
|
|
963
|
+
process.on('SIGINT', async () => {
|
|
964
|
+
await fs.remove(tempDir);
|
|
965
|
+
process.exit();
|
|
966
|
+
});
|
|
967
|
+
});
|
|
968
|
+
|
|
592
969
|
program.parse(process.argv);
|