go-duck-cli 1.0.6 → 1.0.8

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,22 +43,21 @@ 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
47
-
48
- * **Full-Stack Code Generation**: Generates everything from REST and gRPC APIs to the data access layer.
49
- * **Dual-Protocol APIs**: Out-of-the-box support for both REST (Gin) and gRPC (Kratos).
50
- * **Stateful Incremental Updates**: Intelligently applies changes from your GDL schema to the existing codebase.
51
- * **Rich Feature Set**: Includes support for:
52
- * CRUD APIs with pagination and filtering
53
- * GraphQL integration
54
- * Real-time communication with WebSockets and MQTT
55
- * Auditing and API usage metering
56
- * JWT-based security and OIDC integration
57
- * Distributed caching with Redis
58
- * Circuit breakers for resilience
59
- * Full-stack observability with OpenTelemetry
60
- * **Automated Documentation**: Generates a beautiful, multi-page HTML documentation portal for your project.
61
- * **Cloud-Native**: Comes with Docker support and CI/CD pipelines using GitHub Actions.
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.
62
61
 
63
62
  ## 💾 Global Installation
64
63
 
@@ -33,6 +33,7 @@ export const generateDocumentation = async (config, entities, outputDir, enums =
33
33
  { file: 'gdl', title: 'GDL Reference' },
34
34
  { file: 'cli', title: 'CLI & Code Injection' },
35
35
  { file: 'rest', title: 'REST & Search API' },
36
+ { file: 'multitenancy', title: 'Multi-Tenancy' },
36
37
  { file: 'grpc', title: 'Kratos gRPC API' },
37
38
  { file: 'graphql', title: 'GraphQL Framework' },
38
39
  { file: 'realtime', title: 'WebSockets & MQTT' },
@@ -10,14 +10,76 @@ package middleware
10
10
  import (
11
11
  "fmt"
12
12
  "net/http"
13
+ "sync"
13
14
 
14
15
  "github.com/gin-gonic/gin"
16
+ "gorm.io/driver/postgres"
15
17
  "gorm.io/gorm"
18
+ "{{app_name}}/config"
19
+ )
20
+
21
+ // TenantDBManager handles dynamic connection pooling for all tenants
22
+ type TenantDBManager struct {
23
+ masterDB *gorm.DB
24
+ configs *config.Config
25
+ conns map[string]*gorm.DB
26
+ mu sync.RWMutex
27
+ }
28
+
29
+ var (
30
+ manager *TenantDBManager
31
+ once sync.Once
16
32
  )
17
33
 
18
- func TenantMiddleware(db *gorm.DB) gin.HandlerFunc {
34
+ func GetTenantManager(db *gorm.DB, cfg *config.Config) *TenantDBManager {
35
+ once.Do(func() {
36
+ manager = &TenantDBManager{
37
+ masterDB: db,
38
+ configs: cfg,
39
+ conns: make(map[string]*gorm.DB),
40
+ }
41
+ })
42
+ return manager
43
+ }
44
+
45
+ func (m *TenantDBManager) GetDB(dbName string) (*gorm.DB, error) {
46
+ m.mu.RLock()
47
+ if db, ok := m.conns[dbName]; ok {
48
+ m.mu.RUnlock()
49
+ return db, nil
50
+ }
51
+ m.mu.RUnlock()
52
+
53
+ m.mu.Lock()
54
+ defer m.mu.Unlock()
55
+
56
+ // Double check
57
+ if db, ok := m.conns[dbName]; ok {
58
+ return db, nil
59
+ }
60
+
61
+ // Dynamic Connection Opening
62
+ ds := m.configs.GoDuck.Datasource
63
+ dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable TimeZone=UTC",
64
+ ds.Host, ds.Username, ds.Password, dbName, ds.Port)
65
+
66
+ newDB, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
67
+ if err != nil {
68
+ return nil, err
69
+ }
70
+
71
+ m.conns[dbName] = newDB
72
+ return newDB, nil
73
+ }
74
+
75
+ func TenantMiddleware(db *gorm.DB, cfg *config.Config) gin.HandlerFunc {
76
+ mgr := GetTenantManager(db, cfg)
77
+
19
78
  return func(c *gin.Context) {
20
- // 1. Get roles from JWT (previously set by JWTMiddleware)
79
+ // 1. Identification (Hint from Header)
80
+ requestedTenant := c.GetHeader("X-Tenant-ID")
81
+
82
+ // 2. Authorization (Extracted from JWT by JWTMiddleware)
21
83
  userRolesInterface, exists := c.Get("UserRoles")
22
84
  if !exists {
23
85
  c.JSON(http.StatusUnauthorized, gin.H{"error": "No roles found in token"})
@@ -32,17 +94,34 @@ func TenantMiddleware(db *gorm.DB) gin.HandlerFunc {
32
94
  return
33
95
  }
34
96
 
35
- // 2. Lookup DB name for the roles in tenant_roles table
97
+ // 3. Resolution (Which DB is this role authorized to access?)
36
98
  var dbName string
37
99
  err := db.Raw("SELECT db_name FROM tenant_roles WHERE role_name IN ? LIMIT 1", roles).Scan(&dbName).Error
38
100
 
39
101
  if err != nil || dbName == "" {
40
- // Fallback to default DB if no role match (optional behavior)
41
- dbName = "go-duck"
102
+ c.JSON(http.StatusForbidden, gin.H{"error": "Security: No tenant context mapped to user roles"})
103
+ c.Abort()
104
+ return
105
+ }
106
+
107
+ // 4. Security Check (Prevent Cross-Tenant Spoofing)
108
+ if requestedTenant != "" && requestedTenant != dbName {
109
+ c.JSON(http.StatusForbidden, gin.H{"error": "Security Breach: Header/Token tenant mismatch"})
110
+ c.Abort()
111
+ return
112
+ }
113
+
114
+ // 5. Dynamic Switching (Get or Create the DB Connection)
115
+ tenantConn, err := mgr.GetDB(dbName)
116
+ if err != nil {
117
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to resolve tenant database connection"})
118
+ c.Abort()
119
+ return
42
120
  }
43
121
 
44
- // 3. Store tenant info for downstream use
122
+ // 6. Inject Live Connection into Context
45
123
  c.Set("tenantDB", dbName)
124
+ c.Set("tenantDBConn", tenantConn)
46
125
  c.Next()
47
126
  }
48
127
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "go-duck-cli",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "go function generator",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -0,0 +1,83 @@
1
+ <div class="mb-10 text-center lg:text-left border-b border-slate-200 pb-8">
2
+ <div class="inline-flex items-center px-3 py-1 rounded-full bg-blue-100 text-blue-700 text-xs font-semibold tracking-wide uppercase mb-4">
3
+ Enterprise Architecture
4
+ </div>
5
+ <h1 class="text-4xl lg:text-5xl font-extrabold text-slate-900 tracking-tight leading-tight mb-4">Dynamic Multi-Tenancy</h1>
6
+ <p class="text-lg lg:text-xl text-slate-600 max-w-2xl leading-relaxed">Secure, high-performance database-per-tenant isolation with real-time connection hot-swapping.</p>
7
+ </div>
8
+
9
+ <section class="mb-12">
10
+ <h2 class="text-2xl font-bold text-slate-800 mb-6 flex items-center">
11
+ <span class="w-8 h-8 rounded-lg bg-blue-100 text-blue-600 flex items-center justify-center mr-3 text-sm">
12
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path></svg>
13
+ </span>
14
+ The Dual-DB Architecture
15
+ </h2>
16
+ <p class="mb-6 text-slate-600 leading-relaxed">GO-DUCK separates management logic from customer data. Your application maintains a persistent connection to a <strong>Master Database</strong> while dynamically "Relocating" per-request traffic to isolated <strong>Tenant Databases</strong>.</p>
17
+
18
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
19
+ <div class="p-6 rounded-2xl border border-slate-200 bg-white shadow-sm">
20
+ <div class="w-12 h-12 bg-indigo-100 rounded-xl flex items-center justify-center mb-4">
21
+ <svg class="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>
22
+ </div>
23
+ <h3 class="text-xl font-bold text-slate-900 mb-2">Master Registry</h3>
24
+ <p class="text-slate-600 text-sm leading-relaxed">The <code>tenant_roles</code> table in the Master DB acts as the "Grand Concierge," mapping authenticated user roles to physical database names at runtime.</p>
25
+ </div>
26
+ <div class="p-6 rounded-2xl border border-slate-200 bg-white shadow-sm">
27
+ <div class="w-12 h-12 bg-emerald-100 rounded-xl flex items-center justify-center mb-4">
28
+ <svg class="w-6 h-6 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
29
+ </div>
30
+ <h3 class="text-xl font-bold text-slate-900 mb-2">Hot-Swapping Pools</h3>
31
+ <p class="text-slate-600 text-sm leading-relaxed">The <code>TenantDBManager</code> uses lazy loading to open and cache connection pools for tenants only when they are active, ensuring minimal resource overhead.</p>
32
+ </div>
33
+ </div>
34
+ </section>
35
+
36
+ <section class="mb-12">
37
+ <h2 class="text-2xl font-bold text-slate-800 mb-6 flex items-center">
38
+ <span class="w-8 h-8 rounded-lg bg-rose-100 text-rose-600 flex items-center justify-center mr-3 text-sm">
39
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 00-2 2zm10-10V7a4 4 0 00-8 0v4h8z"></path></svg>
40
+ </span>
41
+ Security & Anti-Spoofing
42
+ </h2>
43
+ <p class="mb-6 text-slate-600 leading-relaxed">Header-based tenancy (<code>X-Tenant-ID</code>) is convenient but dangerous. GO-DUCK solves this by cryptographically validating the tenant context against the JWT.</p>
44
+
45
+ <div class="bg-slate-900 rounded-2xl p-6 shadow-xl mb-6">
46
+ <h4 class="text-emerald-400 font-mono text-sm mb-4">// Internal Verification Loop</h4>
47
+ <pre class="text-slate-300 font-mono text-sm leading-relaxed">
48
+ 1. Extract Roles from signed Keycloak JWT
49
+ 2. Lookup authorized DB in Master mapping table
50
+ 3. Compare against X-Tenant-ID header
51
+ 4. If Mismatch: Return 403 Forbidden (Security Breach)
52
+ 5. If Match: Activate Dynamic Connection Pool
53
+ </pre>
54
+ </div>
55
+ </section>
56
+
57
+ <section class="mb-12">
58
+ <h2 class="text-2xl font-bold text-slate-800 mb-6 flex items-center">
59
+ <span class="w-8 h-8 rounded-lg bg-amber-100 text-amber-600 flex items-center justify-center mr-3 text-sm">
60
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"></path></svg>
61
+ </span>
62
+ Tenant Provisioning API
63
+ </h2>
64
+ <p class="mb-6 text-slate-600 leading-relaxed">Adding a new customer is a single atomic operation. Use the management API to provision a side-by-side database instantly.</p>
65
+
66
+ <div class="bg-[#1e1e1e] rounded-xl overflow-hidden shadow-lg mb-4">
67
+ <div class="bg-[#2d2d2d] px-4 py-2 border-b border-[#404040] flex items-center">
68
+ <span class="px-2 py-0.5 rounded bg-emerald-500/20 text-emerald-400 text-[10px] font-bold mr-3">POST</span>
69
+ <span class="text-xs text-slate-300 font-mono">/management/db/create</span>
70
+ </div>
71
+ <div class="p-5 text-sm font-mono text-slate-300">
72
+ {
73
+ "role": "ROLE_ACME_CORP",
74
+ "db_name": "acme_isolated_db"
75
+ }
76
+ </div>
77
+ </div>
78
+ <ul class="space-y-3 text-slate-600 text-sm">
79
+ <li class="flex items-center"><svg class="w-4 h-4 text-emerald-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg> Executing physical <code>CREATE DATABASE</code></li>
80
+ <li class="flex items-center"><svg class="w-4 h-4 text-emerald-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg> Mapping role to DB in <code>tenant_roles</code> Master table</li>
81
+ <li class="flex items-center"><svg class="w-4 h-4 text-emerald-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg> Triggering automated Liquibase migrations on new DB</li>
82
+ </ul>
83
+ </section>
@@ -9,7 +9,27 @@
9
9
  <p class="text-indigo-900"><strong>Golden Rule:</strong> In <code>application-dev.yml</code>, ensure your Keycloak Realm, ClientID, and Secret are accurately synced with the local running Docker Keycloak image.</p>
10
10
  </div>
11
11
 
12
- <h3 class="font-semibold mb-2 mt-6">2. Burst Protection (Rate Limiting)</h3>
12
+ <h2 class="text-2xl font-bold text-gray-800 mb-4 border-b pb-2">2. Tenant Context Integrity</h2>
13
+ <p class="mb-4">
14
+ GO-DUCK implements <strong>Enterprise-Grade Data Isolation</strong>. While the <code>X-Tenant-ID</code> header is required for request context, the system is immune to "Tenant Spoofing" attacks.
15
+ </p>
16
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
17
+ <div class="bg-slate-50 p-5 rounded-xl border border-slate-200">
18
+ <h4 class="font-bold text-slate-900 mb-2">Internal Cross-Verification</h4>
19
+ <p class="text-xs text-slate-600 leading-relaxed">
20
+ The <code>TenantMiddleware</code> extracts authorized roles from the signed JWT and performs a server-side lookup of the valid DB context. It then <strong>cross-checks</strong> this against the <code>X-Tenant-ID</code> header.
21
+ </p>
22
+ </div>
23
+ <div class="bg-rose-50 p-5 rounded-xl border border-rose-200">
24
+ <h4 class="font-bold text-rose-900 mb-2">Spoof Protection</h4>
25
+ <p class="text-xs text-rose-700 leading-relaxed">
26
+ If a user attempts to access <code>Tenant-B</code> by modifying headers while their JWT is only valid for <code>Tenant-A</code>, the request is immediately aborted with a <code>403 Forbidden - Security Breach</code> error.
27
+ <a href="multitenancy.html" class="text-rose-600 font-bold hover:underline mt-2 inline-block">Learn more about isolation &rarr;</a>
28
+ </p>
29
+ </div>
30
+ </div>
31
+
32
+ <h3 class="font-semibold mb-2 mt-6">3. Burst Protection (Rate Limiting)</h3>
13
33
  <p class="mb-4">Using the standard <code>x/time/rate</code> package, a Token Bucket rate limiter is attached to the Gin Engine globally to mitigate DDOS vectors and abusive scripting.</p>
14
34
  <pre><code class="language-yaml"># Inside your application-prod.yml
15
35
  go-duck:
@@ -21,20 +21,20 @@ Config *config.Config
21
21
  }
22
22
 
23
23
  // Create{{capitalize name}}
24
- func (ctrl *{{capitalize name}}Controller) Create(c *gin.Context) {
25
- tenant, _ := c.Get("tenantDB")
26
- tenantStr := fmt.Sprintf("%v", tenant)
27
- ctx := c.Request.Context()
24
+ db := ctrl.DB
25
+ if tdb, exists := c.Get("tenantDBConn"); exists {
26
+ db = tdb.(*gorm.DB)
27
+ }
28
28
 
29
- var entity models.{{capitalize name}}
30
- if err := c.ShouldBindJSON(&entity); err != nil {
31
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
32
- return
33
- }
34
- if err := ctrl.DB.WithContext(ctx).Create(&entity).Error; err != nil {
35
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
36
- return
37
- }
29
+ var entity models.{{capitalize name}}
30
+ if err := c.ShouldBindJSON(&entity); err != nil {
31
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
32
+ return
33
+ }
34
+ if err := db.WithContext(ctx).Create(&entity).Error; err != nil {
35
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
36
+ return
37
+ }
38
38
 
39
39
  // Dynamic Cache Invalidation (Tenant Aware)
40
40
  cache.ClearPattern(tenantStr + ":{{capitalize name}}*")
@@ -50,9 +50,13 @@ c.JSON(http.StatusCreated, entity)
50
50
 
51
51
  // GetAll{{capitalize name}}s (with filtering, pagination, and lazy/eager loading)
52
52
  func (ctrl *{{capitalize name}}Controller) GetAll(c *gin.Context) {
53
- var entities []models.{{capitalize name}}
54
- ctx := c.Request.Context()
55
- query := ctrl.DB.WithContext(ctx)
53
+ db := ctrl.DB
54
+ if tdb, exists := c.Get("tenantDBConn"); exists {
55
+ db = tdb.(*gorm.DB)
56
+ }
57
+ var entities []models.{{capitalize name}}
58
+ ctx := c.Request.Context()
59
+ query := db.WithContext(ctx)
56
60
 
57
61
  // 1. Pagination
58
62
  page, _ := strconv.Atoi(c.DefaultQuery("page", "0"))
@@ -104,8 +108,12 @@ c.JSON(http.StatusOK, entity)
104
108
  return
105
109
  }
106
110
 
107
- // 2. Fallback to DB (With Context for Tracing)
108
- query := ctrl.DB.WithContext(ctx)
111
+ // 2. Fallback to DB (With Context for Tracing)
112
+ db := ctrl.DB
113
+ if tdb, exists := c.Get("tenantDBConn"); exists {
114
+ db = tdb.(*gorm.DB)
115
+ }
116
+ query := db.WithContext(ctx)
109
117
  if c.Query("eager") == "true" {
110
118
  {{#each relationships}}
111
119
  query = query.Preload("{{capitalize from.field}}")
@@ -128,14 +136,13 @@ c.JSON(http.StatusOK, entity)
128
136
  }
129
137
 
130
138
  // Update (PUT) - Full Update
131
- func (ctrl *{{capitalize name}}Controller) Update(c *gin.Context) {
132
- id := c.Param("id")
133
- tenant, _ := c.Get("tenantDB")
134
- tenantStr := fmt.Sprintf("%v", tenant)
135
- ctx := c.Request.Context()
139
+ db := ctrl.DB
140
+ if tdb, exists := c.Get("tenantDBConn"); exists {
141
+ db = tdb.(*gorm.DB)
142
+ }
136
143
 
137
- var entity models.{{capitalize name}}
138
- if err := ctrl.DB.WithContext(ctx).First(&entity, id).Error; err != nil {
144
+ var entity models.{{capitalize name}}
145
+ if err := db.WithContext(ctx).First(&entity, id).Error; err != nil {
139
146
  c.JSON(http.StatusNotFound, gin.H{"error": "Resource not found"})
140
147
  return
141
148
  }
@@ -148,7 +155,7 @@ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
148
155
  return
149
156
  }
150
157
 
151
- if err := ctrl.DB.WithContext(ctx).Save(&entity).Error; err != nil {
158
+ if err := db.WithContext(ctx).Save(&entity).Error; err != nil {
152
159
  c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
153
160
  return
154
161
  }
@@ -166,14 +173,13 @@ c.JSON(http.StatusOK, entity)
166
173
  }
167
174
 
168
175
  // Patch (PATCH) - Partial Update
169
- func (ctrl *{{capitalize name}}Controller) Patch(c *gin.Context) {
170
- id := c.Param("id")
171
- tenant, _ := c.Get("tenantDB")
172
- tenantStr := fmt.Sprintf("%v", tenant)
173
- ctx := c.Request.Context()
176
+ db := ctrl.DB
177
+ if tdb, exists := c.Get("tenantDBConn"); exists {
178
+ db = tdb.(*gorm.DB)
179
+ }
174
180
 
175
- var entity models.{{capitalize name}}
176
- if err := ctrl.DB.WithContext(ctx).First(&entity, id).Error; err != nil {
181
+ var entity models.{{capitalize name}}
182
+ if err := db.WithContext(ctx).First(&entity, id).Error; err != nil {
177
183
  c.JSON(http.StatusNotFound, gin.H{"error": "Resource not found"})
178
184
  return
179
185
  }
@@ -185,13 +191,13 @@ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
185
191
  return
186
192
  }
187
193
 
188
- if err := ctrl.DB.WithContext(ctx).Model(&entity).Updates(updates).Error; err != nil {
194
+ if err := db.WithContext(ctx).Model(&entity).Updates(updates).Error; err != nil {
189
195
  c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
190
196
  return
191
197
  }
192
198
 
193
199
  // Fetch updated
194
- ctrl.DB.WithContext(ctx).First(&entity, id)
200
+ db.WithContext(ctx).First(&entity, id)
195
201
 
196
202
  // Cache Invalidation (Tenant Aware)
197
203
  cache.Delete(fmt.Sprintf("%s:{{capitalize name}}:%s", tenantStr, id))
@@ -206,10 +212,10 @@ c.JSON(http.StatusOK, gin.H{"message": "Updated successfully", "data": entity})
206
212
  }
207
213
 
208
214
  // BulkCreate handles creating multiple entities in one transaction
209
- func (ctrl *{{capitalize name}}Controller) BulkCreate(c *gin.Context) {
210
- tenant, _ := c.Get("tenantDB")
211
- tenantStr := fmt.Sprintf("%v", tenant)
212
- ctx := c.Request.Context()
215
+ db := ctrl.DB
216
+ if tdb, exists := c.Get("tenantDBConn"); exists {
217
+ db = tdb.(*gorm.DB)
218
+ }
213
219
 
214
220
  var entities []models.{{capitalize name}}
215
221
  if err := c.ShouldBindJSON(&entities); err != nil {
@@ -217,7 +223,7 @@ func (ctrl *{{capitalize name}}Controller) BulkCreate(c *gin.Context) {
217
223
  return
218
224
  }
219
225
 
220
- if err := ctrl.DB.WithContext(ctx).Create(&entities).Error; err != nil {
226
+ if err := db.WithContext(ctx).Create(&entities).Error; err != nil {
221
227
  c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
222
228
  return
223
229
  }
@@ -235,10 +241,10 @@ func (ctrl *{{capitalize name}}Controller) BulkCreate(c *gin.Context) {
235
241
  }
236
242
 
237
243
  // BulkUpdate handles updating multiple entities in one transaction
238
- func (ctrl *{{capitalize name}}Controller) BulkUpdate(c *gin.Context) {
239
- tenant, _ := c.Get("tenantDB")
240
- tenantStr := fmt.Sprintf("%v", tenant)
241
- ctx := c.Request.Context()
244
+ db := ctrl.DB
245
+ if tdb, exists := c.Get("tenantDBConn"); exists {
246
+ db = tdb.(*gorm.DB)
247
+ }
242
248
 
243
249
  var entities []models.{{capitalize name}}
244
250
  if err := c.ShouldBindJSON(&entities); err != nil {
@@ -246,7 +252,7 @@ func (ctrl *{{capitalize name}}Controller) BulkUpdate(c *gin.Context) {
246
252
  return
247
253
  }
248
254
 
249
- err := ctrl.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
255
+ err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
250
256
  for _, e := range entities {
251
257
  if err := tx.Save(&e).Error; err != nil {
252
258
  return err
@@ -271,10 +277,10 @@ func (ctrl *{{capitalize name}}Controller) BulkUpdate(c *gin.Context) {
271
277
  }
272
278
 
273
279
  // BulkPatch handles partial updates for multiple entities
274
- func (ctrl *{{capitalize name}}Controller) BulkPatch(c *gin.Context) {
275
- tenant, _ := c.Get("tenantDB")
276
- tenantStr := fmt.Sprintf("%v", tenant)
277
- ctx := c.Request.Context()
280
+ db := ctrl.DB
281
+ if tdb, exists := c.Get("tenantDBConn"); exists {
282
+ db = tdb.(*gorm.DB)
283
+ }
278
284
 
279
285
  var updates []struct {
280
286
  ID uint `json:"id"`
@@ -285,7 +291,7 @@ func (ctrl *{{capitalize name}}Controller) BulkPatch(c *gin.Context) {
285
291
  return
286
292
  }
287
293
 
288
- err := ctrl.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
294
+ err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
289
295
  for _, u := range updates {
290
296
  if err := tx.Model(&models.{{capitalize name}}{}).Where("id = ?", u.ID).Updates(u.Changes).Error; err != nil {
291
297
  return err
@@ -304,19 +310,18 @@ func (ctrl *{{capitalize name}}Controller) BulkPatch(c *gin.Context) {
304
310
  }
305
311
 
306
312
  // Delete
307
- func (ctrl *{{capitalize name}}Controller) Delete(c *gin.Context) {
308
- id := c.Param("id")
309
- tenant, _ := c.Get("tenantDB")
310
- tenantStr := fmt.Sprintf("%v", tenant)
311
- ctx := c.Request.Context()
313
+ db := ctrl.DB
314
+ if tdb, exists := c.Get("tenantDBConn"); exists {
315
+ db = tdb.(*gorm.DB)
316
+ }
312
317
 
313
- var entity models.{{capitalize name}}
314
- if err := ctrl.DB.WithContext(ctx).First(&entity, id).Error; err != nil {
318
+ var entity models.{{capitalize name}}
319
+ if err := db.WithContext(ctx).First(&entity, id).Error; err != nil {
315
320
  c.JSON(http.StatusNotFound, gin.H{"error": "Resource not found"})
316
321
  return
317
322
  }
318
323
 
319
- if err := ctrl.DB.WithContext(ctx).Delete(&entity).Error; err != nil {
324
+ if err := db.WithContext(ctx).Delete(&entity).Error; err != nil {
320
325
  c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
321
326
  return
322
327
  }
@@ -143,7 +143,7 @@ mgmt.POST("/db/create", management.CreateDatabaseAndMigrate(masterDB))
143
143
  // 9. Secured Application APIs
144
144
  api := r.Group("/api")
145
145
  api.Use(middleware.JWTMiddleware())
146
- api.Use(middleware.TenantMiddleware(masterDB))
146
+ api.Use(middleware.TenantMiddleware(masterDB, appConfig))
147
147
  api.Use(middleware.AuditMiddleware(masterDB))
148
148
  api.Use(middleware.MeteringMiddleware(masterDB))
149
149
  {