go-duck-cli 1.3.2 → 1.3.5

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 CHANGED
@@ -59,8 +59,9 @@ GO-DUCK has officially reached the **410% Achievement Status**, evolving from a
59
59
  | **Silo Discovery & Privacy Proxy** | 🚀 **ELITE (+10%)** | Silo discovery API with physical DB name masking. |
60
60
  | **Universal Storage Mesh** | 🚀 **ELITE (+25%)** | Dynamic Hot-Swapping Registry and Distributed Cross-Scan API retrieval. |
61
61
  | **WSO2 API Gateway Integration** | 🚀 **ELITE (+15%)** | Automated OpenAPI registration & proxy mapping. |
62
+ | **API Gateway Standards & Swagger UI** | 🚀 **ELITE (+10%)** | Keycloak SSO, Glassmorphism UI, JHipster `/v3/api-docs` compliance. |
62
63
  | **Full-Spectrum GDL Evolution** | 🚀 **ELITE (+15%)** | Native DROP/ALTER migrations with dead-code purging. |
63
- | **TOTAL ACHIEVEMENT STATUS** | 🏆 **410%** | **ELITE STATUS CONFIRMED.** 👑 |
64
+ | **TOTAL ACHIEVEMENT STATUS** | 🏆 **420%** | **ELITE STATUS CONFIRMED.** 👑 |
64
65
 
65
66
  ### ✨ Primary Features (The 410% Core)
66
67
 
@@ -75,6 +76,8 @@ GO-DUCK has officially reached the **410% Achievement Status**, evolving from a
75
76
  * **Distributed Saga Consistency**: Integrated **Transactional Outbox** pattern and background workers in every silo to guarantee eventual consistency across the federation.
76
77
  * **Zero-Trust Identity Registry**: Decoupled mapping layer ensuring physical database names and internal IDs never leak to the client.
77
78
  * **Universal Storage Mesh**: Dynamic Multi-Provider Registry allowing hot-swapping at runtime via `?provider=` queries, alongside Distributed Cross-Scan endpoints to auto-locate files across AWS, GCS, SFTP, and GitHub lakes.
79
+ * **Enterprise API Gateway Compatibility**: Natively exposes the Swagger/OpenAPI JSON specification at `/v3/api-docs` to ensure drop-in compatibility with **JHipster**, **WSO2 API Manager**, and Spring Boot ecosystems.
80
+ * **Next-Gen Swagger UI**: A fully glassmorphism-styled Swagger UI with seamless Keycloak SSO integration. Features automatic JWT token refresh, dynamic `X-Tenant-ID` header injection, and CLI metadata branding. Now features a **Live Interactive MQTT Dictionary**, allowing users to instantly subscribe or publish payloads directly from the docs via WebSockets!
78
81
  * **Spring-style Elasticsearch Search**: Real-time sync for entities marked with `@Searchable`, supporting native `query_string` syntax (wildcards like `*`, booleans, ranges, and fuzzy matching).
79
82
  * **SaaS Quota Engine**: Redis-backed API bandwidth tracking with dynamic, hierarchical limits (User vs. Role mapping).
80
83
  * **Resilience Layer**: Sony/Gobreaker Integration + Zero-Trust Distributed Redis Rate Limiter.
@@ -5,6 +5,9 @@ export const generateAIDocs = async (config, entities, outputDir, enums, openEnt
5
5
  const aiDocsDir = path.join(outputDir, 'docs', 'ai');
6
6
  await fs.ensureDir(aiDocsDir);
7
7
 
8
+ const apiPrefixRaw = config.server?.rest?.['api-path-prefix'] || '/api';
9
+ const apiPrefix = apiPrefixRaw.endsWith('/') ? apiPrefixRaw.slice(0, -1) : apiPrefixRaw;
10
+
8
11
  // 1. ARCHITECTURE.md
9
12
  const appName = config.name || 'go-duck-app';
10
13
  const hasMongo = config.datasource?.mongodb?.enabled;
@@ -32,7 +35,7 @@ export const generateAIDocs = async (config, entities, outputDir, enums, openEnt
32
35
  archContent += `- **Active**: ${storageActive ? 'Yes' : 'No'}\n`;
33
36
  if (storageActive) {
34
37
  archContent += `- **Enabled Nodes**: ${Object.keys(config.storage).filter(k => config.storage[k]?.enabled).join(', ')}\n`;
35
- archContent += `- **Endpoints**: \n - Upload: \`POST /api/storage/upload?provider=\`\n - Exact Retrieve: \`GET /api/storage/download/*key?provider=\`\n - Cross-Scan Locate: \`GET /api/storage/scan/*key\`\n`;
38
+ archContent += `- **Endpoints**: \n - Upload: \`POST ${apiPrefix}/storage/upload?provider=\`\n - Exact Retrieve: \`GET ${apiPrefix}/storage/download/*key?provider=\`\n - Cross-Scan Locate: \`GET ${apiPrefix}/storage/scan/*key\`\n`;
36
39
  }
37
40
  const bootstrapActive = config.storage?.bootstrap?.enabled;
38
41
  if (bootstrapActive) {
@@ -53,25 +56,25 @@ export const generateAIDocs = async (config, entities, outputDir, enums, openEnt
53
56
 
54
57
  // 2. ENDPOINTS.md
55
58
  let endpointsContent = `# REST & gRPC API Surface\n\n`;
56
- endpointsContent += `## Base Path: \`/api\`\n\n`;
59
+ endpointsContent += `## Base Path: \`${apiPrefix}\`\n\n`;
57
60
  endpointsContent += `### Standard Entity Endpoints\n`;
58
61
  for (const entity of entities) {
59
62
  const routeName = entity.name.toLowerCase() + 's';
60
63
  endpointsContent += `\n#### ${entity.name}\n`;
61
- endpointsContent += `- \`GET /api/${routeName}\` (Pagination & dynamic sorting, e.g. \`?page=1&size=10&eager=true&sort=id,asc\`)\n`;
62
- endpointsContent += `- \`GET /api/${routeName}/:id\`\n`;
63
- endpointsContent += `- \`POST /api/${routeName}\`\n`;
64
- endpointsContent += `- \`PUT /api/${routeName}/:id\`\n`;
65
- endpointsContent += `- \`DELETE /api/${routeName}/:id\`\n`;
66
- endpointsContent += `- \`POST /api/${routeName}/bulk\` (Bulk Create/Update)\n`;
64
+ endpointsContent += `- \`GET ${apiPrefix}/${routeName}\` (Pagination & dynamic sorting, e.g. \`?page=1&size=10&eager=true&sort=id,asc\`)\n`;
65
+ endpointsContent += `- \`GET ${apiPrefix}/${routeName}/:id\`\n`;
66
+ endpointsContent += `- \`POST ${apiPrefix}/${routeName}\`\n`;
67
+ endpointsContent += `- \`PUT ${apiPrefix}/${routeName}/:id\`\n`;
68
+ endpointsContent += `- \`DELETE ${apiPrefix}/${routeName}/:id\`\n`;
69
+ endpointsContent += `- \`POST ${apiPrefix}/${routeName}/bulk\` (Bulk Create/Update)\n`;
67
70
  if (entity.isSearchable) {
68
- endpointsContent += `- \`GET /api/search/${routeName}?q={query}\` (Elasticsearch)\n`;
71
+ endpointsContent += `- \`GET ${apiPrefix}/search/${routeName}?q={query}\` (Elasticsearch)\n`;
69
72
  }
70
73
  }
71
74
 
72
75
  endpointsContent += `\n## Infrastructure & Management APIs (SuperAdmin Protected)\n`;
73
- endpointsContent += `- \`GET /api/admin/audit\` (Fetches Global Delta logs from the Centralized Audit Engine)\n`;
74
- endpointsContent += `- \`POST /api/admin/metering/limit\` (Set SaaS Quota limits)\n`;
76
+ endpointsContent += `- \`GET ${apiPrefix}/admin/audit\` (Fetches Global Delta logs from the Centralized Audit Engine)\n`;
77
+ endpointsContent += `- \`POST ${apiPrefix}/admin/metering/limit\` (Set SaaS Quota limits)\n`;
75
78
  endpointsContent += `- \`POST /management/tenant/assign\` (Dynamically initialize new Tenant DBs)\n`;
76
79
 
77
80
  endpointsContent += `\n## Open APIs (No JWT required)\n`;
@@ -2,6 +2,11 @@ import fs from 'fs-extra';
2
2
  import path from 'path';
3
3
  import chalk from 'chalk';
4
4
 
5
+ const toKebabCase = (str) => {
6
+ if (!str) return '';
7
+ return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
8
+ };
9
+
5
10
  export const generateConfigLoader = async (outputDir, configObj) => {
6
11
  const configDir = path.join(outputDir, 'config');
7
12
  await fs.ensureDir(configDir);
@@ -243,7 +248,7 @@ func LoadConfig() (*Config, error) {
243
248
  // Default values
244
249
  v.SetDefault("go-duck.server.rest.port", 8080)
245
250
  v.SetDefault("go-duck.server.rest.protocol", "json")
246
- v.SetDefault("go-duck.server.rest.api-path-prefix", "/${configObj.name || 'api'}/api")
251
+ v.SetDefault("go-duck.server.rest.api-path-prefix", "/${toKebabCase(configObj.name) || 'api'}/api")
247
252
  v.SetDefault("go-duck.security.rate-limit.rps", 100.0)
248
253
  v.SetDefault("go-duck.security.rate-limit.burst", 200)
249
254
  v.SetDefault("go-duck.logging.datadog.enabled", false)
@@ -13,10 +13,15 @@ export const generateDeploymentArtifacts = async (config, projectRootDir) => {
13
13
  await fs.ensureDir(keycloakDir);
14
14
  await fs.ensureDir(githubDir);
15
15
 
16
+ const toKebabCase = (str) => {
17
+ if (!str) return '';
18
+ return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
19
+ };
16
20
  const appName = config.name || 'go-duck';
21
+ const k8sAppName = toKebabCase(appName);
17
22
  const parts = appName.split(/[-_]/);
18
23
  const shortName = (parts.length > 1 ? parts.map(w => w[0]).join('') : appName).substring(0, 10).toLowerCase() || 'goduck';
19
- const appPort = config.server?.port || 8080;
24
+ const appPort = config.server?.rest?.port || 8080;
20
25
  const keycloakHost = config.security?.['keycloak-host'] || 'http://localhost:8180';
21
26
  const keycloakPort = keycloakHost.includes(':') ? keycloakHost.split(':').pop() : '8080';
22
27
 
@@ -75,9 +80,11 @@ FROM gcr.io/distroless/static-debian12
75
80
  WORKDIR /app
76
81
 
77
82
  COPY --from=builder /app/server .
83
+
78
84
  COPY --from=builder /app/application.yml .
79
85
  COPY --from=builder /app/application-dev.yml .
80
86
  COPY --from=builder /app/application-prod.yml .
87
+ COPY --from=builder /app/docs/ /app/docs/
81
88
 
82
89
  EXPOSE ${appPort}
83
90
  ENV GO_PROFILE=prod
@@ -210,10 +217,26 @@ services:
210
217
  - GO_DUCK_DATASOURCE_PASSWORD=${config.datasource?.password || 'password'}
211
218
  - GO_DUCK_DATASOURCE_DATABASE=${config.datasource?.database || 'go_duck_master'}
212
219
  - GO_DUCK_DATASOURCE_PORT=5432
220
+ - GO_DUCK_DATASOURCE_MONGODB_ENABLED=${config.datasource?.mongodb?.enabled ? 'true' : 'false'}
221
+ - GO_DUCK_DATASOURCE_MONGODB_URI=mongodb://mongodb:27017
222
+ - GO_DUCK_DATASOURCE_MONGODB_DATABASE=${config.datasource?.mongodb?.database || 'goduck_document_store'}
213
223
  - GO_DUCK_CACHE_REDIS_HOST=redis:6379
224
+ - GO_DUCK_CACHE_REDIS_PASSWORD=${config.cache?.redis?.password || ''}
214
225
  - GO_DUCK_MESSAGING_MQTT_BROKER=tcp://mosquitto:1883
226
+ - GO_DUCK_MESSAGING_MQTT_USERNAME=${config.messaging?.mqtt?.username || 'dev_user'}
227
+ - GO_DUCK_MESSAGING_MQTT_PASSWORD=${config.messaging?.mqtt?.password || 'dev_password'}
215
228
  - GO_DUCK_TELEMETRY_OTEL_ENDPOINT=otel-collector:4317
216
229
  - GO_DUCK_ELASTICSEARCH_ADDRESSES=http://elasticsearch:9200
230
+ - GO_DUCK_ELASTICSEARCH_USERNAME=${config.elasticsearch?.username || 'elastic'}
231
+ - GO_DUCK_ELASTICSEARCH_PASSWORD=${config.elasticsearch?.password || 'changeme'}
232
+ - GO_DUCK_SECURITY_KEYCLOAK_HOST=http://keycloak:8080
233
+ - GO_DUCK_SECURITY_KEYCLOAK_REALM=${config.security?.['keycloak-realm'] || 'go-duck-realm'}
234
+ - GO_DUCK_SECURITY_KEYCLOAK_APP_CLIENT_ID=${config.security?.['keycloak-app-client-id'] || 'go-duck-app'}
235
+ - GO_DUCK_SECURITY_KEYCLOAK_SERVICE_CLIENT_ID=${config.security?.['keycloak-service-client-id'] || 'go-duck-service'}
236
+ - GO_DUCK_SECURITY_KEYCLOAK_SERVICE_SECRET=${config.security?.['keycloak-service-secret'] || 'service-secret-123'}
237
+ - GO_DUCK_STORAGE_MINIO_ENDPOINT=http://minio:9000
238
+ - GO_DUCK_STORAGE_MINIO_ACCESS_KEY=${config.storage?.minio?.['access-key'] || 'minioadmin'}
239
+ - GO_DUCK_STORAGE_MINIO_SECRET_KEY=${config.storage?.minio?.['secret-key'] || 'minioadmin'}
217
240
  restart: always
218
241
  networks:
219
242
  - go-duck-net
@@ -244,10 +267,26 @@ services:
244
267
  - GO_DUCK_DATASOURCE_PASSWORD=${config.datasource?.password || 'password'}
245
268
  - GO_DUCK_DATASOURCE_DATABASE=${config.datasource?.database || 'go_duck_master'}
246
269
  - GO_DUCK_DATASOURCE_PORT=5432
270
+ - GO_DUCK_DATASOURCE_MONGODB_ENABLED=${config.datasource?.mongodb?.enabled ? 'true' : 'false'}
271
+ - GO_DUCK_DATASOURCE_MONGODB_URI=mongodb://mongodb:27017
272
+ - GO_DUCK_DATASOURCE_MONGODB_DATABASE=${config.datasource?.mongodb?.database || 'goduck_document_store'}
247
273
  - GO_DUCK_CACHE_REDIS_HOST=redis:6379
274
+ - GO_DUCK_CACHE_REDIS_PASSWORD=${config.cache?.redis?.password || ''}
248
275
  - GO_DUCK_MESSAGING_MQTT_BROKER=tcp://mosquitto:1883
276
+ - GO_DUCK_MESSAGING_MQTT_USERNAME=${config.messaging?.mqtt?.username || 'dev_user'}
277
+ - GO_DUCK_MESSAGING_MQTT_PASSWORD=${config.messaging?.mqtt?.password || 'dev_password'}
249
278
  - GO_DUCK_TELEMETRY_OTEL_ENDPOINT=otel-collector:4317
250
279
  - GO_DUCK_ELASTICSEARCH_ADDRESSES=http://elasticsearch:9200
280
+ - GO_DUCK_ELASTICSEARCH_USERNAME=${config.elasticsearch?.username || 'elastic'}
281
+ - GO_DUCK_ELASTICSEARCH_PASSWORD=${config.elasticsearch?.password || 'changeme'}
282
+ - GO_DUCK_SECURITY_KEYCLOAK_HOST=http://keycloak:8080
283
+ - GO_DUCK_SECURITY_KEYCLOAK_REALM=${config.security?.['keycloak-realm'] || 'go-duck-realm'}
284
+ - GO_DUCK_SECURITY_KEYCLOAK_APP_CLIENT_ID=${config.security?.['keycloak-app-client-id'] || 'go-duck-app'}
285
+ - GO_DUCK_SECURITY_KEYCLOAK_SERVICE_CLIENT_ID=${config.security?.['keycloak-service-client-id'] || 'go-duck-service'}
286
+ - GO_DUCK_SECURITY_KEYCLOAK_SERVICE_SECRET=${config.security?.['keycloak-service-secret'] || 'service-secret-123'}
287
+ - GO_DUCK_STORAGE_MINIO_ENDPOINT=http://minio:9000
288
+ - GO_DUCK_STORAGE_MINIO_ACCESS_KEY=${config.storage?.minio?.['access-key'] || 'minioadmin'}
289
+ - GO_DUCK_STORAGE_MINIO_SECRET_KEY=${config.storage?.minio?.['secret-key'] || 'minioadmin'}
251
290
  depends_on:
252
291
  postgres:
253
292
  condition: service_healthy
@@ -436,21 +475,21 @@ echo "========================================="
436
475
  const k8sAppYaml = `apiVersion: apps/v1
437
476
  kind: Deployment
438
477
  metadata:
439
- name: ${appName}
478
+ name: ${k8sAppName}
440
479
  labels:
441
- app: ${appName}
480
+ app: ${k8sAppName}
442
481
  spec:
443
482
  replicas: 1
444
483
  selector:
445
484
  matchLabels:
446
- app: ${appName}
485
+ app: ${k8sAppName}
447
486
  template:
448
487
  metadata:
449
488
  labels:
450
- app: ${appName}
489
+ app: ${k8sAppName}
451
490
  spec:
452
491
  containers:
453
- - name: ${appName}
492
+ - name: ${k8sAppName}
454
493
  image: ${appName}:latest
455
494
  env:
456
495
  - name: GO_PROFILE
@@ -458,7 +497,7 @@ spec:
458
497
  - name: GO_DUCK_SERVER_REST_PORT
459
498
  value: "${appPort}"
460
499
  - name: GO_DUCK_DATASOURCE_HOST
461
- value: postgres.${shortName}-postgres-k8s.svc.cluster.local
500
+ value: postgres.${k8sAppName}-postgres-k8s.svc.cluster.local
462
501
  - name: GO_DUCK_DATASOURCE_USERNAME
463
502
  value: "${config.datasource?.username || 'postgres'}"
464
503
  - name: GO_DUCK_DATASOURCE_PASSWORD
@@ -467,18 +506,48 @@ spec:
467
506
  value: "${config.datasource?.database || 'go_duck_master'}"
468
507
  - name: GO_DUCK_DATASOURCE_PORT
469
508
  value: "5432"
509
+ - name: GO_DUCK_DATASOURCE_MONGODB_ENABLED
510
+ value: "${config.datasource?.mongodb?.enabled ? 'true' : 'false'}"
511
+ - name: GO_DUCK_DATASOURCE_MONGODB_URI
512
+ value: mongodb://mongodb.${k8sAppName}-mongodb-k8s.svc.cluster.local:27017
513
+ - name: GO_DUCK_DATASOURCE_MONGODB_DATABASE
514
+ value: "${config.datasource?.mongodb?.database || 'goduck_document_store'}"
470
515
  - name: GO_DUCK_CACHE_REDIS_HOST
471
- value: redis.${shortName}-redis-k8s.svc.cluster.local:6379
516
+ value: redis.${k8sAppName}-redis-k8s.svc.cluster.local:6379
517
+ - name: GO_DUCK_CACHE_REDIS_PASSWORD
518
+ value: "${config.cache?.redis?.password || ''}"
472
519
  - name: GO_DUCK_MESSAGING_MQTT_BROKER
473
- value: tcp://mosquitto.${shortName}-mosquitto-k8s.svc.cluster.local:1883
520
+ value: tcp://mosquitto.${k8sAppName}-mosquitto-k8s.svc.cluster.local:1883
521
+ - name: GO_DUCK_MESSAGING_MQTT_USERNAME
522
+ value: "${config.messaging?.mqtt?.username || 'dev_user'}"
523
+ - name: GO_DUCK_MESSAGING_MQTT_PASSWORD
524
+ value: "${config.messaging?.mqtt?.password || 'dev_password'}"
474
525
  - name: GO_DUCK_MESSAGING_NATS_URL
475
- value: nats://nats.${shortName}-nats-k8s.svc.cluster.local:4222
526
+ value: nats://nats.${k8sAppName}-nats-k8s.svc.cluster.local:4222
476
527
  - name: GO_DUCK_TELEMETRY_OTEL_ENDPOINT
477
- value: otel-collector.${shortName}-otel-collector-k8s.svc.cluster.local:4317
528
+ value: otel-collector.${k8sAppName}-otel-collector-k8s.svc.cluster.local:4317
478
529
  - name: GO_DUCK_ELASTICSEARCH_ADDRESSES
479
- value: http://elasticsearch.${shortName}-elasticsearch-k8s.svc.cluster.local:9200
530
+ value: http://elasticsearch.${k8sAppName}-elasticsearch-k8s.svc.cluster.local:9200
531
+ - name: GO_DUCK_ELASTICSEARCH_USERNAME
532
+ value: "${config.elasticsearch?.username || 'elastic'}"
533
+ - name: GO_DUCK_ELASTICSEARCH_PASSWORD
534
+ value: "${config.elasticsearch?.password || 'changeme'}"
480
535
  - name: GO_DUCK_SECURITY_KEYCLOAK_HOST
481
- value: http://keycloak.${shortName}-keycloak-k8s.svc.cluster.local:8080
536
+ value: http://keycloak.${k8sAppName}-keycloak-k8s.svc.cluster.local:8080
537
+ - name: GO_DUCK_SECURITY_KEYCLOAK_REALM
538
+ value: "${config.security?.['keycloak-realm'] || 'go-duck-realm'}"
539
+ - name: GO_DUCK_SECURITY_KEYCLOAK_APP_CLIENT_ID
540
+ value: "${config.security?.['keycloak-app-client-id'] || 'go-duck-app'}"
541
+ - name: GO_DUCK_SECURITY_KEYCLOAK_SERVICE_CLIENT_ID
542
+ value: "${config.security?.['keycloak-service-client-id'] || 'go-duck-service'}"
543
+ - name: GO_DUCK_SECURITY_KEYCLOAK_SERVICE_SECRET
544
+ value: "${config.security?.['keycloak-service-secret'] || 'service-secret-123'}"
545
+ - name: GO_DUCK_STORAGE_MINIO_ENDPOINT
546
+ value: "http://minio.${k8sAppName}-minio-k8s.svc.cluster.local:9000"
547
+ - name: GO_DUCK_STORAGE_MINIO_ACCESS_KEY
548
+ value: "${config.storage?.minio?.['access-key'] || 'minioadmin'}"
549
+ - name: GO_DUCK_STORAGE_MINIO_SECRET_KEY
550
+ value: "${config.storage?.minio?.['secret-key'] || 'minioadmin'}"
482
551
  ports:
483
552
  - name: http
484
553
  containerPort: ${appPort}
@@ -488,7 +557,7 @@ spec:
488
557
  apiVersion: v1
489
558
  kind: Service
490
559
  metadata:
491
- name: ${appName}
560
+ name: ${k8sAppName}
492
561
  spec:
493
562
  type: NodePort
494
563
  ports:
@@ -501,7 +570,7 @@ spec:
501
570
  targetPort: grpc
502
571
  nodePort: 30090
503
572
  selector:
504
- app: ${appName}
573
+ app: ${k8sAppName}
505
574
  `;
506
575
 
507
576
  const k8sPostgresYaml = `apiVersion: v1
@@ -939,7 +1008,7 @@ spec:
939
1008
  await fs.writeFile(path.join(k8sDir, 'mosquitto.conf'), mosquittoConf);
940
1009
  // Namespace calculation: unique namespace per service
941
1010
  const applyNs = (yamlString, serviceName) => {
942
- const nsName = `${shortName}-${serviceName}-k8s`;
1011
+ const nsName = `${k8sAppName}-${serviceName}-k8s`;
943
1012
  const nsBlock = `apiVersion: v1\nkind: Namespace\nmetadata:\n name: ${nsName}\n---\n`;
944
1013
  return nsBlock + yamlString.replace(/^metadata:$/gm, `metadata:\n namespace: ${nsName}`);
945
1014
  };
@@ -950,7 +1019,7 @@ spec:
950
1019
  // We conditionally add minio if storage is enabled (or any blob provider), but for safety we'll just generate its namespace always.
951
1020
  servicesList.push('minio');
952
1021
 
953
- const k8sNamespaceYaml = servicesList.map(svc => `apiVersion: v1\nkind: Namespace\nmetadata:\n name: ${shortName}-${svc}-k8s`).join('\n---\n');
1022
+ const k8sNamespaceYaml = servicesList.map(svc => `apiVersion: v1\nkind: Namespace\nmetadata:\n name: ${k8sAppName}-${svc}-k8s`).join('\n---\n');
954
1023
 
955
1024
  await fs.writeFile(path.join(k8sDir, 'namespace.yaml'), k8sNamespaceYaml);
956
1025
 
@@ -72,7 +72,7 @@ export const generateDocumentation = async (config, entities, outputDir, enums =
72
72
  entities: entities,
73
73
  enums: enums,
74
74
  openEntities: openEntities,
75
- serverPort: config.server?.port || 8080,
75
+ serverPort: config.server?.rest?.port || 8080,
76
76
  grpcPort: grpcPort,
77
77
  mqttPort: mqttPort
78
78
  };
@@ -0,0 +1,54 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+
5
+ export const generateMQTTTopicsJSON = async (config, entities, outputDir) => {
6
+ const docsDir = path.join(outputDir, 'docs');
7
+ await fs.ensureDir(docsDir);
8
+
9
+ const jsonFilePath = path.join(docsDir, 'mqtt_topics.json5');
10
+ const needle = '// needle-mqtt-topics-add-below';
11
+
12
+ let content = '';
13
+ if (await fs.pathExists(jsonFilePath)) {
14
+ content = await fs.readFile(jsonFilePath, 'utf8');
15
+ } else {
16
+ content = `{
17
+ "topics": {
18
+ // Custom topics can be added here manually.
19
+
20
+ ${needle}
21
+ }
22
+ }`;
23
+ }
24
+
25
+ const topicPrefix = config.messaging?.mqtt?.['topic-prefix'] || 'go-duck/events';
26
+ let newEntries = '';
27
+
28
+ for (const entity of entities) {
29
+ const entityName = entity.name.toLowerCase();
30
+ const baseTopic = `${topicPrefix}/${entityName}`;
31
+
32
+ const createdTopic = `"${baseTopic}/created"`;
33
+ if (!content.includes(createdTopic)) {
34
+ newEntries += ` // Auto-generated topics for ${entity.name}\n`;
35
+ newEntries += ` ${createdTopic}: "Triggered when a new ${entity.name} is created.",\n`;
36
+ newEntries += ` "${baseTopic}/updated": "Triggered when an existing ${entity.name} is updated.",\n`;
37
+ newEntries += ` "${baseTopic}/deleted": "Triggered when a ${entity.name} is deleted.",\n\n`;
38
+ }
39
+ }
40
+
41
+ if (newEntries.length > 0) {
42
+ if (content.includes(needle)) {
43
+ content = content.replace(needle, newEntries + ` ${needle}`);
44
+ } else {
45
+ // Fallback if needle is missing somehow
46
+ content = content.replace(/}\s*}$/, `,\n${newEntries}\n }\n}`);
47
+ }
48
+
49
+ await fs.writeFile(jsonFilePath, content);
50
+ console.log(chalk.gray(' Updated MQTT Topics Dictionary: mqtt_topics.json5'));
51
+ } else {
52
+ console.log(chalk.gray(' MQTT Topics Dictionary up to date.'));
53
+ }
54
+ };
@@ -4,6 +4,9 @@ import chalk from 'chalk';
4
4
 
5
5
  export const generatePostmanCollection = async (config, entities, outputDir, openEntities = []) => {
6
6
  const docsDir = path.join(outputDir, 'docs');
7
+ const apiPrefixRaw = config.server?.rest?.['api-path-prefix'] || '/api';
8
+ const apiPrefix = apiPrefixRaw.endsWith('/') ? apiPrefixRaw.slice(0, -1) : apiPrefixRaw;
9
+ const apiPathArr = apiPrefix.split('/').filter(p => p !== '');
7
10
  await fs.ensureDir(docsDir);
8
11
 
9
12
  const collection = {
@@ -15,7 +18,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
15
18
  item: [],
16
19
  variable: [
17
20
  { key: "host", value: "localhost", type: "string" },
18
- { key: "port", value: String(config.server?.port || 8080), type: "string" },
21
+ { key: "port", value: String(config.server?.rest?.port || 8080), type: "string" },
19
22
  { key: "tenant", value: "tenant_1", type: "string" },
20
23
  { key: "token", value: "", type: "string" },
21
24
  { key: "keycloak_url", value: config.security?.['keycloak-host'] || "http://localhost:8180", type: "string" },
@@ -140,11 +143,11 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
140
143
  { key: "X-Tenant-ID", value: "{{tenant}}" }
141
144
  ],
142
145
  url: {
143
- raw: "http://{{host}}:{{port}}/api/admin/audit",
146
+ raw: `http://{{host}}:{{port}}${apiPrefix}/admin/audit`,
144
147
  protocol: "http",
145
148
  host: ["{{host}}"],
146
149
  port: "{{port}}",
147
- path: ["api", "admin", "audit"]
150
+ path: [...apiPathArr, "admin", "audit"]
148
151
  }
149
152
  }
150
153
  },
@@ -157,11 +160,11 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
157
160
  { key: "X-Tenant-ID", value: "{{tenant}}" }
158
161
  ],
159
162
  url: {
160
- raw: "http://{{host}}:{{port}}/api/metering/usage",
163
+ raw: `http://{{host}}:{{port}}${apiPrefix}/metering/usage`,
161
164
  protocol: "http",
162
165
  host: ["{{host}}"],
163
166
  port: "{{port}}",
164
- path: ["api", "metering", "usage"]
167
+ path: [...apiPathArr, "metering", "usage"]
165
168
  }
166
169
  }
167
170
  }
@@ -361,8 +364,9 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
361
364
  else if (f.type === 'Float' || f.type === 'BigDecimal') obj[f.name] = 99.99;
362
365
  else if (f.type === 'Boolean') obj[f.name] = true;
363
366
  else if (f.type === 'LocalDate') obj[f.name] = "2024-01-01";
364
- else if (f.type === 'Instant') obj[f.name] = "2024-01-01T12:00:00Z";
367
+ else if (f.type === 'Instant' || f.type === 'DateTime' || f.type === 'Datetime') obj[f.name] = "2024-01-01T12:00:00Z";
365
368
  else if (f.type === 'JSON' || f.type === 'JSONB') obj[f.name] = {"attribute": "example_value"};
369
+ else if (f.isEnum) obj[f.name] = "ACTIVE"; // Safe fallback for enums
366
370
  else obj[f.name] = "test";
367
371
  }
368
372
  return JSON.stringify(obj, null, 2);
@@ -390,7 +394,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
390
394
  request: {
391
395
  method: "GET",
392
396
  header: [ { key: "X-Tenant-ID", value: "{{tenant}}" } ],
393
- url: { raw: `http://{{host}}:{{port}}/open/api/${name}s?page=1&size=10&eager=true&sort=id,asc`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["open", "api", `${name}s`], query: [ { key: "page", value: "1" }, { key: "size", value: "10" }, { key: "eager", value: "true" }, { key: "sort", value: "id,asc" } ] }
397
+ url: { raw: `http://{{host}}:{{port}}/open${apiPrefix}/${name}s?page=1&size=10&eager=true&sort=id,asc`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["open", ...apiPathArr, `${name}s`], query: [ { key: "page", value: "1" }, { key: "size", value: "10" }, { key: "eager", value: "true" }, { key: "sort", value: "id,asc" } ] }
394
398
  }
395
399
  });
396
400
  publicItems.push({
@@ -398,7 +402,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
398
402
  request: {
399
403
  method: "GET",
400
404
  header: [ { key: "X-Tenant-ID", value: "{{tenant}}" } ],
401
- url: { raw: `http://{{host}}:{{port}}/open/api/${name}s/1`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["open", "api", `${name}s`, "1"] }
405
+ url: { raw: `http://{{host}}:{{port}}/open${apiPrefix}/${name}s/1`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["open", ...apiPathArr, `${name}s`, "1"] }
402
406
  }
403
407
  });
404
408
  }
@@ -409,7 +413,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
409
413
  method: "POST",
410
414
  header: [ { key: "X-Tenant-ID", value: "{{tenant}}" }, { key: "Content-Type", value: "application/json" } ],
411
415
  body: { mode: "raw", raw: generateDummyJson(entity.fields) },
412
- url: { raw: `http://{{host}}:{{port}}/open/api/${name}s`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["open", "api", `${name}s`] }
416
+ url: { raw: `http://{{host}}:{{port}}/open${apiPrefix}/${name}s`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["open", ...apiPathArr, `${name}s`] }
413
417
  }
414
418
  });
415
419
  }
@@ -425,7 +429,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
425
429
  request: {
426
430
  method: "GET",
427
431
  header: [ { key: "Authorization", value: "Bearer {{token}}" }, { key: "X-Tenant-ID", value: "{{tenant}}" } ],
428
- url: { raw: `http://{{host}}:{{port}}/api/${name}s?page=1&size=10&eager=true&sort=id,asc`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["api", `${name}s`], query: [ { key: "page", value: "1" }, { key: "size", value: "10" }, { key: "eager", value: "true" }, { key: "sort", value: "id,asc" } ] }
432
+ url: { raw: `http://{{host}}:{{port}}${apiPrefix}/${name}s?page=1&size=10&eager=true&sort=id,asc`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: [...apiPathArr, `${name}s`], query: [ { key: "page", value: "1" }, { key: "size", value: "10" }, { key: "eager", value: "true" }, { key: "sort", value: "id,asc" } ] }
429
433
  }
430
434
  },
431
435
  {
@@ -434,7 +438,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
434
438
  method: "POST",
435
439
  header: [ { key: "Authorization", value: "Bearer {{token}}" }, { key: "X-Tenant-ID", value: "{{tenant}}" }, { key: "Content-Type", value: "application/json" } ],
436
440
  body: { mode: "raw", raw: generateDummyJson(entity.fields) },
437
- url: { raw: `http://{{host}}:{{port}}/api/${name}s`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["api", `${name}s`] }
441
+ url: { raw: `http://{{host}}:{{port}}${apiPrefix}/${name}s`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: [...apiPathArr, `${name}s`] }
438
442
  }
439
443
  },
440
444
  {
@@ -442,7 +446,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
442
446
  request: {
443
447
  method: "GET",
444
448
  header: [ { key: "Authorization", value: "Bearer {{token}}" }, { key: "X-Tenant-ID", value: "{{tenant}}" } ],
445
- url: { raw: `http://{{host}}:{{port}}/api/${name}s/1`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["api", `${name}s`, "1"] }
449
+ url: { raw: `http://{{host}}:{{port}}${apiPrefix}/${name}s/1`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: [...apiPathArr, `${name}s`, "1"] }
446
450
  }
447
451
  },
448
452
  {
@@ -450,7 +454,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
450
454
  request: {
451
455
  method: "GET",
452
456
  header: [ { key: "Authorization", value: "Bearer {{token}}" }, { key: "X-Tenant-ID", value: "{{tenant}}" } ],
453
- url: { raw: `http://{{host}}:{{port}}/api/rpc/${name}?id=gt.0`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["api", "rpc", name], query: [ { key: "id", value: "gt.0" } ] }
457
+ url: { raw: `http://{{host}}:{{port}}${apiPrefix}/rpc/${name}?id=gt.0`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: [...apiPathArr, "rpc", name], query: [ { key: "id", value: "gt.0" } ] }
454
458
  }
455
459
  }
456
460
  ];
@@ -461,7 +465,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
461
465
  request: {
462
466
  method: "GET",
463
467
  header: [ { key: "Authorization", value: "Bearer {{token}}" }, { key: "X-Tenant-ID", value: "{{tenant}}" } ],
464
- url: { raw: `http://{{host}}:{{port}}/api/search/${name}?q=Sample`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["api", "search", name], query: [ { key: "q", value: "Sample" } ] }
468
+ url: { raw: `http://{{host}}:{{port}}${apiPrefix}/search/${name}?q=Sample`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: [...apiPathArr, "search", name], query: [ { key: "q", value: "Sample" } ] }
465
469
  }
466
470
  });
467
471
  }
@@ -513,11 +517,11 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
513
517
  ]
514
518
  },
515
519
  url: {
516
- raw: "http://{{host}}:{{port}}/api/storage/upload?provider=sftp",
520
+ raw: `http://{{host}}:{{port}}${apiPrefix}/storage/upload?provider=sftp`,
517
521
  protocol: "http",
518
522
  host: ["{{host}}"],
519
523
  port: "{{port}}",
520
- path: ["api", "storage", "upload"],
524
+ path: [...apiPathArr, "storage", "upload"],
521
525
  query: [{ key: "provider", value: "sftp" }]
522
526
  }
523
527
  }
@@ -531,11 +535,11 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
531
535
  { key: "X-Tenant-ID", value: "{{tenant}}" }
532
536
  ],
533
537
  url: {
534
- raw: "http://{{host}}:{{port}}/api/storage/download/farm/animals/photo.jpg?provider=sftp",
538
+ raw: `http://{{host}}:{{port}}${apiPrefix}/storage/download/farm/animals/photo.jpg?provider=sftp`,
535
539
  protocol: "http",
536
540
  host: ["{{host}}"],
537
541
  port: "{{port}}",
538
- path: ["api", "storage", "download", "farm", "animals", "photo.jpg"],
542
+ path: [...apiPathArr, "storage", "download", "farm", "animals", "photo.jpg"],
539
543
  query: [{ key: "provider", value: "sftp" }]
540
544
  }
541
545
  }
@@ -549,11 +553,11 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
549
553
  { key: "X-Tenant-ID", value: "{{tenant}}" }
550
554
  ],
551
555
  url: {
552
- raw: "http://{{host}}:{{port}}/api/storage/scan/farm/animals/photo.jpg",
556
+ raw: `http://{{host}}:{{port}}${apiPrefix}/storage/scan/farm/animals/photo.jpg`,
553
557
  protocol: "http",
554
558
  host: ["{{host}}"],
555
559
  port: "{{port}}",
556
- path: ["api", "storage", "scan", "farm", "animals", "photo.jpg"]
560
+ path: [...apiPathArr, "storage", "scan", "farm", "animals", "photo.jpg"]
557
561
  }
558
562
  }
559
563
  }
@@ -19,10 +19,17 @@ export const generateRouterCode = async (outputDir, config, entities, openEntiti
19
19
  const templateSource = await fs.readFile(templatePath, 'utf8');
20
20
  const template = Handlebars.compile(templateSource);
21
21
 
22
+ const packageJsonPath = path.resolve(__dirname, '../package.json');
23
+ const packageJson = await fs.readJson(packageJsonPath);
24
+ const cliVersion = packageJson.version;
25
+ const generatedDate = new Date().toISOString();
26
+
22
27
  const content = template({
23
28
  app_name: config.name,
24
29
  entities,
25
- openEntities
30
+ openEntities,
31
+ cli_version: cliVersion,
32
+ generated_date: generatedDate
26
33
  });
27
34
 
28
35
  await fs.writeFile(path.join(routerDir, 'router.go'), content);