go-duck-cli 1.2.0 β†’ 1.2.11

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.
@@ -70,9 +72,12 @@ GO-DUCK has officially reached the **375% Achievement Status**, evolving from a
70
72
  * **Distributed Saga Consistency**: Integrated **Transactional Outbox** pattern and background workers in every silo to guarantee eventual consistency across the federation.
71
73
  * **Zero-Trust Identity Registry**: Decoupled mapping layer ensuring physical database names and internal IDs never leak to the client.
72
74
  * **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.
75
+ * **Spring-style Elasticsearch Search**: Real-time sync for entities marked with `@Searchable`, supporting native `query_string` syntax (wildcards like `*`, booleans, ranges, and fuzzy matching).
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
 
@@ -156,10 +156,9 @@ func ExecuteSearch(ctx context.Context, entityName string, queryStr string, cfg
156
156
  } else {
157
157
  query = map[string]interface{}{
158
158
  "query": map[string]interface{}{
159
- "multi_match": map[string]interface{}{
160
- "query": queryStr,
161
- "fields": []string{"*"},
162
- "fuzziness": "AUTO",
159
+ "query_string": map[string]interface{}{
160
+ "query": queryStr,
161
+ "analyze_wildcard": true,
163
162
  },
164
163
  },
165
164
  }
@@ -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
  )
package/index.js CHANGED
@@ -365,7 +365,7 @@ const generateEntities = async (gdlPath, outputDir, config) => {
365
365
 
366
366
  if (!await fs.pathExists(gdlPath)) {
367
367
  console.log(chalk.yellow(`⚠️ GDL path not found: ${gdlPath} - Executing Zero-Entity Infrastructure Scaffold.`));
368
- const delta = { newEntities: [], newFields: {}, newRelationships: [] };
368
+ const delta = { newEntities: [], newFields: {}, newRelationships: [], deletedEntities: [], deletedFields: {}, alteredFields: {} };
369
369
  await generateLiquibaseChangelogs(entities, relationships, outputDir, delta, enums);
370
370
  return { entities, relationships, enums, openEntities };
371
371
  }
@@ -377,7 +377,7 @@ const generateEntities = async (gdlPath, outputDir, config) => {
377
377
 
378
378
  if (files.length === 0) {
379
379
  console.log(chalk.yellow(`⚠️ No .gdl files found in: ${gdlPath} - Executing Zero-Entity Infrastructure Scaffold.`));
380
- const delta = { newEntities: [], newFields: {}, newRelationships: [] };
380
+ const delta = { newEntities: [], newFields: {}, newRelationships: [], deletedEntities: [], deletedFields: {}, alteredFields: {} };
381
381
  await generateLiquibaseChangelogs(entities, relationships, outputDir, delta, enums);
382
382
  return { entities, relationships, enums, openEntities };
383
383
  }
@@ -402,7 +402,10 @@ const generateEntities = async (gdlPath, outputDir, config) => {
402
402
  const delta = {
403
403
  newEntities: [],
404
404
  newFields: {},
405
- newRelationships: []
405
+ newRelationships: [],
406
+ deletedEntities: [],
407
+ deletedFields: {},
408
+ alteredFields: {}
406
409
  };
407
410
 
408
411
  // Calculate Delta for Incremental Migrations
@@ -416,6 +419,33 @@ const generateEntities = async (gdlPath, outputDir, config) => {
416
419
  if (newFields.length > 0) {
417
420
  delta.newFields[entity.name] = newFields;
418
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);
419
449
  }
420
450
  }
421
451
 
@@ -481,6 +511,21 @@ const generateEntities = async (gdlPath, outputDir, config) => {
481
511
  await saveEntitySnapshot(outputDir, entity);
482
512
  }
483
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
+
484
529
  // Generate Incremental Changelogs!
485
530
  await generateLiquibaseChangelogs(entities, relationships, outputDir, delta, enums);
486
531
  console.log(chalk.green('βœ… Goose SQL incremental migrations updated!'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "go-duck-cli",
3
- "version": "1.2.0",
3
+ "version": "1.2.11",
4
4
  "description": "The Ultimate Evolutionary Go Microservice Scaffolder.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -81,6 +81,17 @@ DELETE /api/{{#if entities.length}}{{toLowerCase entities.[0].name}}s{{else}}ent
81
81
  -H "Authorization: Bearer YOUR_JWT" \
82
82
  -H "X-Tenant-ID: tenant_1"</code></pre>
83
83
  <p class="text-sm text-gray-500 mt-2">Supported operators: <code>eq, neq, gt, gte, lt, lte, like, ilike</code></p>
84
+
85
+ <h3 class="font-semibold mb-2 mt-6">Elasticsearch Global Search (Spring-style):</h3>
86
+ <p class="mb-2">For entities marked with <code>@Searchable</code>, use the native Elasticsearch endpoint for advanced queries (wildcards, booleans, and ranges).</p>
87
+ <pre><code class="language-bash">curl -G "http://localhost:{{serverPort}}/api/search/{{#if entities.length}}{{toLowerCase entities.[0].name}}{{else}}entity{{/if}}" \
88
+ --data-urlencode "q=name:John AND age:>18" \
89
+ -H "Authorization: Bearer YOUR_JWT"</code></pre>
90
+ <ul class="text-sm text-gray-500 mt-2 list-disc pl-5">
91
+ <li><strong>Wildcards:</strong> <code>q=name*</code> (matches "name15", "name_abc")</li>
92
+ <li><strong>Boolean Logic:</strong> <code>q=status:PUBLISHED AND (author:John OR author:Jane)</code></li>
93
+ <li><strong>Ranges:</strong> <code>q=age:[18 TO 30]</code> or <code>q=created_at:>2023-01-01</code></li>
94
+ </ul>
84
95
  </section>
85
96
 
86
97
  <!-- Audit & Metering -->
@@ -212,7 +223,8 @@ res, err := client.Get{{#if entities.length}}{{capitalize entities.[0].name}}{{e
212
223
  <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
224
  <ul class="list-disc pl-6 space-y-2">
214
225
  <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>
226
+ <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>
227
+ <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
228
  <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
229
  </ul>
218
230
  </section>
@@ -115,6 +115,21 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
115
115
  log.Printf("Warning: Failed to connect to fallback database for migration: %v", err)
116
116
  }
117
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
+ }
118
133
  }
119
134
  }
120
135