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 +4 -1
- package/generators/ai_docs.js +14 -11
- package/generators/config.js +6 -1
- package/generators/devops.js +86 -17
- package/generators/docs.js +1 -1
- package/generators/mqtt-topics.js +54 -0
- package/generators/postman.js +24 -20
- package/generators/router.js +8 -1
- package/generators/swagger.js +26 -23
- 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/ai_docs.js
CHANGED
|
@@ -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 /
|
|
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:
|
|
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
|
|
62
|
-
endpointsContent += `- \`GET
|
|
63
|
-
endpointsContent += `- \`POST
|
|
64
|
-
endpointsContent += `- \`PUT
|
|
65
|
-
endpointsContent += `- \`DELETE
|
|
66
|
-
endpointsContent += `- \`POST
|
|
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 /
|
|
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 /
|
|
74
|
-
endpointsContent += `- \`POST /
|
|
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`;
|
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
|
|
@@ -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: ${
|
|
478
|
+
name: ${k8sAppName}
|
|
440
479
|
labels:
|
|
441
|
-
app: ${
|
|
480
|
+
app: ${k8sAppName}
|
|
442
481
|
spec:
|
|
443
482
|
replicas: 1
|
|
444
483
|
selector:
|
|
445
484
|
matchLabels:
|
|
446
|
-
app: ${
|
|
485
|
+
app: ${k8sAppName}
|
|
447
486
|
template:
|
|
448
487
|
metadata:
|
|
449
488
|
labels:
|
|
450
|
-
app: ${
|
|
489
|
+
app: ${k8sAppName}
|
|
451
490
|
spec:
|
|
452
491
|
containers:
|
|
453
|
-
- name: ${
|
|
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.${
|
|
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.${
|
|
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.${
|
|
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.${
|
|
526
|
+
value: nats://nats.${k8sAppName}-nats-k8s.svc.cluster.local:4222
|
|
476
527
|
- name: GO_DUCK_TELEMETRY_OTEL_ENDPOINT
|
|
477
|
-
value: otel-collector.${
|
|
528
|
+
value: otel-collector.${k8sAppName}-otel-collector-k8s.svc.cluster.local:4317
|
|
478
529
|
- name: GO_DUCK_ELASTICSEARCH_ADDRESSES
|
|
479
|
-
value: http://elasticsearch.${
|
|
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.${
|
|
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: ${
|
|
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: ${
|
|
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 = `${
|
|
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: ${
|
|
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
|
|
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
|
@@ -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:
|
|
146
|
+
raw: `http://{{host}}:{{port}}${apiPrefix}/admin/audit`,
|
|
144
147
|
protocol: "http",
|
|
145
148
|
host: ["{{host}}"],
|
|
146
149
|
port: "{{port}}",
|
|
147
|
-
path: [
|
|
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:
|
|
163
|
+
raw: `http://{{host}}:{{port}}${apiPrefix}/metering/usage`,
|
|
161
164
|
protocol: "http",
|
|
162
165
|
host: ["{{host}}"],
|
|
163
166
|
port: "{{port}}",
|
|
164
|
-
path: [
|
|
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
|
|
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
|
|
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
|
|
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}}
|
|
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}}
|
|
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}}
|
|
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}}/
|
|
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}}/
|
|
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:
|
|
520
|
+
raw: `http://{{host}}:{{port}}${apiPrefix}/storage/upload?provider=sftp`,
|
|
517
521
|
protocol: "http",
|
|
518
522
|
host: ["{{host}}"],
|
|
519
523
|
port: "{{port}}",
|
|
520
|
-
path: [
|
|
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:
|
|
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: [
|
|
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:
|
|
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: [
|
|
560
|
+
path: [...apiPathArr, "storage", "scan", "farm", "animals", "photo.jpg"]
|
|
557
561
|
}
|
|
558
562
|
}
|
|
559
563
|
}
|
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);
|