go-duck-cli 1.3.3 → 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 +4 -1
- package/generators/config.js +6 -1
- package/generators/devops.js +26 -19
- package/generators/docs.js +1 -1
- package/generators/mqtt-topics.js +54 -0
- package/generators/postman.js +3 -2
- package/generators/router.js +8 -1
- package/generators/swagger.js +18 -18
- package/index.js +10 -1
- package/package.json +22 -1
- package/templates/docs/pages/index.hbs +12 -2
- package/templates/docs/pages/integrations.hbs +6 -1
- package/templates/docs/pages/mosquitto.hbs +6 -0
- package/templates/go/router.go.hbs +467 -15
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** | 🏆 **
|
|
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.
|
package/generators/config.js
CHANGED
|
@@ -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)
|
package/generators/devops.js
CHANGED
|
@@ -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
|
|
@@ -468,21 +475,21 @@ echo "========================================="
|
|
|
468
475
|
const k8sAppYaml = `apiVersion: apps/v1
|
|
469
476
|
kind: Deployment
|
|
470
477
|
metadata:
|
|
471
|
-
name: ${
|
|
478
|
+
name: ${k8sAppName}
|
|
472
479
|
labels:
|
|
473
|
-
app: ${
|
|
480
|
+
app: ${k8sAppName}
|
|
474
481
|
spec:
|
|
475
482
|
replicas: 1
|
|
476
483
|
selector:
|
|
477
484
|
matchLabels:
|
|
478
|
-
app: ${
|
|
485
|
+
app: ${k8sAppName}
|
|
479
486
|
template:
|
|
480
487
|
metadata:
|
|
481
488
|
labels:
|
|
482
|
-
app: ${
|
|
489
|
+
app: ${k8sAppName}
|
|
483
490
|
spec:
|
|
484
491
|
containers:
|
|
485
|
-
- name: ${
|
|
492
|
+
- name: ${k8sAppName}
|
|
486
493
|
image: ${appName}:latest
|
|
487
494
|
env:
|
|
488
495
|
- name: GO_PROFILE
|
|
@@ -490,7 +497,7 @@ spec:
|
|
|
490
497
|
- name: GO_DUCK_SERVER_REST_PORT
|
|
491
498
|
value: "${appPort}"
|
|
492
499
|
- name: GO_DUCK_DATASOURCE_HOST
|
|
493
|
-
value: postgres.${
|
|
500
|
+
value: postgres.${k8sAppName}-postgres-k8s.svc.cluster.local
|
|
494
501
|
- name: GO_DUCK_DATASOURCE_USERNAME
|
|
495
502
|
value: "${config.datasource?.username || 'postgres'}"
|
|
496
503
|
- name: GO_DUCK_DATASOURCE_PASSWORD
|
|
@@ -502,31 +509,31 @@ spec:
|
|
|
502
509
|
- name: GO_DUCK_DATASOURCE_MONGODB_ENABLED
|
|
503
510
|
value: "${config.datasource?.mongodb?.enabled ? 'true' : 'false'}"
|
|
504
511
|
- name: GO_DUCK_DATASOURCE_MONGODB_URI
|
|
505
|
-
value: mongodb://mongodb.${
|
|
512
|
+
value: mongodb://mongodb.${k8sAppName}-mongodb-k8s.svc.cluster.local:27017
|
|
506
513
|
- name: GO_DUCK_DATASOURCE_MONGODB_DATABASE
|
|
507
514
|
value: "${config.datasource?.mongodb?.database || 'goduck_document_store'}"
|
|
508
515
|
- name: GO_DUCK_CACHE_REDIS_HOST
|
|
509
|
-
value: redis.${
|
|
516
|
+
value: redis.${k8sAppName}-redis-k8s.svc.cluster.local:6379
|
|
510
517
|
- name: GO_DUCK_CACHE_REDIS_PASSWORD
|
|
511
518
|
value: "${config.cache?.redis?.password || ''}"
|
|
512
519
|
- name: GO_DUCK_MESSAGING_MQTT_BROKER
|
|
513
|
-
value: tcp://mosquitto.${
|
|
520
|
+
value: tcp://mosquitto.${k8sAppName}-mosquitto-k8s.svc.cluster.local:1883
|
|
514
521
|
- name: GO_DUCK_MESSAGING_MQTT_USERNAME
|
|
515
522
|
value: "${config.messaging?.mqtt?.username || 'dev_user'}"
|
|
516
523
|
- name: GO_DUCK_MESSAGING_MQTT_PASSWORD
|
|
517
524
|
value: "${config.messaging?.mqtt?.password || 'dev_password'}"
|
|
518
525
|
- name: GO_DUCK_MESSAGING_NATS_URL
|
|
519
|
-
value: nats://nats.${
|
|
526
|
+
value: nats://nats.${k8sAppName}-nats-k8s.svc.cluster.local:4222
|
|
520
527
|
- name: GO_DUCK_TELEMETRY_OTEL_ENDPOINT
|
|
521
|
-
value: otel-collector.${
|
|
528
|
+
value: otel-collector.${k8sAppName}-otel-collector-k8s.svc.cluster.local:4317
|
|
522
529
|
- name: GO_DUCK_ELASTICSEARCH_ADDRESSES
|
|
523
|
-
value: http://elasticsearch.${
|
|
530
|
+
value: http://elasticsearch.${k8sAppName}-elasticsearch-k8s.svc.cluster.local:9200
|
|
524
531
|
- name: GO_DUCK_ELASTICSEARCH_USERNAME
|
|
525
532
|
value: "${config.elasticsearch?.username || 'elastic'}"
|
|
526
533
|
- name: GO_DUCK_ELASTICSEARCH_PASSWORD
|
|
527
534
|
value: "${config.elasticsearch?.password || 'changeme'}"
|
|
528
535
|
- name: GO_DUCK_SECURITY_KEYCLOAK_HOST
|
|
529
|
-
value: http://keycloak.${
|
|
536
|
+
value: http://keycloak.${k8sAppName}-keycloak-k8s.svc.cluster.local:8080
|
|
530
537
|
- name: GO_DUCK_SECURITY_KEYCLOAK_REALM
|
|
531
538
|
value: "${config.security?.['keycloak-realm'] || 'go-duck-realm'}"
|
|
532
539
|
- name: GO_DUCK_SECURITY_KEYCLOAK_APP_CLIENT_ID
|
|
@@ -536,7 +543,7 @@ spec:
|
|
|
536
543
|
- name: GO_DUCK_SECURITY_KEYCLOAK_SERVICE_SECRET
|
|
537
544
|
value: "${config.security?.['keycloak-service-secret'] || 'service-secret-123'}"
|
|
538
545
|
- name: GO_DUCK_STORAGE_MINIO_ENDPOINT
|
|
539
|
-
value: "http://minio.${
|
|
546
|
+
value: "http://minio.${k8sAppName}-minio-k8s.svc.cluster.local:9000"
|
|
540
547
|
- name: GO_DUCK_STORAGE_MINIO_ACCESS_KEY
|
|
541
548
|
value: "${config.storage?.minio?.['access-key'] || 'minioadmin'}"
|
|
542
549
|
- name: GO_DUCK_STORAGE_MINIO_SECRET_KEY
|
|
@@ -550,7 +557,7 @@ spec:
|
|
|
550
557
|
apiVersion: v1
|
|
551
558
|
kind: Service
|
|
552
559
|
metadata:
|
|
553
|
-
name: ${
|
|
560
|
+
name: ${k8sAppName}
|
|
554
561
|
spec:
|
|
555
562
|
type: NodePort
|
|
556
563
|
ports:
|
|
@@ -563,7 +570,7 @@ spec:
|
|
|
563
570
|
targetPort: grpc
|
|
564
571
|
nodePort: 30090
|
|
565
572
|
selector:
|
|
566
|
-
app: ${
|
|
573
|
+
app: ${k8sAppName}
|
|
567
574
|
`;
|
|
568
575
|
|
|
569
576
|
const k8sPostgresYaml = `apiVersion: v1
|
|
@@ -1001,7 +1008,7 @@ spec:
|
|
|
1001
1008
|
await fs.writeFile(path.join(k8sDir, 'mosquitto.conf'), mosquittoConf);
|
|
1002
1009
|
// Namespace calculation: unique namespace per service
|
|
1003
1010
|
const applyNs = (yamlString, serviceName) => {
|
|
1004
|
-
const nsName = `${
|
|
1011
|
+
const nsName = `${k8sAppName}-${serviceName}-k8s`;
|
|
1005
1012
|
const nsBlock = `apiVersion: v1\nkind: Namespace\nmetadata:\n name: ${nsName}\n---\n`;
|
|
1006
1013
|
return nsBlock + yamlString.replace(/^metadata:$/gm, `metadata:\n namespace: ${nsName}`);
|
|
1007
1014
|
};
|
|
@@ -1012,7 +1019,7 @@ spec:
|
|
|
1012
1019
|
// We conditionally add minio if storage is enabled (or any blob provider), but for safety we'll just generate its namespace always.
|
|
1013
1020
|
servicesList.push('minio');
|
|
1014
1021
|
|
|
1015
|
-
const k8sNamespaceYaml = servicesList.map(svc => `apiVersion: v1\nkind: Namespace\nmetadata:\n name: ${
|
|
1022
|
+
const k8sNamespaceYaml = servicesList.map(svc => `apiVersion: v1\nkind: Namespace\nmetadata:\n name: ${k8sAppName}-${svc}-k8s`).join('\n---\n');
|
|
1016
1023
|
|
|
1017
1024
|
await fs.writeFile(path.join(k8sDir, 'namespace.yaml'), k8sNamespaceYaml);
|
|
1018
1025
|
|
package/generators/docs.js
CHANGED
|
@@ -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
|
+
};
|
package/generators/postman.js
CHANGED
|
@@ -18,7 +18,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
|
|
|
18
18
|
item: [],
|
|
19
19
|
variable: [
|
|
20
20
|
{ key: "host", value: "localhost", type: "string" },
|
|
21
|
-
{ key: "port", value: String(config.server?.port || 8080), type: "string" },
|
|
21
|
+
{ key: "port", value: String(config.server?.rest?.port || 8080), type: "string" },
|
|
22
22
|
{ key: "tenant", value: "tenant_1", type: "string" },
|
|
23
23
|
{ key: "token", value: "", type: "string" },
|
|
24
24
|
{ key: "keycloak_url", value: config.security?.['keycloak-host'] || "http://localhost:8180", type: "string" },
|
|
@@ -364,8 +364,9 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
|
|
|
364
364
|
else if (f.type === 'Float' || f.type === 'BigDecimal') obj[f.name] = 99.99;
|
|
365
365
|
else if (f.type === 'Boolean') obj[f.name] = true;
|
|
366
366
|
else if (f.type === 'LocalDate') obj[f.name] = "2024-01-01";
|
|
367
|
-
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";
|
|
368
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
|
|
369
370
|
else obj[f.name] = "test";
|
|
370
371
|
}
|
|
371
372
|
return JSON.stringify(obj, null, 2);
|
package/generators/router.js
CHANGED
|
@@ -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);
|
package/generators/swagger.js
CHANGED
|
@@ -17,7 +17,7 @@ export const generateSwaggerDocs = async (config, entities, outputDir, openEntit
|
|
|
17
17
|
description: `Generated documentation for ${config.name} microservice`
|
|
18
18
|
},
|
|
19
19
|
servers: [
|
|
20
|
-
{ url:
|
|
20
|
+
{ url: "/", description: "Current Host" }
|
|
21
21
|
],
|
|
22
22
|
paths: {},
|
|
23
23
|
components: {
|
|
@@ -51,7 +51,7 @@ export const generateSwaggerDocs = async (config, entities, outputDir, openEntit
|
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
const commonHeaders = [
|
|
54
|
-
{ name: 'X-Tenant-ID', in: 'header', required:
|
|
54
|
+
{ name: 'X-Tenant-ID', in: 'header', required: false, schema: { type: 'string', default: 'default' }, description: 'Multi-tenancy context identifier' }
|
|
55
55
|
];
|
|
56
56
|
|
|
57
57
|
const isOpen = (entityName, action) => {
|
|
@@ -84,7 +84,7 @@ export const generateSwaggerDocs = async (config, entities, outputDir, openEntit
|
|
|
84
84
|
properties: {
|
|
85
85
|
id: { type: 'integer' },
|
|
86
86
|
...entity.fields.reduce((acc, field) => {
|
|
87
|
-
acc[field.name] =
|
|
87
|
+
acc[field.name] = getSwaggerFieldSchema(field.type);
|
|
88
88
|
return acc;
|
|
89
89
|
}, {}),
|
|
90
90
|
createdAt: { type: 'string', format: 'date-time' },
|
|
@@ -273,7 +273,7 @@ export const generateSwaggerDocs = async (config, entities, outputDir, openEntit
|
|
|
273
273
|
}
|
|
274
274
|
},
|
|
275
275
|
parameters: [
|
|
276
|
-
{ name: 'X-Tenant-ID', in: 'header', required:
|
|
276
|
+
{ name: 'X-Tenant-ID', in: 'header', required: false, schema: { type: 'string', default: 'master_internal' }, description: 'SuperAdmin internal master bypass token' }
|
|
277
277
|
],
|
|
278
278
|
responses: {
|
|
279
279
|
200: { description: 'Success' },
|
|
@@ -318,19 +318,19 @@ export const generateSwaggerDocs = async (config, entities, outputDir, openEntit
|
|
|
318
318
|
console.log(chalk.gray(' Generated Swagger Documentation: swagger.json'));
|
|
319
319
|
};
|
|
320
320
|
|
|
321
|
-
const
|
|
322
|
-
const
|
|
323
|
-
'String': 'string',
|
|
324
|
-
'Integer': 'integer',
|
|
325
|
-
'Float': 'number',
|
|
326
|
-
'Boolean': 'boolean',
|
|
327
|
-
'Long': 'integer',
|
|
328
|
-
'BigDecimal': 'number',
|
|
329
|
-
'LocalDate': 'string',
|
|
330
|
-
'Instant': 'string',
|
|
331
|
-
'JSON': 'object',
|
|
332
|
-
'JSONB': 'object',
|
|
333
|
-
'Text': 'string'
|
|
321
|
+
const getSwaggerFieldSchema = (type) => {
|
|
322
|
+
const schemas = {
|
|
323
|
+
'String': { type: 'string' },
|
|
324
|
+
'Integer': { type: 'integer', format: 'int32' },
|
|
325
|
+
'Float': { type: 'number', format: 'double' },
|
|
326
|
+
'Boolean': { type: 'boolean' },
|
|
327
|
+
'Long': { type: 'integer', format: 'int64' },
|
|
328
|
+
'BigDecimal': { type: 'number', format: 'double' },
|
|
329
|
+
'LocalDate': { type: 'string', format: 'date' },
|
|
330
|
+
'Instant': { type: 'string', format: 'date-time' },
|
|
331
|
+
'JSON': { type: 'object' },
|
|
332
|
+
'JSONB': { type: 'object' },
|
|
333
|
+
'Text': { type: 'string' }
|
|
334
334
|
};
|
|
335
|
-
return
|
|
335
|
+
return schemas[type] || { type: 'string' };
|
|
336
336
|
};
|
package/index.js
CHANGED
|
@@ -31,6 +31,7 @@ import { generateWebSocketCode } from './generators/websocket.js';
|
|
|
31
31
|
import { generateConfigLoader } from './generators/config.js';
|
|
32
32
|
import { generateLoggerCode } from './generators/logger.js';
|
|
33
33
|
import { generateMQTTCode } from './generators/mqtt.js';
|
|
34
|
+
import { generateMQTTTopicsJSON } from './generators/mqtt-topics.js';
|
|
34
35
|
import { generateCacheCode } from './generators/cache.js';
|
|
35
36
|
import { generateResilienceCode } from './generators/resilience.js';
|
|
36
37
|
import { generateTelemetryCode } from './generators/telemetry.js';
|
|
@@ -875,13 +876,21 @@ const generateYAMLConfigs = async (config, outputDir) => {
|
|
|
875
876
|
delete cleanConfig.datasource['multitenancy-databases'];
|
|
876
877
|
}
|
|
877
878
|
|
|
879
|
+
const toKebabCase = (str) => {
|
|
880
|
+
if (!str) return '';
|
|
881
|
+
return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
const rawApiPrefix = cleanConfig.server?.rest?.['api-path-prefix'] || `/${cleanConfig.name || 'api'}/api`;
|
|
885
|
+
const kebabApiPrefix = rawApiPrefix.split('/').map(segment => toKebabCase(segment)).join('/');
|
|
886
|
+
|
|
878
887
|
const extendedConfig = {
|
|
879
888
|
...cleanConfig,
|
|
880
889
|
server: {
|
|
881
890
|
rest: {
|
|
882
891
|
port: cleanConfig.server?.rest?.port || cleanConfig.server?.port || 8080,
|
|
883
892
|
protocol: cleanConfig.server?.rest?.protocol || 'json',
|
|
884
|
-
'api-path-prefix':
|
|
893
|
+
'api-path-prefix': kebabApiPrefix
|
|
885
894
|
},
|
|
886
895
|
'read-timeout': cleanConfig.server?.['read-timeout'] || '30s',
|
|
887
896
|
'write-timeout': cleanConfig.server?.['write-timeout'] || '30s',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "go-duck-cli",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.5",
|
|
4
4
|
"description": "The Ultimate Evolutionary Go Microservice Scaffolder.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
12
12
|
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18.0.0"
|
|
15
|
+
},
|
|
13
16
|
"author": "heavenscode",
|
|
14
17
|
"license": "ISC",
|
|
15
18
|
"dependencies": {
|
|
@@ -21,5 +24,23 @@
|
|
|
21
24
|
"inquirer": "^8.2.7",
|
|
22
25
|
"js-yaml": "^4.1.1",
|
|
23
26
|
"open": "^11.0.0"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"go",
|
|
30
|
+
"golang",
|
|
31
|
+
"microservice",
|
|
32
|
+
"scaffold",
|
|
33
|
+
"cli",
|
|
34
|
+
"generator",
|
|
35
|
+
"go-duck"
|
|
36
|
+
],
|
|
37
|
+
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/heavenscode/go-duck.git"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://goduck.theheavenscode.com",
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/heavenscode/go-duck/issues"
|
|
24
45
|
}
|
|
25
46
|
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
|
17
17
|
<span class="relative inline-flex rounded-full h-3 w-3 bg-emerald-500"></span>
|
|
18
18
|
</span>
|
|
19
|
-
<p class="text-[11px] font-black text-indigo-900 uppercase tracking-[0.25em]">The
|
|
19
|
+
<p class="text-[11px] font-black text-indigo-900 uppercase tracking-[0.25em]">The 420% Elite Milestone Surpassed</p>
|
|
20
20
|
</div>
|
|
21
21
|
|
|
22
22
|
<!-- Heroic Logo Anchor (Enhanced) -->
|
|
@@ -121,6 +121,16 @@
|
|
|
121
121
|
</div>
|
|
122
122
|
</div>
|
|
123
123
|
|
|
124
|
+
<div class="lg:col-span-12 bg-white p-10 rounded-[2.5rem] border border-amber-100 shadow-sm hover:shadow-2xl transition-all group relative overflow-hidden cursor-crosshair bg-gradient-to-br from-white to-amber-50/30">
|
|
125
|
+
<p class="text-[9px] font-bold text-amber-600 uppercase tracking-widest mb-4">Elite Extension (+10%)</p>
|
|
126
|
+
<h4 class="text-2xl font-black text-slate-900 mb-3 tracking-tight italic">API Gateway Standards & Swagger UI</h4>
|
|
127
|
+
<p class="text-slate-600 leading-relaxed mb-8">Natively exposes OpenAPI JSON at the JHipster-standard <code>/v3/api-docs</code> endpoint. Features a completely re-engineered, glassmorphism Swagger UI deeply integrated with Keycloak SSO and automatic token refreshing.</p>
|
|
128
|
+
<div class="flex gap-3">
|
|
129
|
+
<span class="px-4 py-1.5 bg-amber-100 text-amber-700 text-[9px] font-black rounded-lg">JHIPSTER COMPATIBLE</span>
|
|
130
|
+
<span class="px-4 py-1.5 bg-amber-100 text-amber-700 text-[9px] font-black rounded-lg">KEYCLOAK SSO UI</span>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
124
134
|
<div class="lg:col-span-12 bg-white p-10 rounded-[2.5rem] border border-fuchsia-100 shadow-sm hover:shadow-2xl transition-all group relative overflow-hidden cursor-crosshair bg-gradient-to-br from-white to-fuchsia-50/30">
|
|
125
135
|
<p class="text-[9px] font-bold text-fuchsia-600 uppercase tracking-widest mb-4">Elite Extension (+12%)</p>
|
|
126
136
|
<h4 class="text-2xl font-black text-slate-900 mb-3 tracking-tight italic">K8s Network Isolation & NodePorts</h4>
|
|
@@ -436,7 +446,7 @@
|
|
|
436
446
|
</div>
|
|
437
447
|
|
|
438
448
|
<div class="mt-24 text-slate-500 font-mono text-[10px] uppercase tracking-[0.6em] font-black relative z-10">
|
|
439
|
-
GO-DUCK • THE
|
|
449
|
+
GO-DUCK • THE 420% ELITE MILESTONE • SOVEREIGN CODE ORCHESTRATION
|
|
440
450
|
</div>
|
|
441
451
|
</div>
|
|
442
452
|
</section>
|
|
@@ -86,8 +86,13 @@ class ApiProvider {
|
|
|
86
86
|
<h2 class="text-2xl font-bold text-gray-800 mb-4 border-b pb-2">3. WSO2 API Manager Integration</h2>
|
|
87
87
|
<p class="mb-4">GO-DUCK generated APIs can automatically register themselves with a <strong>WSO2 API Manager</strong> instance on startup. This uses the WSO2 Publisher REST API to import your dynamically generated OpenAPI 3.0 specs.</p>
|
|
88
88
|
|
|
89
|
+
<div class="bg-indigo-50 border-l-4 border-indigo-500 p-4 mb-6 rounded-r">
|
|
90
|
+
<h4 class="font-bold text-indigo-900 mb-1">Standard API Gateway Discovery</h4>
|
|
91
|
+
<p class="text-sm text-indigo-800">For external gateways (Kong, Apigee) and ecosystems like <strong>JHipster</strong> and <strong>Spring Boot</strong>, your microservice natively exposes its OpenAPI JSON at the standard <code>/v3/api-docs</code> endpoint. Legacy systems can still use <code>/swagger.json</code>.</p>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
89
94
|
<div class="bg-blue-50 border-l-4 border-blue-500 p-4 mb-6 rounded-r">
|
|
90
|
-
<p class="text-blue-900"><strong>Configuration:</strong> Enable this by adding the <code>wso2</code> block to your <code>config.yaml</code>.</p>
|
|
95
|
+
<p class="text-blue-900"><strong>WSO2 Configuration:</strong> Enable this by adding the <code>wso2</code> block to your <code>config.yaml</code>.</p>
|
|
91
96
|
</div>
|
|
92
97
|
|
|
93
98
|
<pre><code class="language-yaml">go-duck:
|
|
@@ -29,6 +29,12 @@ messaging.Publish("events/users", event)</code></pre>
|
|
|
29
29
|
mosquitto_sub -h localhost -p {{mqttPort}} -t "audit/logs/#" -v</code></pre>
|
|
30
30
|
</section>
|
|
31
31
|
|
|
32
|
+
<section class="mb-10">
|
|
33
|
+
<h2 class="text-2xl font-bold text-gray-800 mb-4 border-b pb-2">Interactive Swagger Console</h2>
|
|
34
|
+
<p class="mb-4">GO-DUCK's automatically generated Swagger UI features a built-in <strong>Interactive MQTT Topics Dictionary</strong>. By connecting via WebSockets, it allows authenticated users to dynamically <strong>SUBSCRIBE</strong> to event streams or <strong>PUBLISH</strong> payloads directly from the documentation interface!</p>
|
|
35
|
+
<p class="mb-4 text-sm text-gray-600 italic">Note: Ensure your Mosquitto broker is configured to accept WebSocket connections (default port 9001) for this feature to work.</p>
|
|
36
|
+
</section>
|
|
37
|
+
|
|
32
38
|
<section class="mb-10">
|
|
33
39
|
<h2 class="text-2xl font-bold text-gray-800 mb-4 border-b pb-2">External Resources</h2>
|
|
34
40
|
<ul class="space-y-2">
|
|
@@ -5,6 +5,7 @@ import (
|
|
|
5
5
|
"fmt"
|
|
6
6
|
"log"
|
|
7
7
|
"net/http"
|
|
8
|
+
"strings"
|
|
8
9
|
"time"
|
|
9
10
|
|
|
10
11
|
"github.com/gin-gonic/gin"
|
|
@@ -162,41 +163,492 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
|
|
|
162
163
|
})
|
|
163
164
|
|
|
164
165
|
// Swagger Docs & UI
|
|
165
|
-
|
|
166
|
+
// Expose standard OpenAPI endpoints (JHipster / Spring / WSO2 compatibility)
|
|
167
|
+
r.StaticFile("/v3/api-docs", "./docs/swagger.json")
|
|
168
|
+
r.StaticFile("/swagger.json", "./docs/swagger.json") // Legacy fallback
|
|
169
|
+
r.StaticFile("/api/mqtt-topics", "./docs/mqtt_topics.json5")
|
|
170
|
+
r.StaticFile("/logo.png", "./docs/web/logo.png")
|
|
166
171
|
r.GET("/swagger", func(c *gin.Context) {
|
|
167
|
-
|
|
172
|
+
swaggerHTML := `<!DOCTYPE html>
|
|
168
173
|
<html lang="en">
|
|
169
174
|
<head>
|
|
170
175
|
<meta charset="UTF-8">
|
|
171
|
-
<title>Swagger UI</title>
|
|
176
|
+
<title>Swagger UI (Secured)</title>
|
|
172
177
|
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui.css" />
|
|
178
|
+
<script src="https://cdn.jsdelivr.net/npm/keycloak-js@24.0.4/dist/keycloak.min.js"></script>
|
|
179
|
+
<script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
|
|
173
180
|
<style>
|
|
174
181
|
html { box-sizing: border-box; overflow: -moz-scrollbars-vertical; overflow-y: scroll; }
|
|
175
182
|
*, *:before, *:after { box-sizing: inherit; }
|
|
176
|
-
body { margin:0; background: #fafafa; }
|
|
183
|
+
body { margin:0; background: #fafafa; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
|
|
184
|
+
|
|
185
|
+
.top-nav {
|
|
186
|
+
background: rgba(255, 255, 255, 0.8);
|
|
187
|
+
backdrop-filter: blur(10px);
|
|
188
|
+
-webkit-backdrop-filter: blur(10px);
|
|
189
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
|
190
|
+
padding: 12px 24px;
|
|
191
|
+
display: flex;
|
|
192
|
+
justify-content: space-between;
|
|
193
|
+
align-items: center;
|
|
194
|
+
position: sticky;
|
|
195
|
+
top: 0;
|
|
196
|
+
z-index: 1000;
|
|
197
|
+
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.nav-brand {
|
|
201
|
+
font-weight: 700;
|
|
202
|
+
font-size: 1.2rem;
|
|
203
|
+
color: #333;
|
|
204
|
+
display: flex;
|
|
205
|
+
align-items: center;
|
|
206
|
+
gap: 10px;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.nav-controls {
|
|
210
|
+
display: flex;
|
|
211
|
+
align-items: center;
|
|
212
|
+
gap: 16px;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.tenant-input {
|
|
216
|
+
padding: 8px 12px;
|
|
217
|
+
border: 1px solid #ddd;
|
|
218
|
+
border-radius: 6px;
|
|
219
|
+
font-size: 0.9rem;
|
|
220
|
+
outline: none;
|
|
221
|
+
transition: border-color 0.2s;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.tenant-input:focus {
|
|
225
|
+
border-color: #4a90e2;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.btn {
|
|
229
|
+
padding: 8px 16px;
|
|
230
|
+
border-radius: 6px;
|
|
231
|
+
border: none;
|
|
232
|
+
font-weight: 600;
|
|
233
|
+
cursor: pointer;
|
|
234
|
+
transition: all 0.2s;
|
|
235
|
+
font-size: 0.9rem;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.btn-login {
|
|
239
|
+
background: #4a90e2;
|
|
240
|
+
color: white;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.btn-login:hover {
|
|
244
|
+
background: #357abd;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.btn-logout {
|
|
248
|
+
background: #e74c3c;
|
|
249
|
+
color: white;
|
|
250
|
+
display: none;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.btn-logout:hover {
|
|
254
|
+
background: #c0392b;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.status-indicator {
|
|
258
|
+
display: flex;
|
|
259
|
+
align-items: center;
|
|
260
|
+
gap: 6px;
|
|
261
|
+
font-size: 0.85rem;
|
|
262
|
+
color: #666;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.dot {
|
|
266
|
+
width: 8px;
|
|
267
|
+
height: 8px;
|
|
268
|
+
border-radius: 50%;
|
|
269
|
+
background: #ccc;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.dot.active {
|
|
273
|
+
background: #2ecc71;
|
|
274
|
+
box-shadow: 0 0 8px #2ecc71;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/* MQTT Topics Viewer */
|
|
278
|
+
#mqtt-modal {
|
|
279
|
+
display: none;
|
|
280
|
+
position: fixed;
|
|
281
|
+
top: 0; left: 0; width: 100%; height: 100%;
|
|
282
|
+
background: rgba(0,0,0,0.6);
|
|
283
|
+
z-index: 2000;
|
|
284
|
+
justify-content: center;
|
|
285
|
+
align-items: center;
|
|
286
|
+
}
|
|
287
|
+
.mqtt-content {
|
|
288
|
+
background: #000;
|
|
289
|
+
width: 80%; max-width: 900px;
|
|
290
|
+
border-radius: 8px;
|
|
291
|
+
padding: 20px;
|
|
292
|
+
color: #00ff00;
|
|
293
|
+
font-family: monospace;
|
|
294
|
+
position: relative;
|
|
295
|
+
box-shadow: 0 10px 25px rgba(0,0,0,0.5);
|
|
296
|
+
}
|
|
297
|
+
.mqtt-header {
|
|
298
|
+
display: flex;
|
|
299
|
+
justify-content: space-between;
|
|
300
|
+
margin-bottom: 10px;
|
|
301
|
+
border-bottom: 1px solid #333;
|
|
302
|
+
padding-bottom: 10px;
|
|
303
|
+
}
|
|
304
|
+
.mqtt-close { color: white; cursor: pointer; font-size: 1.5rem; }
|
|
305
|
+
.mqtt-dl { background: #333; color: white; border: none; padding: 5px 10px; cursor: pointer; border-radius: 4px; }
|
|
306
|
+
.mqtt-dl:hover { background: #555; }
|
|
307
|
+
|
|
308
|
+
.syntax-punct { color: #ff0000; }
|
|
309
|
+
|
|
310
|
+
/* Interactive MQTT Actions */
|
|
311
|
+
.mqtt-action-btn {
|
|
312
|
+
margin-left: 10px; padding: 2px 8px; border: none; border-radius: 4px;
|
|
313
|
+
font-size: 0.8rem; font-weight: bold; color: white; cursor: pointer;
|
|
314
|
+
vertical-align: middle;
|
|
315
|
+
}
|
|
316
|
+
#mqtt-action-modal {
|
|
317
|
+
display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
|
318
|
+
background: rgba(0,0,0,0.8); z-index: 3000; justify-content: center; align-items: center;
|
|
319
|
+
}
|
|
320
|
+
.mqtt-action-content {
|
|
321
|
+
background: #1e1e1e; width: 80%; max-width: 800px; border-radius: 8px; padding: 20px;
|
|
322
|
+
color: #fff; font-family: monospace; position: relative;
|
|
323
|
+
}
|
|
324
|
+
.mqtt-console {
|
|
325
|
+
background: #000; color: #0f0; padding: 10px; height: 300px; overflow-y: auto;
|
|
326
|
+
border-radius: 4px; margin-top: 10px; border: 1px solid #333; white-space: pre-wrap;
|
|
327
|
+
}
|
|
328
|
+
.mqtt-editor {
|
|
329
|
+
width: 100%; height: 200px; background: #000; color: #0f0; font-family: monospace;
|
|
330
|
+
padding: 10px; border: 1px solid #333; border-radius: 4px; margin-top: 10px;
|
|
331
|
+
}
|
|
332
|
+
.broker-input {
|
|
333
|
+
background: #333; color: #fff; border: 1px solid #555; padding: 5px;
|
|
334
|
+
border-radius: 4px; width: 250px; margin-left: 15px; font-family: monospace;
|
|
335
|
+
}
|
|
177
336
|
</style>
|
|
178
337
|
</head>
|
|
179
338
|
<body>
|
|
339
|
+
<div class="top-nav">
|
|
340
|
+
<div class="nav-brand">
|
|
341
|
+
<img src="/logo.png" alt="GO-DUCK Logo" style="height: 32px; width: auto;" />
|
|
342
|
+
<div style="display: flex; flex-direction: column; line-height: 1.2;">
|
|
343
|
+
<span>{{app_name}} API Explorer</span>
|
|
344
|
+
<span style="font-size: 0.65em; font-weight: normal; color: #94a3b8;">
|
|
345
|
+
GO-DUCK v{{cli_version}} • Generated: {{generated_date}}
|
|
346
|
+
</span>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
<div class="nav-controls">
|
|
350
|
+
<div class="status-indicator">
|
|
351
|
+
<div class="dot" id="auth-dot"></div>
|
|
352
|
+
<span id="auth-status">Unauthenticated</span>
|
|
353
|
+
</div>
|
|
354
|
+
<input type="text" id="tenant-input" class="tenant-input" placeholder="X-Tenant-ID" value="tenant_1" title="Multi-tenant DB Target" />
|
|
355
|
+
<button id="login-btn" class="btn btn-login">Login with Keycloak</button>
|
|
356
|
+
<button id="logout-btn" class="btn btn-logout">Logout</button>
|
|
357
|
+
<button id="mqtt-btn" class="btn" style="background: #2c3e50; color: white;">MQTT Topics</button>
|
|
358
|
+
</div>
|
|
359
|
+
</div>
|
|
360
|
+
|
|
180
361
|
<div id="swagger-ui"></div>
|
|
362
|
+
|
|
363
|
+
<div id="mqtt-modal">
|
|
364
|
+
<div class="mqtt-content">
|
|
365
|
+
<div class="mqtt-header">
|
|
366
|
+
<h3 style="margin: 0; color: white;">MQTT Topics Dictionary (JSON5)</h3>
|
|
367
|
+
<div>
|
|
368
|
+
<button class="mqtt-dl" id="mqtt-dl-btn">Download JSON5</button>
|
|
369
|
+
<span class="mqtt-close" id="mqtt-close">×</span>
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
<pre id="mqtt-viewer" style="overflow-x: auto; max-height: 70vh; margin: 0;"></pre>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
<div id="mqtt-action-modal">
|
|
377
|
+
<div class="mqtt-action-content">
|
|
378
|
+
<div style="display: flex; justify-content: space-between; border-bottom: 1px solid #333; padding-bottom: 10px;">
|
|
379
|
+
<div>
|
|
380
|
+
<h3 style="margin: 0; display: inline-block;"><span id="action-title">SUBSCRIBE</span>: <span id="action-topic" style="color: #3498db;"></span></h3>
|
|
381
|
+
<input type="text" id="broker-url" class="broker-input" value="ws://localhost:9001" title="Broker WS URL" />
|
|
382
|
+
<span id="broker-status" style="margin-left: 10px; font-size: 0.9rem; color: #f39c12;">Disconnected</span>
|
|
383
|
+
</div>
|
|
384
|
+
<span class="mqtt-close" id="mqtt-action-close">×</span>
|
|
385
|
+
</div>
|
|
386
|
+
|
|
387
|
+
<!-- Subscribe View -->
|
|
388
|
+
<div id="view-subscribe" style="display: none;">
|
|
389
|
+
<div class="mqtt-console" id="mqtt-console">Waiting for messages...\n</div>
|
|
390
|
+
</div>
|
|
391
|
+
|
|
392
|
+
<!-- Publish View -->
|
|
393
|
+
<div id="view-publish" style="display: none;">
|
|
394
|
+
<textarea id="mqtt-payload" class="mqtt-editor">{\n "example": "payload"\n}</textarea>
|
|
395
|
+
<button id="mqtt-publish-btn" class="btn" style="background: #e74c3c; color: white; margin-top: 15px; width: 100%;">PUBLISH PAYLOAD</button>
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
|
|
181
400
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui-bundle.js"></script>
|
|
182
401
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui-standalone-preset.js"></script>
|
|
183
402
|
<script>
|
|
403
|
+
const KEYCLOAK_URL = 'KEYCLOAK_URL_PLACEHOLDER';
|
|
404
|
+
const KEYCLOAK_REALM = 'KEYCLOAK_REALM_PLACEHOLDER';
|
|
405
|
+
const KEYCLOAK_CLIENT = 'KEYCLOAK_CLIENT_PLACEHOLDER';
|
|
406
|
+
|
|
407
|
+
let keycloak = null;
|
|
408
|
+
|
|
409
|
+
function updateUI(authenticated) {
|
|
410
|
+
if (authenticated) {
|
|
411
|
+
document.getElementById('login-btn').style.display = 'none';
|
|
412
|
+
document.getElementById('logout-btn').style.display = 'block';
|
|
413
|
+
document.getElementById('auth-dot').classList.add('active');
|
|
414
|
+
document.getElementById('auth-status').innerText = 'Authenticated (' + (keycloak.tokenParsed?.preferred_username || 'User') + ')';
|
|
415
|
+
} else {
|
|
416
|
+
document.getElementById('login-btn').style.display = 'block';
|
|
417
|
+
document.getElementById('logout-btn').style.display = 'none';
|
|
418
|
+
document.getElementById('auth-dot').classList.remove('active');
|
|
419
|
+
document.getElementById('auth-status').innerText = 'Unauthenticated';
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
184
423
|
window.onload = function() {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
424
|
+
// Initialize Keycloak
|
|
425
|
+
keycloak = new Keycloak({
|
|
426
|
+
url: KEYCLOAK_URL,
|
|
427
|
+
realm: KEYCLOAK_REALM,
|
|
428
|
+
clientId: KEYCLOAK_CLIENT
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
keycloak.init({ onLoad: 'check-sso', checkLoginIframe: false }).then(authenticated => {
|
|
432
|
+
updateUI(authenticated);
|
|
433
|
+
|
|
434
|
+
document.getElementById('login-btn').onclick = () => keycloak.login();
|
|
435
|
+
document.getElementById('logout-btn').onclick = () => keycloak.logout();
|
|
436
|
+
|
|
437
|
+
if (authenticated) {
|
|
438
|
+
// Auto-refresh token logic
|
|
439
|
+
setInterval(() => {
|
|
440
|
+
keycloak.updateToken(70).then(refreshed => {
|
|
441
|
+
if (refreshed) {
|
|
442
|
+
console.log('Token refreshed automatically');
|
|
443
|
+
}
|
|
444
|
+
}).catch(() => {
|
|
445
|
+
console.error('Failed to refresh token');
|
|
446
|
+
updateUI(false);
|
|
447
|
+
});
|
|
448
|
+
}, 60000); // Check every minute
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// 4. Initialize Swagger UI
|
|
452
|
+
const ui = SwaggerUIBundle({
|
|
453
|
+
url: "/v3/api-docs",
|
|
454
|
+
dom_id: '#swagger-ui',
|
|
455
|
+
deepLinking: true,
|
|
456
|
+
presets: [
|
|
457
|
+
SwaggerUIBundle.presets.apis,
|
|
458
|
+
SwaggerUIStandalonePreset
|
|
459
|
+
],
|
|
460
|
+
layout: "StandaloneLayout",
|
|
461
|
+
requestInterceptor: (req) => {
|
|
462
|
+
if (keycloak && keycloak.token) {
|
|
463
|
+
req.headers['Authorization'] = 'Bearer ' + keycloak.token;
|
|
464
|
+
}
|
|
465
|
+
const tenantId = document.getElementById('tenant-input').value;
|
|
466
|
+
if (tenantId) {
|
|
467
|
+
req.headers['X-Tenant-ID'] = tenantId;
|
|
468
|
+
}
|
|
469
|
+
return req;
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
}).catch(err => {
|
|
473
|
+
console.error("Keycloak initialization failed", err);
|
|
474
|
+
document.getElementById('auth-status').innerText = 'Keycloak init failed';
|
|
475
|
+
|
|
476
|
+
// Still load swagger without auth interceptor
|
|
477
|
+
window.ui = SwaggerUIBundle({
|
|
478
|
+
url: "/v3/api-docs",
|
|
479
|
+
dom_id: '#swagger-ui',
|
|
480
|
+
deepLinking: true,
|
|
481
|
+
presets: [
|
|
482
|
+
SwaggerUIBundle.presets.apis,
|
|
483
|
+
SwaggerUIStandalonePreset
|
|
484
|
+
],
|
|
485
|
+
layout: "StandaloneLayout"
|
|
486
|
+
});
|
|
194
487
|
});
|
|
195
|
-
|
|
488
|
+
|
|
489
|
+
// MQTT Viewer Logic
|
|
490
|
+
const mqttModal = document.getElementById('mqtt-modal');
|
|
491
|
+
const actionModal = document.getElementById('mqtt-action-modal');
|
|
492
|
+
const actionTitle = document.getElementById('action-title');
|
|
493
|
+
const actionTopic = document.getElementById('action-topic');
|
|
494
|
+
const viewSub = document.getElementById('view-subscribe');
|
|
495
|
+
const viewPub = document.getElementById('view-publish');
|
|
496
|
+
const brokerInput = document.getElementById('broker-url');
|
|
497
|
+
const brokerStatus = document.getElementById('broker-status');
|
|
498
|
+
const mqttConsole = document.getElementById('mqtt-console');
|
|
499
|
+
const payloadEditor = document.getElementById('mqtt-payload');
|
|
500
|
+
|
|
501
|
+
let rawJson5 = '';
|
|
502
|
+
let mqttClient = null;
|
|
503
|
+
let currentTopic = '';
|
|
504
|
+
|
|
505
|
+
// Open Action Modal Logic (Global so inline onClick can call it)
|
|
506
|
+
window.openMqttAction = function(topic, action) {
|
|
507
|
+
currentTopic = topic;
|
|
508
|
+
actionTitle.innerText = action;
|
|
509
|
+
actionTopic.innerText = topic;
|
|
510
|
+
|
|
511
|
+
if (action === 'SEND') {
|
|
512
|
+
viewSub.style.display = 'none';
|
|
513
|
+
viewPub.style.display = 'block';
|
|
514
|
+
} else {
|
|
515
|
+
viewSub.style.display = 'block';
|
|
516
|
+
viewPub.style.display = 'none';
|
|
517
|
+
mqttConsole.innerHTML = `Waiting for messages on ${topic}...\\n`;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
actionModal.style.display = 'flex';
|
|
521
|
+
connectMqtt();
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
function connectMqtt() {
|
|
525
|
+
if (mqttClient) {
|
|
526
|
+
if (mqttClient.options.href === brokerInput.value && mqttClient.connected) {
|
|
527
|
+
// Already connected to this broker, just subscribe if needed
|
|
528
|
+
if (actionTitle.innerText === 'SUBSCRIBE') mqttClient.subscribe(currentTopic);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
mqttClient.end();
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
brokerStatus.innerText = "Connecting...";
|
|
535
|
+
brokerStatus.style.color = "#f39c12";
|
|
536
|
+
|
|
537
|
+
try {
|
|
538
|
+
mqttClient = mqtt.connect(brokerInput.value);
|
|
539
|
+
} catch(e) {
|
|
540
|
+
brokerStatus.innerText = "Error: " + e.message;
|
|
541
|
+
brokerStatus.style.color = "#e74c3c";
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
mqttClient.on('connect', () => {
|
|
546
|
+
brokerStatus.innerText = "Connected";
|
|
547
|
+
brokerStatus.style.color = "#2ecc71";
|
|
548
|
+
if (actionTitle.innerText === 'SUBSCRIBE') {
|
|
549
|
+
mqttClient.subscribe(currentTopic, (err) => {
|
|
550
|
+
if(!err) mqttConsole.innerHTML += `Subscribed to ${currentTopic}\\n`;
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
mqttClient.on('message', (topic, message) => {
|
|
556
|
+
if (topic === currentTopic) {
|
|
557
|
+
const time = new Date().toLocaleTimeString();
|
|
558
|
+
let msgStr = message.toString();
|
|
559
|
+
try { msgStr = JSON.stringify(JSON.parse(msgStr), null, 2); } catch(e){}
|
|
560
|
+
mqttConsole.innerHTML += `\\n<span style="color:#888">[${time}]</span>\\n${msgStr}\\n`;
|
|
561
|
+
mqttConsole.scrollTop = mqttConsole.scrollHeight;
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
mqttClient.on('error', (err) => {
|
|
566
|
+
brokerStatus.innerText = "Error";
|
|
567
|
+
brokerStatus.style.color = "#e74c3c";
|
|
568
|
+
console.error("MQTT Error:", err);
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
brokerInput.addEventListener('change', connectMqtt);
|
|
573
|
+
|
|
574
|
+
document.getElementById('mqtt-publish-btn').onclick = () => {
|
|
575
|
+
if (!mqttClient || !mqttClient.connected) {
|
|
576
|
+
alert("Broker not connected!");
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const payload = payloadEditor.value;
|
|
580
|
+
mqttClient.publish(currentTopic, payload, {}, (err) => {
|
|
581
|
+
if (err) {
|
|
582
|
+
alert("Failed to publish: " + err);
|
|
583
|
+
} else {
|
|
584
|
+
alert("Message published to " + currentTopic);
|
|
585
|
+
actionModal.style.display = 'none';
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
document.getElementById('mqtt-action-close').onclick = () => {
|
|
591
|
+
if (mqttClient && actionTitle.innerText === 'SUBSCRIBE') {
|
|
592
|
+
mqttClient.unsubscribe(currentTopic);
|
|
593
|
+
}
|
|
594
|
+
actionModal.style.display = 'none';
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
document.getElementById('mqtt-btn').onclick = () => {
|
|
598
|
+
fetch('/api/mqtt-topics')
|
|
599
|
+
.then(res => {
|
|
600
|
+
if (!res.ok) throw new Error("Status " + res.status);
|
|
601
|
+
return res.text();
|
|
602
|
+
})
|
|
603
|
+
.then(text => {
|
|
604
|
+
rawJson5 = text;
|
|
605
|
+
let highlighted = text
|
|
606
|
+
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
607
|
+
.replace(/([{}[\]:,])/g, '<span class="syntax-punct">$1</span>');
|
|
608
|
+
|
|
609
|
+
// Inject buttons next to topics (Only if authenticated)
|
|
610
|
+
highlighted = highlighted.replace(/"([^"]+)"(<span class="syntax-punct">.*?<\\/span>)/g, (match, topic, punct) => {
|
|
611
|
+
if (topic.includes('/')) {
|
|
612
|
+
let btnHtml = "";
|
|
613
|
+
if (keycloak && keycloak.authenticated) {
|
|
614
|
+
const isSend = /patch|put|post|create|update/i.test(topic);
|
|
615
|
+
const btnLabel = isSend ? "SEND" : "SUBSCRIBE";
|
|
616
|
+
const btnColor = isSend ? "#e74c3c" : "#3498db";
|
|
617
|
+
btnHtml = ` <button class="mqtt-action-btn" style="background:${btnColor}" onclick="openMqttAction('${topic}', '${btnLabel}')">${btnLabel}</button>`;
|
|
618
|
+
}
|
|
619
|
+
return `"${topic}"${punct}${btnHtml}`;
|
|
620
|
+
}
|
|
621
|
+
return match;
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
document.getElementById('mqtt-viewer').innerHTML = highlighted;
|
|
625
|
+
mqttModal.style.display = 'flex';
|
|
626
|
+
}).catch(err => alert('Failed to fetch MQTT topics: ' + err));
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
document.getElementById('mqtt-close').onclick = () => mqttModal.style.display = 'none';
|
|
630
|
+
|
|
631
|
+
document.getElementById('mqtt-dl-btn').onclick = () => {
|
|
632
|
+
const blob = new Blob([rawJson5], { type: 'application/json5' });
|
|
633
|
+
const url = URL.createObjectURL(blob);
|
|
634
|
+
const a = document.createElement('a');
|
|
635
|
+
a.href = url;
|
|
636
|
+
a.download = 'mqtt_topics.json5';
|
|
637
|
+
document.body.appendChild(a);
|
|
638
|
+
a.click();
|
|
639
|
+
document.body.removeChild(a);
|
|
640
|
+
URL.revokeObjectURL(url);
|
|
641
|
+
};
|
|
196
642
|
};
|
|
197
643
|
</script>
|
|
198
644
|
</body>
|
|
199
|
-
</html>`
|
|
645
|
+
</html>`
|
|
646
|
+
|
|
647
|
+
htmlStr := strings.ReplaceAll(swaggerHTML, "KEYCLOAK_URL_PLACEHOLDER", appConfig.GoDuck.Security.KeycloakHost)
|
|
648
|
+
htmlStr = strings.ReplaceAll(htmlStr, "KEYCLOAK_REALM_PLACEHOLDER", appConfig.GoDuck.Security.KeycloakRealm)
|
|
649
|
+
htmlStr = strings.ReplaceAll(htmlStr, "KEYCLOAK_CLIENT_PLACEHOLDER", appConfig.GoDuck.Security.KeycloakAppClientID)
|
|
650
|
+
|
|
651
|
+
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(htmlStr))
|
|
200
652
|
})
|
|
201
653
|
|
|
202
654
|
// Management APIs (Run-time DB onboarding)
|