go-duck-cli 1.1.49 β†’ 1.2.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.
package/README.md CHANGED
@@ -43,9 +43,9 @@ 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
- ## πŸ¦† The 375% Elite Status: Milestone Surpassed
46
+ ## πŸ¦† The 410% Elite Status: Milestone Surpassed
47
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.
48
+ GO-DUCK has officially reached the **410% 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, real-time remote bootstrapping, WSO2 integration, and full-spectrum GDL evolution.
49
49
 
50
50
  | Milestone Component | Status | Technical Value Add |
51
51
  | :--- | :--- | :--- |
@@ -57,9 +57,11 @@ GO-DUCK has officially reached the **375% Achievement Status**, evolving from a
57
57
  | **Super Admin Security Boundary** | πŸš€ **ELITE (+13%)** | Strict isolation between Business and Infrastructure Control APIs. |
58
58
  | **Silo Discovery & Privacy Proxy** | πŸš€ **ELITE (+10%)** | Silo discovery API with physical DB name masking. |
59
59
  | **Universal Storage Mesh** | πŸš€ **ELITE (+25%)** | Dynamic Hot-Swapping Registry and Distributed Cross-Scan API retrieval. |
60
- | **TOTAL ACHIEVEMENT STATUS** | πŸ† **375%** | **ELITE STATUS CONFIRMED.** πŸ‘‘ |
60
+ | **WSO2 API Gateway Integration** | πŸš€ **ELITE (+15%)** | Automated OpenAPI registration & proxy mapping. |
61
+ | **Full-Spectrum GDL Evolution** | πŸš€ **ELITE (+15%)** | Native DROP/ALTER migrations with dead-code purging. |
62
+ | **TOTAL ACHIEVEMENT STATUS** | πŸ† **410%** | **ELITE STATUS CONFIRMED.** πŸ‘‘ |
61
63
 
62
- ### ✨ Primary Features (The 375% Core)
64
+ ### ✨ Primary Features (The 410% Core)
63
65
 
64
66
  * **Federated Multi-Tenancy**: Side-by-side **Database-per-Tenant isolation** with a **Master-Tenant Registry** (Role ↔ DB ↔ Opaque UUID).
65
67
  * **Industrial-Grade Parallel Harvester**: Asynchronous goroutine-based data aggregation across multiple silos with `?federated=true` opt-in.
@@ -73,6 +75,9 @@ GO-DUCK has officially reached the **375% Achievement Status**, evolving from a
73
75
  * **Spring-style Elasticsearch Search**: Real-time sync for entities marked with `@Searchable`, supporting fuzzy matching and complex queries.
74
76
  * **SaaS Quota Engine**: Redis-backed API bandwidth tracking with dynamic, hierarchical limits (User vs. Role mapping).
75
77
  * **Resilience Layer**: Sony/Gobreaker Integration + Zero-Trust Distributed Redis Rate Limiter.
78
+ * **Comprehensive GDL Schema Evolution**: Detects structural deltas intelligently and automatically generates Goose SQL for dropped entities (`DROP TABLE`), dropped fields (`DROP COLUMN`), and altered fields (`ALTER TYPE`, constraints) to keep databases in absolute sync.
79
+ * **Dead-Code Purging**: Automatically cleans up orphaned Go models and controllers when removing entities from the `.gdl` blueprint.
80
+ * **Saga Outbox GORM Stability**: Generates robust, compile-ready `outbox_worker.go` components with native GORM package resolution.
76
81
 
77
82
  ### πŸ—ΊοΈ System Topology & Identity Registry
78
83
 
@@ -207,6 +207,15 @@ type Config struct {
207
207
  Enabled bool \`mapstructure:"enabled"\`
208
208
  Provider string \`mapstructure:"provider"\`
209
209
  } \`mapstructure:"serverless"\`
210
+ Integrations struct {
211
+ WSO2 struct {
212
+ Enabled bool \`mapstructure:"enabled"\`
213
+ PublisherURL string \`mapstructure:"publisher-url"\`
214
+ ClientID string \`mapstructure:"client-id"\`
215
+ ClientSecret string \`mapstructure:"client-secret"\`
216
+ GatewayEnvironments []string \`mapstructure:"gateway-environments"\`
217
+ } \`mapstructure:"wso2"\`
218
+ } \`mapstructure:"integrations"\`
210
219
  } \`mapstructure:"go-duck"\`
211
220
  Environment struct {
212
221
  ActiveProfile string \`mapstructure:"active_profile"\`
@@ -267,6 +276,9 @@ func LoadConfig() (*Config, error) {
267
276
  v.SetDefault("go-duck.serverless.provider", "aws")
268
277
  v.SetDefault("go-duck.datasource.mongodb.enabled", false)
269
278
  v.SetDefault("go-duck.datasource.mongodb.uri", "mongodb://localhost:27017")
279
+ v.SetDefault("go-duck.integrations.wso2.enabled", false)
280
+ v.SetDefault("go-duck.integrations.wso2.publisher-url", "https://localhost:9443/api/am/publisher/v3")
281
+ v.SetDefault("go-duck.integrations.wso2.gateway-environments", []string{"Production", "Sandbox"})
270
282
 
271
283
  v.AutomaticEnv()
272
284
  v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
@@ -79,7 +79,13 @@ DROP TABLE IF EXISTS api_usage CASCADE;
79
79
 
80
80
  const entitiesToCreate = delta ? delta.newEntities : entities;
81
81
 
82
- if (entitiesToCreate.length === 0 && (!delta || (!delta.newFields && !delta.newRelationships))) {
82
+ const hasNewFields = delta && delta.newFields && Object.keys(delta.newFields).length > 0;
83
+ const hasNewRelationships = delta && delta.newRelationships && delta.newRelationships.length > 0;
84
+ const hasDeletedEntities = delta && delta.deletedEntities && delta.deletedEntities.length > 0;
85
+ const hasDeletedFields = delta && delta.deletedFields && Object.keys(delta.deletedFields).length > 0;
86
+ const hasAlteredFields = delta && delta.alteredFields && Object.keys(delta.alteredFields).length > 0;
87
+
88
+ if (entitiesToCreate.length === 0 && !hasNewFields && !hasNewRelationships && !hasDeletedEntities && !hasDeletedFields && !hasAlteredFields) {
83
89
  return;
84
90
  }
85
91
 
@@ -125,6 +131,70 @@ DROP TABLE IF EXISTS api_usage CASCADE;
125
131
  }
126
132
  }
127
133
 
134
+ // Delete Entities
135
+ if (delta && delta.deletedEntities) {
136
+ for (const entity of delta.deletedEntities) {
137
+ sqlUp += `DROP TABLE IF EXISTS ${entity.name.toLowerCase()} CASCADE;\n\n`;
138
+ sqlDown += `-- Cannot easily reverse DROP TABLE ${entity.name.toLowerCase()}\n`;
139
+ }
140
+ }
141
+
142
+ // Delete Fields
143
+ if (delta && delta.deletedFields) {
144
+ for (const [entityName, fields] of Object.entries(delta.deletedFields)) {
145
+ for (const field of fields) {
146
+ sqlUp += `ALTER TABLE ${entityName.toLowerCase()} DROP COLUMN IF EXISTS ${toSnakeCase(field.name)};\n`;
147
+
148
+ let sqlType = toLiquibaseType(field, enums);
149
+ if (sqlType === 'JSON') sqlType = 'JSONB';
150
+ sqlDown += `ALTER TABLE ${entityName.toLowerCase()} ADD COLUMN IF NOT EXISTS ${toSnakeCase(field.name)} ${sqlType};\n`;
151
+ }
152
+ }
153
+ }
154
+
155
+ // Alter Fields
156
+ if (delta && delta.alteredFields) {
157
+ for (const [entityName, alterations] of Object.entries(delta.alteredFields)) {
158
+ for (const alt of alterations) {
159
+ const fieldName = toSnakeCase(alt.new.name);
160
+
161
+ // Type changes
162
+ if (alt.new.type !== alt.old.type) {
163
+ let newSqlType = toLiquibaseType(alt.new, enums);
164
+ if (newSqlType === 'JSON') newSqlType = 'JSONB';
165
+ let oldSqlType = toLiquibaseType(alt.old, enums);
166
+ if (oldSqlType === 'JSON') oldSqlType = 'JSONB';
167
+
168
+ sqlUp += `ALTER TABLE ${entityName.toLowerCase()} ALTER COLUMN ${fieldName} TYPE ${newSqlType} USING ${fieldName}::${newSqlType};\n`;
169
+ sqlDown += `ALTER TABLE ${entityName.toLowerCase()} ALTER COLUMN ${fieldName} TYPE ${oldSqlType} USING ${fieldName}::${oldSqlType};\n`;
170
+ }
171
+
172
+ // Required (NOT NULL) changes
173
+ if (!!alt.new.required !== !!alt.old.required) {
174
+ if (alt.new.required) {
175
+ sqlUp += `ALTER TABLE ${entityName.toLowerCase()} ALTER COLUMN ${fieldName} SET NOT NULL;\n`;
176
+ sqlDown += `ALTER TABLE ${entityName.toLowerCase()} ALTER COLUMN ${fieldName} DROP NOT NULL;\n`;
177
+ } else {
178
+ sqlUp += `ALTER TABLE ${entityName.toLowerCase()} ALTER COLUMN ${fieldName} DROP NOT NULL;\n`;
179
+ sqlDown += `ALTER TABLE ${entityName.toLowerCase()} ALTER COLUMN ${fieldName} SET NOT NULL;\n`;
180
+ }
181
+ }
182
+
183
+ // Unique changes
184
+ if (!!alt.new.unique !== !!alt.old.unique) {
185
+ const constraintName = `${entityName.toLowerCase()}_${fieldName}_key`;
186
+ if (alt.new.unique) {
187
+ sqlUp += `ALTER TABLE ${entityName.toLowerCase()} ADD CONSTRAINT ${constraintName} UNIQUE (${fieldName});\n`;
188
+ sqlDown += `ALTER TABLE ${entityName.toLowerCase()} DROP CONSTRAINT IF EXISTS ${constraintName};\n`;
189
+ } else {
190
+ sqlUp += `ALTER TABLE ${entityName.toLowerCase()} DROP CONSTRAINT IF EXISTS ${constraintName};\n`;
191
+ sqlDown += `ALTER TABLE ${entityName.toLowerCase()} ADD CONSTRAINT ${constraintName} UNIQUE (${fieldName});\n`;
192
+ }
193
+ }
194
+ }
195
+ }
196
+ }
197
+
128
198
  sqlUp += '-- +goose StatementEnd\n\n';
129
199
  sqlDown += '-- +goose StatementEnd\n';
130
200
 
@@ -133,7 +203,10 @@ DROP TABLE IF EXISTS api_usage CASCADE;
133
203
  descParts.push('initial_schema');
134
204
  } else {
135
205
  if (delta.newEntities?.length > 0) descParts.push('create_' + delta.newEntities.map(e => e.name.toLowerCase()).join('_'));
136
- if (delta.newFields) descParts.push('add_fields');
206
+ if (hasNewFields) descParts.push('add_fields');
207
+ if (hasDeletedEntities) descParts.push('drop_tables');
208
+ if (hasDeletedFields) descParts.push('drop_fields');
209
+ if (hasAlteredFields) descParts.push('alter_fields');
137
210
  }
138
211
 
139
212
  const fileName = `${timestamp}_${descParts.join('_')}.sql`;
@@ -42,6 +42,7 @@ import (
42
42
  "{{app_name}}/models"
43
43
  "{{app_name}}/config"
44
44
  "{{app_name}}/middleware"
45
+ "gorm.io/gorm"
45
46
  "gorm.io/gorm/clause"
46
47
  "gorm.io/gorm/schema"
47
48
  )
@@ -0,0 +1,156 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+
5
+ export const generateWSO2Integration = async (outputDir, config) => {
6
+ const integrationsDir = path.join(outputDir, 'integrations', 'wso2');
7
+ await fs.ensureDir(integrationsDir);
8
+
9
+ const wso2Go = `
10
+ package wso2
11
+
12
+ import (
13
+ "bytes"
14
+ "encoding/json"
15
+ "fmt"
16
+ "io"
17
+ "mime/multipart"
18
+ "net/http"
19
+ "os"
20
+ "path/filepath"
21
+ "time"
22
+
23
+ "${config.name || 'go-duck'}/config"
24
+ "${config.name || 'go-duck'}/logger"
25
+ )
26
+
27
+ type TokenResponse struct {
28
+ AccessToken string \`json:"access_token"\`
29
+ Scope string \`json:"scope"\`
30
+ TokenType string \`json:"token_type"\`
31
+ ExpiresIn int \`json:"expires_in"\`
32
+ }
33
+
34
+ func RegisterAPI(appConfig *config.Config) error {
35
+ wso2Config := appConfig.GoDuck.Integrations.WSO2
36
+ if !wso2Config.Enabled {
37
+ return nil
38
+ }
39
+
40
+ logger.Info("Starting WSO2 APIM Publisher Registration...")
41
+
42
+ // 1. Authenticate to get OAuth2 Access Token
43
+ token, err := getAccessToken(wso2Config.PublisherURL, wso2Config.ClientID, wso2Config.ClientSecret)
44
+ if err != nil {
45
+ return fmt.Errorf("failed to authenticate with WSO2: %v", err)
46
+ }
47
+
48
+ // 2. Locate swagger.json
49
+ swaggerPath := filepath.Join("docs", "swagger.json")
50
+ if _, err := os.Stat(swaggerPath); os.IsNotExist(err) {
51
+ return fmt.Errorf("swagger.json not found at %s. Cannot register with WSO2", swaggerPath)
52
+ }
53
+
54
+ // 3. Register via /apis/import-openapi
55
+ err = importOpenAPI(wso2Config.PublisherURL, token, swaggerPath)
56
+ if err != nil {
57
+ return fmt.Errorf("failed to import OpenAPI to WSO2: %v", err)
58
+ }
59
+
60
+ logger.Info("Successfully registered API with WSO2 Publisher!")
61
+ return nil
62
+ }
63
+
64
+ func getAccessToken(publisherURL, clientID, clientSecret string) (string, error) {
65
+ // WSO2 DCR/Token endpoint is typically exposed at /oauth2/token on the Gateway or KM
66
+ // If the PublisherURL is https://localhost:9443/api/am/publisher/v3,
67
+ // Token URL is typically https://localhost:9443/oauth2/token
68
+ baseURL := publisherURL[:len(publisherURL)-len("/api/am/publisher/v3")]
69
+ tokenURL := baseURL + "/oauth2/token"
70
+
71
+ data := "grant_type=client_credentials&scope=apim:api_create apim:api_publish"
72
+ req, err := http.NewRequest("POST", tokenURL, bytes.NewBufferString(data))
73
+ if err != nil {
74
+ return "", err
75
+ }
76
+
77
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
78
+ req.SetBasicAuth(clientID, clientSecret)
79
+
80
+ client := &http.Client{Timeout: 10 * time.Second}
81
+ resp, err := client.Do(req)
82
+ if err != nil {
83
+ return "", err
84
+ }
85
+ defer resp.Body.Close()
86
+
87
+ if resp.StatusCode != http.StatusOK {
88
+ bodyBytes, _ := io.ReadAll(resp.Body)
89
+ return "", fmt.Errorf("token request failed with status %d: %s", resp.StatusCode, string(bodyBytes))
90
+ }
91
+
92
+ var tokenRes TokenResponse
93
+ if err := json.NewDecoder(resp.Body).Decode(&tokenRes); err != nil {
94
+ return "", err
95
+ }
96
+
97
+ return tokenRes.AccessToken, nil
98
+ }
99
+
100
+ func importOpenAPI(publisherURL, token, swaggerPath string) error {
101
+ importURL := publisherURL + "/apis/import-openapi"
102
+
103
+ file, err := os.Open(swaggerPath)
104
+ if err != nil {
105
+ return err
106
+ }
107
+ defer file.Close()
108
+
109
+ body := &bytes.Buffer{}
110
+ writer := multipart.NewWriter(body)
111
+
112
+ // Additional data fields
113
+ _ = writer.WriteField("additionalProperties", \`{"visibility":"PUBLIC"}\`)
114
+
115
+ // File field
116
+ part, err := writer.CreateFormFile("file", filepath.Base(swaggerPath))
117
+ if err != nil {
118
+ return err
119
+ }
120
+ _, err = io.Copy(part, file)
121
+ if err != nil {
122
+ return err
123
+ }
124
+
125
+ err = writer.Close()
126
+ if err != nil {
127
+ return err
128
+ }
129
+
130
+ req, err := http.NewRequest("POST", importURL, body)
131
+ if err != nil {
132
+ return err
133
+ }
134
+
135
+ req.Header.Set("Authorization", "Bearer "+token)
136
+ req.Header.Set("Content-Type", writer.FormDataContentType())
137
+
138
+ client := &http.Client{Timeout: 30 * time.Second}
139
+ resp, err := client.Do(req)
140
+ if err != nil {
141
+ return err
142
+ }
143
+ defer resp.Body.Close()
144
+
145
+ if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
146
+ bodyBytes, _ := io.ReadAll(resp.Body)
147
+ return fmt.Errorf("import failed with status %d: %s", resp.StatusCode, string(bodyBytes))
148
+ }
149
+
150
+ return nil
151
+ }
152
+ `;
153
+
154
+ await fs.writeFile(path.join(integrationsDir, 'wso2.go'), wso2Go);
155
+ console.log(chalk.gray(' Generated WSO2 API Manager Integration client: integrations/wso2/wso2.go'));
156
+ };
package/index.js CHANGED
@@ -46,6 +46,7 @@ import { generateElasticsearchLayer } from './generators/elasticsearch.js';
46
46
  import { generateServerlessHandler } from './generators/serverless.js';
47
47
  import { generateRouterCode } from './generators/router.js';
48
48
  import { generateAIDocs } from './generators/ai_docs.js';
49
+ import { generateWSO2Integration } from './generators/wso2.js';
49
50
 
50
51
  export const generateAuditCode = async (config, outputDir) => {
51
52
  const middlewareDir = path.join(outputDir, 'middleware');
@@ -364,7 +365,7 @@ const generateEntities = async (gdlPath, outputDir, config) => {
364
365
 
365
366
  if (!await fs.pathExists(gdlPath)) {
366
367
  console.log(chalk.yellow(`⚠️ GDL path not found: ${gdlPath} - Executing Zero-Entity Infrastructure Scaffold.`));
367
- const delta = { newEntities: [], newFields: {}, newRelationships: [] };
368
+ const delta = { newEntities: [], newFields: {}, newRelationships: [], deletedEntities: [], deletedFields: {}, alteredFields: {} };
368
369
  await generateLiquibaseChangelogs(entities, relationships, outputDir, delta, enums);
369
370
  return { entities, relationships, enums, openEntities };
370
371
  }
@@ -376,7 +377,7 @@ const generateEntities = async (gdlPath, outputDir, config) => {
376
377
 
377
378
  if (files.length === 0) {
378
379
  console.log(chalk.yellow(`⚠️ No .gdl files found in: ${gdlPath} - Executing Zero-Entity Infrastructure Scaffold.`));
379
- const delta = { newEntities: [], newFields: {}, newRelationships: [] };
380
+ const delta = { newEntities: [], newFields: {}, newRelationships: [], deletedEntities: [], deletedFields: {}, alteredFields: {} };
380
381
  await generateLiquibaseChangelogs(entities, relationships, outputDir, delta, enums);
381
382
  return { entities, relationships, enums, openEntities };
382
383
  }
@@ -401,7 +402,10 @@ const generateEntities = async (gdlPath, outputDir, config) => {
401
402
  const delta = {
402
403
  newEntities: [],
403
404
  newFields: {},
404
- newRelationships: []
405
+ newRelationships: [],
406
+ deletedEntities: [],
407
+ deletedFields: {},
408
+ alteredFields: {}
405
409
  };
406
410
 
407
411
  // Calculate Delta for Incremental Migrations
@@ -415,6 +419,33 @@ const generateEntities = async (gdlPath, outputDir, config) => {
415
419
  if (newFields.length > 0) {
416
420
  delta.newFields[entity.name] = newFields;
417
421
  }
422
+
423
+ // Check for deleted fields
424
+ const deletedFields = prev.fields.filter(pf => !entity.fields.some(f => f.name === pf.name));
425
+ if (deletedFields.length > 0) {
426
+ delta.deletedFields[entity.name] = deletedFields;
427
+ }
428
+
429
+ // Check for altered fields
430
+ const alteredFields = [];
431
+ for (const f of entity.fields) {
432
+ const pf = prev.fields.find(p => p.name === f.name);
433
+ if (pf) {
434
+ if (pf.type !== f.type || !!pf.required !== !!f.required || !!pf.unique !== !!f.unique) {
435
+ alteredFields.push({ new: f, old: pf });
436
+ }
437
+ }
438
+ }
439
+ if (alteredFields.length > 0) {
440
+ delta.alteredFields[entity.name] = alteredFields;
441
+ }
442
+ }
443
+ }
444
+
445
+ // Check for deleted entities
446
+ for (const prev of previousEntities) {
447
+ if (!entities.some(e => e.name === prev.name)) {
448
+ delta.deletedEntities.push(prev);
418
449
  }
419
450
  }
420
451
 
@@ -480,6 +511,21 @@ const generateEntities = async (gdlPath, outputDir, config) => {
480
511
  await saveEntitySnapshot(outputDir, entity);
481
512
  }
482
513
 
514
+ // Clean up files for deleted entities
515
+ if (delta && delta.deletedEntities) {
516
+ for (const deleted of delta.deletedEntities) {
517
+ const modelFile = path.join(outputDir, 'models', `${deleted.name.toLowerCase()}.go`);
518
+ const controllerFile = path.join(outputDir, 'controllers', `${deleted.name.toLowerCase()}_controller.go`);
519
+ const snapshotFile = path.join(outputDir, '.go-duck', `${deleted.name.toLowerCase()}.json`);
520
+
521
+ if (await fs.pathExists(modelFile)) await fs.remove(modelFile);
522
+ if (await fs.pathExists(controllerFile)) await fs.remove(controllerFile);
523
+ if (await fs.pathExists(snapshotFile)) await fs.remove(snapshotFile);
524
+
525
+ console.log(chalk.red(` - Removed Entity & Controller: ${deleted.name}`));
526
+ }
527
+ }
528
+
483
529
  // Generate Incremental Changelogs!
484
530
  await generateLiquibaseChangelogs(entities, relationships, outputDir, delta, enums);
485
531
  console.log(chalk.green('βœ… Goose SQL incremental migrations updated!'));
@@ -575,6 +621,10 @@ program
575
621
  await generateAIDocs(config, entities, absoluteOutputDir, enums, openEntities);
576
622
  console.log(chalk.green('πŸ€– System-level AI Blueprint Documentation generated!'));
577
623
 
624
+ // 8.7 Generate WSO2 API Manager Integration
625
+ await generateWSO2Integration(absoluteOutputDir, config);
626
+ console.log(chalk.green('🌐 WSO2 API Manager Integration Module generated!'));
627
+
578
628
  // 9. Generate main.go
579
629
  const mainTemplatePath = path.resolve(__dirname, 'templates/go/main.go.hbs');
580
630
  if (await fs.pathExists(mainTemplatePath)) {
@@ -920,6 +970,15 @@ const generateYAMLConfigs = async (config, outputDir) => {
920
970
  enabled: cleanConfig.serverless?.enabled || false,
921
971
  provider: cleanConfig.serverless?.provider || 'aws'
922
972
  },
973
+ integrations: {
974
+ wso2: {
975
+ enabled: cleanConfig.integrations?.wso2?.enabled || false,
976
+ 'publisher-url': cleanConfig.integrations?.wso2?.['publisher-url'] || 'https://localhost:9443/api/am/publisher/v3',
977
+ 'client-id': cleanConfig.integrations?.wso2?.['client-id'] || '',
978
+ 'client-secret': cleanConfig.integrations?.wso2?.['client-secret'] || '',
979
+ 'gateway-environments': cleanConfig.integrations?.wso2?.['gateway-environments'] || ['Production', 'Sandbox']
980
+ }
981
+ },
923
982
  multitenancy: {
924
983
  enabled: cleanConfig.multitenancy?.enabled || false,
925
984
  'hide-silo-names': cleanConfig.multitenancy?.['hide-silo-names'] || false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "go-duck-cli",
3
- "version": "1.1.49",
3
+ "version": "1.2.1",
4
4
  "description": "The Ultimate Evolutionary Go Microservice Scaffolder.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -212,7 +212,8 @@ res, err := client.Get{{#if entities.length}}{{capitalize entities.[0].name}}{{e
212
212
  <p class="mb-4">Running <code>go-duck import-gdl</code> calculates stateful differences using the `.go-duck/` snapshot folder. It generates atomic, Go-embedded timestamped Goose SQL migrations in <code>migrations/sql/</code>.</p>
213
213
  <ul class="list-disc pl-6 space-y-2">
214
214
  <li>The application natively compiles SQL inside the Go binary via <code>go:embed</code>.</li>
215
- <li>Includes smart nullability and automatic indexing for Foreign Keys.</li>
215
+ <li><strong>Full-Spectrum Schema Evolution:</strong> The CLI detects complex deltas, generating SQL for dropped entities (`DROP TABLE`), dropped fields (`DROP COLUMN`), and altered fields (`ALTER TYPE`, constraints).</li>
216
+ <li><strong>Dead-Code Purging:</strong> The CLI natively tracks and deletes orphaned `.go` models, controllers, and state snapshots for entities you remove from the `.gdl` blueprint.</li>
216
217
  <li><strong>Needle Support:</strong> We inject `// go-duck-needle-*` markers in <code>main.go</code> and <code>grpc.go</code> for safe evolutionary code additions without destroying manual updates.</li>
217
218
  </ul>
218
219
  </section>
@@ -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 380% Elite Milestone Surpassed</p>
19
+ <p class="text-[11px] font-black text-indigo-900 uppercase tracking-[0.25em]">The 395% Elite Milestone Surpassed</p>
20
20
  </div>
21
21
 
22
22
  <!-- Heroic Logo Anchor (Enhanced) -->
@@ -59,7 +59,7 @@
59
59
  <div class="relative z-10 flex flex-col lg:flex-row items-center justify-between gap-12">
60
60
  <div class="max-w-xl text-center lg:text-left">
61
61
  <h3 class="text-[11px] font-black text-indigo-500 uppercase tracking-[0.4em] mb-4 text-center lg:text-left">Operational Readiness</h3>
62
- <h2 class="text-5xl font-black text-slate-900 tracking-tighter mb-6 italic">The 380% Paradigm Status.</h2>
62
+ <h2 class="text-5xl font-black text-slate-900 tracking-tighter mb-6 italic">The 395% Paradigm Status.</h2>
63
63
  <p class="text-slate-500 leading-relaxed font-medium">GO-DUCK has evolved from a simple generator into a high-performance distributed orchestrator, achieving industrial compliance across all silo ecosystems.</p>
64
64
  </div>
65
65
  <div class="grid grid-cols-2 gap-8 text-center bg-slate-50 p-8 rounded-[2rem] border border-slate-100 shadow-inner">
@@ -110,6 +110,16 @@
110
110
  <span class="px-4 py-1.5 bg-blue-100 text-blue-700 text-[9px] font-black rounded-lg">ZERO-LEAK PROXY</span>
111
111
  </div>
112
112
  </div>
113
+
114
+ <div class="lg:col-span-12 bg-white p-10 rounded-[2.5rem] border border-orange-100 shadow-sm hover:shadow-2xl transition-all group relative overflow-hidden cursor-crosshair bg-gradient-to-br from-white to-orange-50/30">
115
+ <p class="text-[9px] font-bold text-orange-600 uppercase tracking-widest mb-4">Elite Extension (+15%)</p>
116
+ <h4 class="text-2xl font-black text-slate-900 mb-3 tracking-tight italic">WSO2 API Gateway Integration</h4>
117
+ <p class="text-slate-600 leading-relaxed mb-8">Natively orchestrates with WSO2 API Manager using automated OAuth2 Client Credentials Flow, pushing dynamic OpenAPI specifications to Gateways at boot time.</p>
118
+ <div class="flex gap-3">
119
+ <span class="px-4 py-1.5 bg-orange-100 text-orange-700 text-[9px] font-black rounded-lg">ZERO-TOUCH PROVISIONING</span>
120
+ <span class="px-4 py-1.5 bg-orange-100 text-orange-700 text-[9px] font-black rounded-lg">WSO2 REST PROXY</span>
121
+ </div>
122
+ </div>
113
123
  </div>
114
124
  </div>
115
125
  </section>
@@ -416,7 +426,7 @@
416
426
  </div>
417
427
 
418
428
  <div class="mt-24 text-slate-500 font-mono text-[10px] uppercase tracking-[0.6em] font-black relative z-10">
419
- GO-DUCK &bull; THE 380% ELITE MILESTONE &bull; SOVEREIGN CODE ORCHESTRATION
429
+ GO-DUCK &bull; THE 395% ELITE MILESTONE &bull; SOVEREIGN CODE ORCHESTRATION
420
430
  </div>
421
431
  </div>
422
432
  </section>
@@ -81,3 +81,24 @@ class ApiProvider {
81
81
  }
82
82
  </code></pre>
83
83
  </section>
84
+
85
+ <section class="mb-10">
86
+ <h2 class="text-2xl font-bold text-gray-800 mb-4 border-b pb-2">3. WSO2 API Manager Integration</h2>
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
+
89
+ <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>
91
+ </div>
92
+
93
+ <pre><code class="language-yaml">go-duck:
94
+ integrations:
95
+ wso2:
96
+ enabled: true
97
+ publisher-url: "https://localhost:9443/api/am/publisher/v3"
98
+ client-id: "YOUR_WSO2_CLIENT_ID"
99
+ client-secret: "YOUR_WSO2_CLIENT_SECRET"
100
+ gateway-environments:
101
+ - "Production"
102
+ - "Sandbox"
103
+ </code></pre>
104
+ </section>
@@ -62,7 +62,7 @@
62
62
  <div class="flex-grow h-1.5 bg-slate-100 rounded-full overflow-hidden">
63
63
  <div class="w-[88%] h-full bg-indigo-500 shadow-[0_0_8px_rgba(99,102,241,0.5)]"></div>
64
64
  </div>
65
- <span class="text-[8px] font-black text-indigo-600 uppercase">380%</span>
65
+ <span class="text-[8px] font-black text-indigo-600 uppercase">395%</span>
66
66
  </div>
67
67
  </div>
68
68
  </div>
@@ -11,6 +11,7 @@ import (
11
11
  "{{app_name}}/internal/server"
12
12
  "{{app_name}}/internal/worker"
13
13
  "{{app_name}}/internal/repository"
14
+ "{{app_name}}/integrations/wso2"
14
15
  "gorm.io/driver/postgres"
15
16
  "gorm.io/gorm"
16
17
  )
@@ -49,7 +50,12 @@ func main() {
49
50
  go outboxWorker.Start(context.Background())
50
51
  }
51
52
 
52
- // 5. Start HTTP Server
53
+ // 5. Register with WSO2 API Manager (if enabled)
54
+ if err := wso2.RegisterAPI(appConfig); err != nil {
55
+ logger.Error("WSO2 API Manager Registration failed: %v", err)
56
+ }
57
+
58
+ // 6. Start HTTP Server
53
59
  port := fmt.Sprintf(":%d", appConfig.GoDuck.Server.Port)
54
60
  logger.Info("Starting HTTP server on %s", port)
55
61
  r.Run(port)
@@ -14,7 +14,9 @@ import (
14
14
  "gorm.io/driver/postgres"
15
15
  "gorm.io/gorm"
16
16
  "gorm.io/plugin/opentelemetry/tracing"
17
+ {{#if multitenancy_enabled}}
17
18
  "{{app_name}}/management"
19
+ {{/if}}
18
20
  "{{app_name}}/middleware"
19
21
  "{{app_name}}/controllers"
20
22
  "{{app_name}}/models"
@@ -113,6 +115,21 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
113
115
  log.Printf("Warning: Failed to connect to fallback database for migration: %v", err)
114
116
  }
115
117
  }
118
+
119
+ // 10c. Migrate all provisioned tenant silos
120
+ var tenantRoles []models.TenantRole
121
+ if err := masterDB.Find(&tenantRoles).Error; err == nil {
122
+ mgr := middleware.GetTenantManager(masterDB, appConfig)
123
+ for _, tenant := range tenantRoles {
124
+ if tenantDB, err := mgr.GetDB(tenant.DBName); err == nil {
125
+ if err := migrations.RunGoNativeMigrationsForTenant(tenantDB); err != nil {
126
+ log.Printf("Warning: Failed to run migrations on tenant %s silo %s: %v", tenant.RoleName, tenant.DBName, err)
127
+ } else {
128
+ log.Printf("βœ… Migrated tenant silo: %s", tenant.DBName)
129
+ }
130
+ }
131
+ }
132
+ }
116
133
  }
117
134
  }
118
135
 
@@ -183,12 +200,14 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
183
200
  })
184
201
 
185
202
  // Management APIs (Run-time DB onboarding)
203
+ {{#if multitenancy_enabled}}
186
204
  mgmt := r.Group("/management")
187
205
  mgmt.Use(middleware.JWTMiddleware())
188
206
  mgmt.Use(middleware.SuperAdminRoleMiddleware(appConfig))
189
207
  {
190
208
  mgmt.POST("/tenant/assign", management.CreateDatabaseAndMigrate(masterDB))
191
209
  }
210
+ {{/if}}
192
211
 
193
212
  // Open Application APIs (No Auth)
194
213
  openApi := r.Group("/open/api")
@@ -241,7 +260,9 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
241
260
  api.Use(middleware.MeteringMiddleware(masterDB))
242
261
  {
243
262
  // Silo Portfolio
263
+ {{#if multitenancy_enabled}}
244
264
  api.GET("/silos/me", management.GetMySilos(masterDB))
265
+ {{/if}}
245
266
 
246
267
  // Business Reporting APIs
247
268
  meteringCtrl := controllers.MeteringController{DB: masterDB}