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 +15 -16
- package/generators/docs.js +1 -0
- package/generators/multitenancy.js +85 -6
- package/package.json +1 -1
- package/templates/docs/pages/multitenancy.hbs +83 -0
- package/templates/docs/pages/security.hbs +21 -1
- package/templates/go/controller.go.hbs +63 -58
- package/templates/go/main.go.hbs +1 -1
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
|
|
49
|
-
* **Dual-Protocol APIs**:
|
|
50
|
-
* **
|
|
51
|
-
* **
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* Circuit
|
|
59
|
-
* Full-stack
|
|
60
|
-
* **Automated Documentation**:
|
|
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
|
|
package/generators/docs.js
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
//
|
|
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
|
@@ -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
|
-
<
|
|
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 →</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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 :=
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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 :=
|
|
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 :=
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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 :=
|
|
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 :=
|
|
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
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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 :=
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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 :=
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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 :=
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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 :=
|
|
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 :=
|
|
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
|
}
|
package/templates/go/main.go.hbs
CHANGED
|
@@ -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
|
{
|