go-duck-cli 1.0.8 → 1.1.1

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.
Files changed (70) hide show
  1. package/README.md +30 -15
  2. package/generators/ai_docs.js +130 -0
  3. package/generators/broker.js +63 -0
  4. package/generators/config.js +149 -7
  5. package/generators/devops.js +210 -43
  6. package/generators/docs.js +23 -4
  7. package/generators/elasticsearch.js +263 -0
  8. package/generators/kratos.js +229 -41
  9. package/generators/metering.js +280 -48
  10. package/generators/migrations.js +92 -198
  11. package/generators/mqtt.js +2 -39
  12. package/generators/multitenancy.js +274 -71
  13. package/generators/nats.js +39 -0
  14. package/generators/outbox.js +171 -0
  15. package/generators/postgrest.js +7 -3
  16. package/generators/postman.js +405 -0
  17. package/generators/repository.js +27 -0
  18. package/generators/router.js +27 -0
  19. package/generators/security.js +95 -14
  20. package/generators/serverless.js +147 -0
  21. package/generators/storage.js +589 -0
  22. package/generators/swagger.js +84 -60
  23. package/generators/telemetry.js +23 -32
  24. package/generators/websocket.js +55 -21
  25. package/index.js +481 -116
  26. package/package.json +6 -4
  27. package/parser/gdl.js +163 -24
  28. package/templates/docs/index.html.hbs +5 -5
  29. package/templates/docs/layout.hbs +221 -62
  30. package/templates/docs/pages/audit.hbs +83 -35
  31. package/templates/docs/pages/cli.hbs +18 -0
  32. package/templates/docs/pages/configuration.hbs +241 -0
  33. package/templates/docs/pages/datadog.hbs +46 -0
  34. package/templates/docs/pages/elasticsearch.hbs +121 -0
  35. package/templates/docs/pages/federation.hbs +241 -0
  36. package/templates/docs/pages/gdl-advanced.hbs +91 -0
  37. package/templates/docs/pages/gdl-annotations.hbs +137 -0
  38. package/templates/docs/pages/gdl-entities.hbs +134 -0
  39. package/templates/docs/pages/gdl-relationships.hbs +80 -0
  40. package/templates/docs/pages/gdl.hbs +60 -204
  41. package/templates/docs/pages/graphql.hbs +58 -44
  42. package/templates/docs/pages/grpc.hbs +53 -90
  43. package/templates/docs/pages/hybrid-store.hbs +127 -0
  44. package/templates/docs/pages/index.hbs +418 -149
  45. package/templates/docs/pages/keycloak.hbs +43 -0
  46. package/templates/docs/pages/legend.hbs +116 -0
  47. package/templates/docs/pages/mosquitto.hbs +39 -0
  48. package/templates/docs/pages/multitenancy.hbs +139 -71
  49. package/templates/docs/pages/otel.hbs +40 -0
  50. package/templates/docs/pages/realtime.hbs +38 -12
  51. package/templates/docs/pages/redis.hbs +40 -0
  52. package/templates/docs/pages/rest.hbs +120 -202
  53. package/templates/docs/pages/saga.hbs +94 -0
  54. package/templates/docs/pages/security.hbs +150 -44
  55. package/templates/docs/pages/serverless.hbs +157 -0
  56. package/templates/docs/pages/storage.hbs +127 -0
  57. package/templates/docs/pages/wizard.hbs +683 -0
  58. package/templates/docs/triple_identity_registry.png +0 -0
  59. package/templates/go/controller.go.hbs +287 -283
  60. package/templates/go/entity.go.hbs +17 -15
  61. package/templates/go/main.go.hbs +47 -180
  62. package/templates/go/migrator.go.hbs +65 -0
  63. package/templates/go/router.go.hbs +272 -0
  64. package/templates/graphql/resolver.go.hbs +53 -34
  65. package/templates/graphql/schema.graphql.hbs +17 -5
  66. package/templates/kratos/service.go.hbs +169 -34
  67. package/templates/proto/entity.proto.hbs +10 -14
  68. package/test_nested.gdl +21 -0
  69. package/templates/docs/intro.mp4 +0 -0
  70. package/test_parser.js +0 -9
package/README.md CHANGED
@@ -43,21 +43,36 @@ But speed without strength is a house made of cards. In the digital forge of the
43
43
 
44
44
  Thus, the **GDL (Go-Duck Language)** was hatched. A single, simple tongue that could command entire legions of code. From that day forth, every developer who whispered GDL into the CLI would see their architecture evolve—bringing the Gopher's speed, the Duck's wisdom, the Gin's clarity, and the Kratos' strength into a single, unified masterpiece.
45
45
 
46
- ## Features Overview (The 260% Milestone)
47
-
48
- * **Full-Stack Code Generation**: Generates everything from REST and gRPC (Kratos) APIs to the internal repository layer.
49
- * **Dual-Protocol APIs**: Multi-protocol support (Gin/REST & Kratos/gRPC) with OIDC/JWT security enforcement.
50
- * **Dynamic Multi-Tenancy**: Side-by-side **Database-pet-Tenant isolation** with Hot-Swapping Connection Pools and a verified Master-Tenant Registry.
51
- * **High-Velocity Bulk Operations**: Transactional `BulkCreate`, `BulkUpdate`, and `BulkPatch` endpoints for all entities.
52
- * **Deep JSON Querying**: PostgREST-like RPC engine supporting arrow operators (`->`, `->>`) for complex JSONB searches.
53
- * **Stateful Incremental Updates**: Intelligently applies schema deltas to your existing codebase without data loss.
54
- * **Rich Ecosystem Components**:
55
- * **Persistence**: GORM (PostgreSQL) + Liquibase migrations.
56
- * **GraphQL**: Full schema and resolver generation.
57
- * **Real-time**: Traced WebSocket envelopes & MQTT notifications.
58
- * **Resilience**: Circuit Breakers (Sony/Gobreaker) & Rate Limiting.
59
- * **Observability**: Full-stack tracing (Otelgin to Otelpgx) + Prometheus metrics.
60
- * **Gorgeous Automated Documentation**: Auto-scaffolded "Apple-style" Developer Guide and High-Fidelity Swagger UI.
46
+ ## 🦆 The 375% Elite Status: Milestone Surpassed
47
+
48
+ GO-DUCK has officially reached the **375% Achievement Status**, evolving from a code generator into a **High-Velocity Distributed Orchestrator.** This elite status confirms that the framework has surpassed the original 350% milestone with industrial-grade Universal Storage extensions and real-time remote bootstrapping.
49
+
50
+ | Milestone Component | Status | Technical Value Add |
51
+ | :--- | :--- | :--- |
52
+ | **Base Core Architecture** | **COMPLETE** | Gin MVC, GORM, Dual-Protocol, Redis, MQTT, Kratos. |
53
+ | **Federated Empire Foundations** | **COMPLETE** | Hard-Silo Isolation, Triple-Identity Registry, Saga Outbox. |
54
+ | **Elite Observability & Search** | ✅ **COMPLETE** | Full OTel Tracing, Glassmorphism Docs, ES Sync. |
55
+ | **Precision Harvesting Ext.** | 🚀 **ELITE (+12%)** | Surgical multi-silo selection via comma-separated `X-Tenant-ID`. |
56
+ | **Industrial Async Execution** | 🚀 **ELITE (+15%)** | Goroutine-based parallel aggregation (The Harvester 2.0). |
57
+ | **Super Admin Security Boundary** | 🚀 **ELITE (+13%)** | Strict isolation between Business and Infrastructure Control APIs. |
58
+ | **Silo Discovery & Privacy Proxy** | 🚀 **ELITE (+10%)** | Silo discovery API with physical DB name masking. |
59
+ | **Universal Storage Mesh** | 🚀 **ELITE (+25%)** | Dynamic Hot-Swapping Registry and Distributed Cross-Scan API retrieval. |
60
+ | **TOTAL ACHIEVEMENT STATUS** | 🏆 **375%** | **ELITE STATUS CONFIRMED.** 👑 |
61
+
62
+ ### ✨ Primary Features (The 375% Core)
63
+
64
+ * **Federated Multi-Tenancy**: Side-by-side **Database-per-Tenant isolation** with a **Master-Tenant Registry** (Role ↔ DB ↔ Opaque UUID).
65
+ * **Industrial-Grade Parallel Harvester**: Asynchronous goroutine-based data aggregation across multiple silos with `?federated=true` opt-in.
66
+ * **Precision Harvesting**: Surgical multi-silo selection via comma-separated `X-Tenant-ID` headers.
67
+ * **Super Admin Security Boundaries**: Strict architectural separation between Business APIs and Infrastructure Control APIs.
68
+ * **Serverless Transformation**: Opt-in AWS Lambda native adapter (via `config.yaml`) with build-tag native entry points for on-demand cloud scaling.
69
+ * **Generic Search Layer**: PostgREST-like RPC endpoint (`/api/rpc/:table`) with **Deep JSON/JSONB Querying** using arrow operators (`->`, `->>`).
70
+ * **Distributed Saga Consistency**: Integrated **Transactional Outbox** pattern and background workers in every silo to guarantee eventual consistency across the federation.
71
+ * **Zero-Trust Identity Registry**: Decoupled mapping layer ensuring physical database names and internal IDs never leak to the client.
72
+ * **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.
73
+ * **Spring-style Elasticsearch Search**: Real-time sync for entities marked with `@Searchable`, supporting fuzzy matching and complex queries.
74
+ * **SaaS Quota Engine**: Redis-backed API bandwidth tracking with dynamic, hierarchical limits (User vs. Role mapping).
75
+ * **Resilience Layer**: Sony/Gobreaker Integration + Zero-Trust Distributed Redis Rate Limiter.
61
76
 
62
77
  ## 💾 Global Installation
63
78
 
@@ -0,0 +1,130 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ export const generateAIDocs = async (config, entities, outputDir, enums, openEntities) => {
5
+ const aiDocsDir = path.join(outputDir, 'docs', 'ai');
6
+ await fs.ensureDir(aiDocsDir);
7
+
8
+ // 1. ARCHITECTURE.md
9
+ const appName = config.name || 'go-duck-app';
10
+ const hasMongo = config.datasource?.mongodb?.enabled;
11
+ const hasPostgres = config.datasource?.host && config.datasource?.password;
12
+
13
+ let archContent = `# Architecture Overview: ${appName}\n\n`;
14
+ archContent += `## Storage & Persistence\n`;
15
+ if (hasPostgres && hasMongo) {
16
+ archContent += `- **Hybrid-Store Data Lake**: This application actively utilizes both PostgreSQL (Relational) and MongoDB (Document) engines simultaneously.\n`;
17
+ } else if (hasMongo) {
18
+ archContent += `- **Document Store**: MongoDB is the primary persistence engine.\n`;
19
+ } else {
20
+ archContent += `- **Relational Store**: PostgreSQL (GORM) is the primary persistence engine.\n`;
21
+ }
22
+ archContent += `- **Multitenancy**: ${config.multitenancy?.enabled ? 'Enabled (Database-per-tenant, via X-Tenant-ID header)' : 'Disabled'}\n`;
23
+
24
+ // Add Caching, MQTT, OpenTelemetry, etc based on config
25
+ archContent += `\n## Real-time & Eventing\n`;
26
+ archContent += `- **MQTT**: ${config.messaging?.mqtt?.enabled ? 'Enabled' : 'Disabled'}\n`;
27
+ archContent += `- **NATS**: ${config.messaging?.nats?.enabled ? 'Enabled' : 'Disabled'}\n`;
28
+ archContent += `- **Redis Caching**: ${config.cache?.redis?.enabled ? 'Enabled' : 'Disabled'}\n`;
29
+
30
+ archContent += `\n## Universal Storage Mesh\n`;
31
+ const storageActive = config.storage && Object.keys(config.storage).filter(k => config.storage[k]?.enabled).length > 0;
32
+ archContent += `- **Active**: ${storageActive ? 'Yes' : 'No'}\n`;
33
+ if (storageActive) {
34
+ 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`;
36
+ }
37
+ const bootstrapActive = config.storage?.bootstrap?.enabled;
38
+ if (bootstrapActive) {
39
+ archContent += `- **Zero-Bake Secret Bootstrapping**: Enabled. Downloads \`id_rsa\` and service accounts securely from GitHub branch \`${config.storage.bootstrap.branch || 'main'}\` at startup before mounting SFTP or S3.\n`;
40
+ }
41
+
42
+ archContent += `\n## Observability\n`;
43
+ archContent += `- **Elasticsearch Sync**: ${config.search?.elasticsearch?.enabled ? 'Enabled (via @Searchable)' : 'Disabled'}\n`;
44
+ archContent += `- **OpenTelemetry**: ${config.telemetry?.otel?.enabled ? 'Enabled' : 'Disabled'}\n`;
45
+
46
+ await fs.writeFile(path.join(aiDocsDir, 'ARCHITECTURE.md'), archContent);
47
+
48
+ // 2. ENDPOINTS.md
49
+ let endpointsContent = `# REST & gRPC API Surface\n\n`;
50
+ endpointsContent += `## Base Path: \`/api\`\n\n`;
51
+ endpointsContent += `### Standard Entity Endpoints\n`;
52
+ for (const entity of entities) {
53
+ const routeName = entity.name.toLowerCase() + 's';
54
+ endpointsContent += `\n#### ${entity.name}\n`;
55
+ endpointsContent += `- \`GET /api/${routeName}\` (Pagination, e.g. \`?page=1&size=10&eager=true\`)\n`;
56
+ endpointsContent += `- \`GET /api/${routeName}/:id\`\n`;
57
+ endpointsContent += `- \`POST /api/${routeName}\`\n`;
58
+ endpointsContent += `- \`PUT /api/${routeName}/:id\`\n`;
59
+ endpointsContent += `- \`DELETE /api/${routeName}/:id\`\n`;
60
+ endpointsContent += `- \`POST /api/${routeName}/bulk\` (Bulk Create/Update)\n`;
61
+ if (entity.isSearchable) {
62
+ endpointsContent += `- \`GET /api/search/${routeName}?q={query}\` (Elasticsearch)\n`;
63
+ }
64
+ }
65
+
66
+ endpointsContent += `\n## Infrastructure & Management APIs (SuperAdmin Protected)\n`;
67
+ endpointsContent += `- \`GET /api/admin/audit\` (Fetches Global Delta logs from the Centralized Audit Engine)\n`;
68
+ endpointsContent += `- \`POST /api/admin/metering/limit\` (Set SaaS Quota limits)\n`;
69
+ endpointsContent += `- \`POST /management/tenant/assign\` (Dynamically initialize new Tenant DBs)\n`;
70
+
71
+ endpointsContent += `\n## Open APIs (No JWT required)\n`;
72
+ const openSet = openEntities || [];
73
+ if (openSet.length > 0) {
74
+ for (const o of openSet) {
75
+ endpointsContent += `- \`Entity: ${o.name}\`, Allowed Actions: \`[${o.actions.join(', ')}]\`\n`;
76
+ }
77
+ } else {
78
+ endpointsContent += `None specified.\n`;
79
+ }
80
+
81
+ await fs.writeFile(path.join(aiDocsDir, 'ENDPOINTS.md'), endpointsContent);
82
+
83
+ // 3. ENTITIES.md
84
+ let entitiesContent = `# Domain Models (GDL Schema)\n\n`;
85
+ for (const entity of entities) {
86
+ entitiesContent += `## ${entity.name} ${entity.isDocument ? '(MongoDB)' : '(PostgreSQL)'}\n`;
87
+ entitiesContent += `Annotations: \n`;
88
+ if (entity.isSearchable) entitiesContent += `- \`@Searchable\`\n`;
89
+ if (entity.isAudited) entitiesContent += `- \`@Audited\`\n`;
90
+ if (entity.isFederated) entitiesContent += `- \`@Federated\`\n`;
91
+
92
+ entitiesContent += `\nFields:\n`;
93
+ for (const field of entity.fields) {
94
+ entitiesContent += `- \`${field.name}\` (${field.type}) ${field.required ? '- Required' : ''} ${field.unique ? '- Unique' : ''}\n`;
95
+ }
96
+ entitiesContent += `\n`;
97
+ }
98
+
99
+ await fs.writeFile(path.join(aiDocsDir, 'ENTITIES.md'), entitiesContent);
100
+
101
+ // 4. PROTOCOLS.md
102
+ let protoContent = `# Additional Network Protocols\n\n`;
103
+ protoContent += `## GraphQL Surface\n`;
104
+ protoContent += `- **Endpoint**: \`POST /query\`\n`;
105
+ protoContent += `- **Playground**: \`GET /\`\n\n`;
106
+
107
+ protoContent += `## WebSocket Engine\n`;
108
+ protoContent += `- **Endpoint**: \`ws[s]://host/ws\`\n`;
109
+ protoContent += `- **Format**: JSON Envelope Payload \`{ "type": "...", "payload": {...} }\`\n\n`;
110
+
111
+ protoContent += `## gRPC (Kratos) Engine\n`;
112
+ protoContent += `- **Port**: \`9000\` (Default)\n`;
113
+ protoContent += `- **Proto Definitions**: Located in \`api/.../\`\n`;
114
+
115
+ await fs.writeFile(path.join(aiDocsDir, 'PROTOCOLS.md'), protoContent);
116
+
117
+ // Provide a master AGENT_README.md in the root
118
+ let agentInstructions = `# LLM / AI AGENT INSTRUCTIONS 🤖\n\n`;
119
+ agentInstructions += `Welcome to the generated codebase for **${appName}**.\n\n`;
120
+ agentInstructions += `This application was generated by the GO-DUCK-CLI (An advanced Evolutionary Go Code Generator).\n\n`;
121
+ agentInstructions += `### How to navigate this system:\n`;
122
+ agentInstructions += `The full system specifications and dynamically generated blueprints have been exported for you in the \`docs/ai/\` folder. Look there before attempting any code modifications.\n\n`;
123
+ agentInstructions += `- **[docs/ai/ARCHITECTURE.md](docs/ai/ARCHITECTURE.md)**: Describes the databases, caching layers, configuration structure, and middleware enabled in this project.\n`;
124
+ agentInstructions += `- **[docs/ai/ENTITIES.md](docs/ai/ENTITIES.md)**: Describes the schemas, MongoDB vs SQL bindings, fields, and logical rules built from the GDL files.\n`;
125
+ agentInstructions += `- **[docs/ai/ENDPOINTS.md](docs/ai/ENDPOINTS.md)**: Lists every active REST and Open API route mapped in this microservice.\n`;
126
+ agentInstructions += `- **[docs/ai/PROTOCOLS.md](docs/ai/PROTOCOLS.md)**: Outlines connection logic for GraphQL, MQTT, gRPC, and WebSockets.\n\n`;
127
+ agentInstructions += `DO NOT blindly guess routing paths or framework paradigms. Please read the generated architecture maps to stay perfectly aligned with the repository's ruleset.\n`;
128
+
129
+ await fs.writeFile(path.join(outputDir, 'AGENT_README.md'), agentInstructions);
130
+ };
@@ -0,0 +1,63 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+
5
+ export const generateBrokerCode = async (config, outputDir) => {
6
+ const messagingDir = path.join(outputDir, 'messaging');
7
+ await fs.ensureDir(messagingDir);
8
+
9
+ const brokerGo = `package messaging
10
+
11
+ import (
12
+ "encoding/json"
13
+ "fmt"
14
+ "time"
15
+ )
16
+
17
+ type EventMessage struct {
18
+ Action string \`json:"action"\`
19
+ Entity string \`json:"entity"\`
20
+ EventTime time.Time \`json:"event_time"\`
21
+ Payload interface{} \`json:"payload"\`
22
+ PreviousValue interface{} \`json:"previous_value,omitempty"\`
23
+ }
24
+
25
+ func PublishEvent(topicPrefix string, tenantDB string, action string, entity string, payload interface{}, prev interface{}) {
26
+ // 1. MQTT Engine (External Client Notifications)
27
+ if MQTTClient != nil && MQTTClient.IsConnected() {
28
+ msg := EventMessage{
29
+ Action: action,
30
+ Entity: entity,
31
+ EventTime: time.Now(),
32
+ Payload: payload,
33
+ PreviousValue: prev,
34
+ }
35
+
36
+ data, err := json.Marshal(msg)
37
+ if err == nil {
38
+ topic := fmt.Sprintf("%s/%s/%s/%s", topicPrefix, tenantDB, entity, action)
39
+ MQTTClient.Publish(topic, 0, false, data)
40
+ }
41
+ }
42
+
43
+ // 2. NATS Engine (Internal Service-to-Service Streaming)
44
+ if NATSConn != nil && NATSConn.IsConnected() {
45
+ subject := fmt.Sprintf("events.%s.%s.%s", tenantDB, entity, action)
46
+
47
+ msg := EventMessage{
48
+ Action: action,
49
+ Entity: entity,
50
+ EventTime: time.Now(),
51
+ Payload: payload,
52
+ PreviousValue: prev,
53
+ }
54
+
55
+ data, _ := json.Marshal(msg)
56
+ NATSConn.Publish(subject, data)
57
+ }
58
+ }
59
+ `;
60
+
61
+ await fs.writeFile(path.join(messagingDir, 'broker.go'), brokerGo);
62
+ console.log(chalk.gray(' Generated Unified Messaging Broker Hub'));
63
+ };
@@ -15,6 +15,7 @@ import (
15
15
  "time"
16
16
 
17
17
  "github.com/spf13/viper"
18
+ "strings"
18
19
  )
19
20
 
20
21
  type Config struct {
@@ -40,10 +41,16 @@ type Config struct {
40
41
  } \`mapstructure:"server"\`
41
42
 
42
43
  Security struct {
43
- KeycloakHost string \`mapstructure:"keycloak-host"\`
44
- KeycloakRealm string \`mapstructure:"keycloak-realm"\`
45
- KeycloakClientID string \`mapstructure:"keycloak-client-id"\`
46
- KeycloakSecret string \`mapstructure:"keycloak-secret"\`
44
+ KeycloakHost string \`mapstructure:"keycloak-host"\`
45
+ KeycloakRealm string \`mapstructure:"keycloak-realm"\`
46
+ KeycloakAppClientID string \`mapstructure:"keycloak-app-client-id"\`
47
+ KeycloakAppClientSecret string \`mapstructure:"keycloak-app-client-secret"\`
48
+ KeycloakServiceClientID string \`mapstructure:"keycloak-service-client-id"\`
49
+ KeycloakServiceSecret string \`mapstructure:"keycloak-service-secret"\`
50
+ KeycloakAdminClientID string \`mapstructure:"keycloak-admin-client-id"\`
51
+ KeycloakAdminSecret string \`mapstructure:"keycloak-admin-secret"\`
52
+ SuperAdminRole string \`mapstructure:"super-admin-role"\`
53
+ ConfidentialMode bool \`mapstructure:"confidential-mode"\`
47
54
  RateLimit struct {
48
55
  RPS float64 \`mapstructure:"rps"\`
49
56
  Burst int \`mapstructure:"burst"\`
@@ -68,6 +75,10 @@ type Config struct {
68
75
  Password string \`mapstructure:"password"\`
69
76
  TopicPrefix string \`mapstructure:"topic-prefix"\`
70
77
  } \`mapstructure:"mqtt"\`
78
+ NATS struct {
79
+ Enabled bool \`mapstructure:"enabled"\`
80
+ URL string \`mapstructure:"url"\`
81
+ } \`mapstructure:"nats"\`
71
82
  } \`mapstructure:"messaging"\`
72
83
 
73
84
  Cache struct {
@@ -98,25 +109,112 @@ type Config struct {
98
109
  } \`mapstructure:"resilience"\`
99
110
 
100
111
  Multitenancy struct {
101
- Enabled bool \`mapstructure:"enabled"\`
112
+ Enabled bool \`mapstructure:"enabled"\`
113
+ HideSiloNames bool \`mapstructure:"hide-silo-names"\`
102
114
  } \`mapstructure:"multitenancy"\`
103
115
 
116
+ Storage struct {
117
+ S3 struct {
118
+ Enabled bool \`mapstructure:"enabled"\`
119
+ Bucket string \`mapstructure:"bucket"\`
120
+ Region string \`mapstructure:"region"\`
121
+ AccessKey string \`mapstructure:"access-key"\`
122
+ SecretKey string \`mapstructure:"secret-key"\`
123
+ Endpoint string \`mapstructure:"endpoint"\`
124
+ } \`mapstructure:"s3"\`
125
+ GCS struct {
126
+ Enabled bool \`mapstructure:"enabled"\`
127
+ Bucket string \`mapstructure:"bucket"\`
128
+ CredentialsFile string \`mapstructure:"credentials-file"\`
129
+ } \`mapstructure:"gcs"\`
130
+ MinIO struct {
131
+ Enabled bool \`mapstructure:"enabled"\`
132
+ Bucket string \`mapstructure:"bucket"\`
133
+ Endpoint string \`mapstructure:"endpoint"\`
134
+ AccessKey string \`mapstructure:"access-key"\`
135
+ SecretKey string \`mapstructure:"secret-key"\`
136
+ UseSSL bool \`mapstructure:"use-ssl"\`
137
+ } \`mapstructure:"minio"\`
138
+ R2 struct {
139
+ Enabled bool \`mapstructure:"enabled"\`
140
+ Bucket string \`mapstructure:"bucket"\`
141
+ AccountID string \`mapstructure:"account-id"\`
142
+ AccessKey string \`mapstructure:"access-key"\`
143
+ SecretKey string \`mapstructure:"secret-key"\`
144
+ } \`mapstructure:"r2"\`
145
+ Generic struct {
146
+ Enabled bool \`mapstructure:"enabled"\`
147
+ Provider string \`mapstructure:"provider"\`
148
+ AccessKey string \`mapstructure:"access-key"\`
149
+ SecretKey string \`mapstructure:"secret-key"\`
150
+ } \`mapstructure:"generic"\`
151
+ SFTP struct {
152
+ Enabled bool \`mapstructure:"enabled"\`
153
+ Host string \`mapstructure:"host"\`
154
+ Port int \`mapstructure:"port"\`
155
+ Username string \`mapstructure:"username"\`
156
+ Password string \`mapstructure:"password"\`
157
+ KeyFile string \`mapstructure:"key-file"\`
158
+ RemotePath string \`mapstructure:"remote-path"\`
159
+ } \`mapstructure:"sftp"\`
160
+ GitHub struct {
161
+ Enabled bool \`mapstructure:"enabled"\`
162
+ Owner string \`mapstructure:"owner"\`
163
+ Repo string \`mapstructure:"repo"\`
164
+ Branch string \`mapstructure:"branch"\`
165
+ Path string \`mapstructure:"path"\`
166
+ Token string \`mapstructure:"token"\`
167
+ Username string \`mapstructure:"username"\`
168
+ Password string \`mapstructure:"password"\`
169
+ } \`mapstructure:"github"\`
170
+ Bootstrap struct {
171
+ Enabled bool \`mapstructure:"enabled"\`
172
+ Owner string \`mapstructure:"owner"\`
173
+ Repo string \`mapstructure:"repo"\`
174
+ Branch string \`mapstructure:"branch"\`
175
+ Token string \`mapstructure:"token"\`
176
+ Files []string \`mapstructure:"files"\`
177
+ } \`mapstructure:"bootstrap"\`
178
+ } \`mapstructure:"storage"\`
179
+
180
+ Elasticsearch struct {
181
+ Enabled bool \`mapstructure:"enabled"\`
182
+ Addresses []string \`mapstructure:"addresses"\`
183
+ Username string \`mapstructure:"username"\`
184
+ Password string \`mapstructure:"password"\`
185
+ AutoSync bool \`mapstructure:"auto_sync"\`
186
+ IndexPrefix string \`mapstructure:"index_prefix"\`
187
+ } \`mapstructure:"elasticsearch"\`
188
+
104
189
  Datasource struct {
105
190
  Host string \`mapstructure:"host"\`
106
191
  Port int \`mapstructure:"port"\`
107
192
  Username string \`mapstructure:"username"\`
108
193
  Password string \`mapstructure:"password"\`
109
194
  Database string \`mapstructure:"database"\`
195
+ SSLMode string \`mapstructure:"ssl-mode"\`
110
196
  MaxOpenConns int \`mapstructure:"max-open-conns"\`
111
197
  MaxIdleConns int \`mapstructure:"max-idle-conns"\`
112
198
  ConnMaxLifetime time.Duration \`mapstructure:"conn-max-lifetime"\`
199
+
200
+ MongoDB struct {
201
+ Enabled bool \`mapstructure:"enabled"\`
202
+ URI string \`mapstructure:"uri"\`
203
+ Database string \`mapstructure:"database"\`
204
+ } \`mapstructure:"mongodb"\`
113
205
  } \`mapstructure:"datasource"\`
206
+ Serverless struct {
207
+ Enabled bool \`mapstructure:"enabled"\`
208
+ Provider string \`mapstructure:"provider"\`
209
+ } \`mapstructure:"serverless"\`
114
210
  } \`mapstructure:"go-duck"\`
115
211
  Environment struct {
116
212
  ActiveProfile string \`mapstructure:"active_profile"\`
117
213
  } \`mapstructure:"environment"\`
118
214
  }
119
215
 
216
+ var AppConfig *Config
217
+
120
218
  func LoadConfig() (*Config, error) {
121
219
  v := viper.New()
122
220
 
@@ -137,6 +235,8 @@ func LoadConfig() (*Config, error) {
137
235
  v.SetDefault("go-duck.logging.datadog.site", "datadoghq.com")
138
236
  v.SetDefault("go-duck.messaging.mqtt.enabled", false)
139
237
  v.SetDefault("go-duck.messaging.mqtt.topic-prefix", "go-duck/events")
238
+ v.SetDefault("go-duck.messaging.nats.enabled", false)
239
+ v.SetDefault("go-duck.messaging.nats.url", "nats://localhost:4222")
140
240
  v.SetDefault("go-duck.cache.redis.enabled", false)
141
241
  v.SetDefault("go-duck.cache.redis.ttl", "10m")
142
242
  v.SetDefault("go-duck.telemetry.otel.enabled", false)
@@ -148,23 +248,65 @@ func LoadConfig() (*Config, error) {
148
248
  v.SetDefault("go-duck.resilience.circuit-breaker.enabled", true)
149
249
  v.SetDefault("go-duck.resilience.circuit-breaker.failure-threshold", 5)
150
250
  v.SetDefault("go-duck.resilience.circuit-breaker.timeout", "60s")
251
+ v.SetDefault("go-duck.storage.s3.enabled", false)
252
+ v.SetDefault("go-duck.storage.gcs.enabled", false)
253
+ v.SetDefault("go-duck.storage.minio.enabled", false)
254
+ v.SetDefault("go-duck.storage.r2.enabled", false)
255
+ v.SetDefault("go-duck.storage.generic.enabled", false)
256
+ v.SetDefault("go-duck.storage.sftp.enabled", false)
257
+ v.SetDefault("go-duck.storage.sftp.port", 22)
258
+ v.SetDefault("go-duck.storage.github.enabled", false)
259
+ v.SetDefault("go-duck.storage.github.branch", "main")
260
+ v.SetDefault("go-duck.storage.bootstrap.enabled", false)
261
+ v.SetDefault("go-duck.storage.bootstrap.branch", "main")
262
+ v.SetDefault("go-duck.elasticsearch.enabled", false)
263
+ v.SetDefault("go-duck.elasticsearch.addresses", []string{"http://localhost:9200"})
264
+ v.SetDefault("go-duck.elasticsearch.index_prefix", "go_duck_")
265
+ v.SetDefault("go-duck.elasticsearch.auto_sync", true)
266
+ v.SetDefault("go-duck.serverless.enabled", false)
267
+ v.SetDefault("go-duck.serverless.provider", "aws")
268
+ v.SetDefault("go-duck.datasource.mongodb.enabled", false)
269
+ v.SetDefault("go-duck.datasource.mongodb.uri", "mongodb://localhost:27017")
270
+
271
+ v.AutomaticEnv()
272
+ v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
151
273
 
152
274
  if err := v.ReadInConfig(); err != nil {
153
275
  return nil, err
154
276
  }
155
277
 
278
+ // Force Viper to bind environment variables for all nested keys found in the config file.
279
+ // This fixes the issue where Viper's Unmarshal ignores env vars for nested structs.
280
+ for _, key := range v.AllKeys() {
281
+ envKey := strings.ToUpper(strings.ReplaceAll(strings.ReplaceAll(key, ".", "_"), "-", "_"))
282
+ v.BindEnv(key, envKey)
283
+ }
284
+
156
285
  var config Config
157
286
  if err := v.Unmarshal(&config); err != nil {
158
287
  return nil, err
159
288
  }
289
+ AppConfig = &config
160
290
 
161
291
  return &config, nil
162
292
  }
163
293
 
294
+ func GetConfig() *Config {
295
+ return AppConfig
296
+ }
297
+
164
298
  func (c *Config) GetDSN() string {
165
299
  ds := c.GoDuck.Datasource
166
- return fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable",
167
- ds.Host, ds.Username, ds.Password, ds.Database, ds.Port)
300
+ sslMode := "disable"
301
+ if ds.SSLMode != "" {
302
+ sslMode = ds.SSLMode
303
+ }
304
+ return fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=%s",
305
+ ds.Host, ds.Username, ds.Password, ds.Database, ds.Port, sslMode)
306
+ }
307
+
308
+ func (c *Config) GetMongoURI() string {
309
+ return c.GoDuck.Datasource.MongoDB.URI
168
310
  }
169
311
  `;
170
312