go-duck-cli 1.0.9 → 1.1.12
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 +30 -15
- package/generators/ai_docs.js +130 -0
- package/generators/broker.js +63 -0
- package/generators/config.js +149 -7
- package/generators/devops.js +210 -43
- package/generators/docs.js +23 -4
- package/generators/elasticsearch.js +263 -0
- package/generators/kratos.js +229 -41
- package/generators/metering.js +280 -48
- package/generators/migrations.js +92 -198
- package/generators/mqtt.js +2 -39
- package/generators/multitenancy.js +274 -71
- package/generators/nats.js +39 -0
- package/generators/outbox.js +171 -0
- package/generators/postgrest.js +7 -3
- package/generators/postman.js +405 -0
- package/generators/repository.js +27 -0
- package/generators/router.js +27 -0
- package/generators/security.js +95 -14
- package/generators/serverless.js +147 -0
- package/generators/storage.js +589 -0
- package/generators/swagger.js +84 -60
- package/generators/telemetry.js +23 -32
- package/generators/websocket.js +55 -21
- package/index.js +493 -116
- package/package.json +6 -4
- package/parser/gdl.js +163 -24
- package/templates/docs/index.html.hbs +5 -5
- package/templates/docs/layout.hbs +221 -62
- package/templates/docs/pages/audit.hbs +83 -35
- package/templates/docs/pages/cli.hbs +18 -0
- package/templates/docs/pages/configuration.hbs +241 -0
- package/templates/docs/pages/datadog.hbs +46 -0
- package/templates/docs/pages/elasticsearch.hbs +121 -0
- package/templates/docs/pages/federation.hbs +241 -0
- package/templates/docs/pages/gdl-advanced.hbs +91 -0
- package/templates/docs/pages/gdl-annotations.hbs +137 -0
- package/templates/docs/pages/gdl-entities.hbs +134 -0
- package/templates/docs/pages/gdl-relationships.hbs +80 -0
- package/templates/docs/pages/gdl.hbs +60 -204
- package/templates/docs/pages/graphql.hbs +58 -44
- package/templates/docs/pages/grpc.hbs +53 -90
- package/templates/docs/pages/hybrid-store.hbs +127 -0
- package/templates/docs/pages/index.hbs +418 -149
- package/templates/docs/pages/keycloak.hbs +43 -0
- package/templates/docs/pages/legend.hbs +116 -0
- package/templates/docs/pages/mosquitto.hbs +39 -0
- package/templates/docs/pages/multitenancy.hbs +139 -71
- package/templates/docs/pages/otel.hbs +40 -0
- package/templates/docs/pages/realtime.hbs +38 -12
- package/templates/docs/pages/redis.hbs +40 -0
- package/templates/docs/pages/rest.hbs +120 -202
- package/templates/docs/pages/saga.hbs +94 -0
- package/templates/docs/pages/security.hbs +150 -44
- package/templates/docs/pages/serverless.hbs +157 -0
- package/templates/docs/pages/storage.hbs +127 -0
- package/templates/docs/pages/wizard.hbs +683 -0
- package/templates/docs/triple_identity_registry.png +0 -0
- package/templates/go/controller.go.hbs +287 -283
- package/templates/go/entity.go.hbs +17 -15
- package/templates/go/main.go.hbs +47 -180
- package/templates/go/migrator.go.hbs +65 -0
- package/templates/go/router.go.hbs +272 -0
- package/templates/graphql/resolver.go.hbs +53 -34
- package/templates/graphql/schema.graphql.hbs +17 -5
- package/templates/kratos/service.go.hbs +169 -34
- package/templates/proto/entity.proto.hbs +10 -14
- package/test_nested.gdl +21 -0
- package/templates/docs/intro.mp4 +0 -0
- package/test_parser.js +0 -9
package/generators/metering.js
CHANGED
|
@@ -19,63 +19,228 @@ import (
|
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
type APIUsage struct {
|
|
22
|
-
ID
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
ID uint \`gorm:"primaryKey" json:"id"\`
|
|
23
|
+
TargetType string \`json:"targetType" gorm:"index:idx_target_api,unique"\` // "user" or "role"
|
|
24
|
+
TargetID string \`json:"targetId" gorm:"index:idx_target_api,unique"\` // KeycloakID or Role Name
|
|
25
|
+
APIPath string \`json:"apiPath" gorm:"index:idx_target_api,unique"\`
|
|
26
|
+
TenantDB string \`json:"tenantDb" gorm:"index:idx_target_api,unique"\`
|
|
27
|
+
UsageCount int64 \`json:"usageCount"\`
|
|
28
|
+
MaxLimit int64 \`json:"maxLimit" gorm:"default:1000"\`
|
|
29
|
+
DurationDays int \`json:"durationDays" gorm:"default:30"\` // Duration in Days
|
|
30
|
+
AutoReset bool \`json:"autoReset" gorm:"default:true"\` // Lock them until they pay if false!
|
|
31
|
+
WindowStart time.Time \`json:"windowStart"\` // When did the current quota period start?
|
|
27
32
|
LastAccessed time.Time \`json:"lastAccessed"\`
|
|
28
33
|
}
|
|
34
|
+
|
|
35
|
+
type APIUsageHistory struct {
|
|
36
|
+
ID uint \`gorm:"primaryKey" json:"id"\`
|
|
37
|
+
TargetType string \`json:"targetType" gorm:"index:idx_hist_target_api"\`
|
|
38
|
+
TargetID string \`json:"targetId" gorm:"index:idx_hist_target_api"\`
|
|
39
|
+
APIPath string \`json:"apiPath" gorm:"index:idx_hist_target_api"\`
|
|
40
|
+
TenantDB string \`json:"tenantDb" gorm:"index:idx_hist_target_api"\`
|
|
41
|
+
FinalUsage int64 \`json:"finalUsage"\`
|
|
42
|
+
MaxLimit int64 \`json:"maxLimit"\`
|
|
43
|
+
DurationDays int \`json:"durationDays"\`
|
|
44
|
+
WindowStart time.Time \`json:"windowStart"\`
|
|
45
|
+
WindowEnd time.Time \`json:"windowEnd"\`
|
|
46
|
+
}
|
|
29
47
|
`;
|
|
30
48
|
|
|
31
49
|
const meteringMiddleware = `
|
|
32
50
|
package middleware
|
|
33
51
|
|
|
34
52
|
import (
|
|
53
|
+
"context"
|
|
54
|
+
"fmt"
|
|
35
55
|
"net/http"
|
|
56
|
+
"strconv"
|
|
57
|
+
"strings"
|
|
36
58
|
"time"
|
|
37
59
|
|
|
38
60
|
"github.com/gin-gonic/gin"
|
|
39
61
|
"gorm.io/gorm"
|
|
62
|
+
"gorm.io/gorm/clause"
|
|
63
|
+
"{{app_name}}/cache"
|
|
40
64
|
"{{app_name}}/models"
|
|
41
65
|
)
|
|
42
66
|
|
|
67
|
+
var meterCtx = context.Background()
|
|
68
|
+
|
|
43
69
|
func MeteringMiddleware(db *gorm.DB) gin.HandlerFunc {
|
|
44
70
|
return func(c *gin.Context) {
|
|
45
|
-
|
|
46
|
-
if
|
|
71
|
+
keycloakId, exists := c.Get("KeycloakID")
|
|
72
|
+
if !exists {
|
|
47
73
|
c.Next()
|
|
48
74
|
return
|
|
49
75
|
}
|
|
76
|
+
userID := keycloakId.(string)
|
|
50
77
|
|
|
78
|
+
rolesList := []string{}
|
|
79
|
+
if roles, exists := c.Get("UserRoles"); exists {
|
|
80
|
+
for _, r := range roles.([]interface{}) {
|
|
81
|
+
rolesList = append(rolesList, r.(string))
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
tenant, _ := c.Get("tenantDB")
|
|
86
|
+
tenantStr := fmt.Sprintf("%v", tenant)
|
|
51
87
|
path := c.Request.URL.Path
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
LastAccessed: time.Now(),
|
|
88
|
+
|
|
89
|
+
if cache.RedisClient != nil {
|
|
90
|
+
var targetType, targetID, limitStr string
|
|
91
|
+
|
|
92
|
+
// 1. Try Specific User Limit
|
|
93
|
+
userLimitKey := fmt.Sprintf("metering_limit:user:%s:%s:%s", userID, tenantStr, path)
|
|
94
|
+
limitStr, _ = cache.RedisClient.Get(meterCtx, userLimitKey).Result()
|
|
95
|
+
if limitStr != "" {
|
|
96
|
+
targetType = "user"
|
|
97
|
+
targetID = userID
|
|
63
98
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if
|
|
67
|
-
|
|
68
|
-
"
|
|
69
|
-
|
|
70
|
-
"
|
|
99
|
+
|
|
100
|
+
// 2. Try Role Limit (First match wins)
|
|
101
|
+
if targetType == "" {
|
|
102
|
+
for _, role := range rolesList {
|
|
103
|
+
roleLimitKey := fmt.Sprintf("metering_limit:role:%s:%s:%s", role, tenantStr, path)
|
|
104
|
+
limitStr, _ = cache.RedisClient.Get(meterCtx, roleLimitKey).Result()
|
|
105
|
+
if limitStr != "" {
|
|
106
|
+
targetType = "role"
|
|
107
|
+
targetID = role
|
|
108
|
+
break
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
restoredActiveWindow := false
|
|
114
|
+
|
|
115
|
+
// 3. Fallback: Parse from Database
|
|
116
|
+
if db != nil && targetType == "" {
|
|
117
|
+
var usage models.APIUsage
|
|
118
|
+
result := db.Where("target_type = 'user' AND target_id = ? AND api_path = ? AND tenant_db = ?", userID, path, tenantStr).First(&usage)
|
|
119
|
+
if result.Error == nil {
|
|
120
|
+
targetType = "user"
|
|
121
|
+
targetID = userID
|
|
122
|
+
} else {
|
|
123
|
+
for _, role := range rolesList {
|
|
124
|
+
result = db.Where("target_type = 'role' AND target_id = ? AND api_path = ? AND tenant_db = ?", role, path, tenantStr).First(&usage)
|
|
125
|
+
if result.Error == nil {
|
|
126
|
+
targetType = "role"
|
|
127
|
+
targetID = role
|
|
128
|
+
break
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if targetType != "" {
|
|
134
|
+
autoResetStr := "true"
|
|
135
|
+
if !usage.AutoReset {
|
|
136
|
+
autoResetStr = "false"
|
|
137
|
+
}
|
|
138
|
+
limitStr = fmt.Sprintf("%d:%d:%s", usage.MaxLimit, usage.DurationDays, autoResetStr)
|
|
139
|
+
cache.RedisClient.Set(meterCtx, fmt.Sprintf("metering_limit:%s:%s:%s:%s", targetType, targetID, tenantStr, path), limitStr, 0)
|
|
140
|
+
|
|
141
|
+
// Safely Restore Usage from DB considering AutoReset
|
|
142
|
+
duration := time.Duration(usage.DurationDays) * 24 * time.Hour
|
|
143
|
+
usageKey := fmt.Sprintf("metering_usage:%s:%s:%s:%s", targetType, targetID, tenantStr, path)
|
|
144
|
+
|
|
145
|
+
// Only restore if AutoReset is disabled OR the Window is still mathematically Active!
|
|
146
|
+
if !usage.AutoReset || usage.DurationDays <= 0 || !time.Now().After(usage.WindowStart.Add(duration)) {
|
|
147
|
+
restoredActiveWindow = true
|
|
148
|
+
if usage.AutoReset && usage.DurationDays > 0 {
|
|
149
|
+
remainingTTL := time.Until(usage.WindowStart.Add(duration))
|
|
150
|
+
cache.RedisClient.Set(meterCtx, usageKey, usage.UsageCount, remainingTTL)
|
|
151
|
+
} else {
|
|
152
|
+
cache.RedisClient.Set(meterCtx, usageKey, usage.UsageCount, 0) // No Expiry (Locked!)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 4. Default Limits (Per User, 1000 requests per 30 Days, Auto Resets)
|
|
159
|
+
if targetType == "" {
|
|
160
|
+
targetType = "user"
|
|
161
|
+
targetID = userID
|
|
162
|
+
limitStr = "1000:30:true"
|
|
163
|
+
cache.RedisClient.Set(meterCtx, userLimitKey, limitStr, 0)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
parts := strings.Split(limitStr, ":")
|
|
167
|
+
maxLimit, _ := strconv.ParseInt(parts[0], 10, 64)
|
|
168
|
+
durationDays, _ := strconv.Atoi(parts[1])
|
|
169
|
+
autoReset := parts[2] == "true"
|
|
170
|
+
|
|
171
|
+
usageKey := fmt.Sprintf("metering_usage:%s:%s:%s:%s", targetType, targetID, tenantStr, path)
|
|
172
|
+
currentUsage, _ := cache.RedisClient.Incr(meterCtx, usageKey).Result()
|
|
173
|
+
|
|
174
|
+
var windowStart time.Time
|
|
175
|
+
|
|
176
|
+
// Explicit Window tracking for absolute fresh usages in Redis
|
|
177
|
+
isNewWindow := currentUsage == 1 && !restoredActiveWindow
|
|
178
|
+
if isNewWindow {
|
|
179
|
+
windowStart = time.Now()
|
|
180
|
+
// Only set TTL if AutoReset is enabled!
|
|
181
|
+
if autoReset && durationDays > 0 {
|
|
182
|
+
cache.RedisClient.Expire(meterCtx, usageKey, time.Duration(durationDays)*24*time.Hour)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if currentUsage > maxLimit {
|
|
187
|
+
c.JSON(http.StatusPaymentRequired, gin.H{
|
|
188
|
+
"error": fmt.Sprintf("SaaS Quota Exceeded for %s", targetType),
|
|
189
|
+
"target": targetID,
|
|
190
|
+
"limit": maxLimit,
|
|
191
|
+
"auto_reset": autoReset,
|
|
192
|
+
"current_use": currentUsage,
|
|
71
193
|
})
|
|
72
194
|
c.Abort()
|
|
73
195
|
return
|
|
74
196
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
197
|
+
|
|
198
|
+
if db != nil {
|
|
199
|
+
go func(tt string, tid string, p string, t string, count int64, lim int64, dur int, ar bool, isNew bool, ws time.Time) {
|
|
200
|
+
if count%10 == 0 || isNew {
|
|
201
|
+
if isNew {
|
|
202
|
+
var old models.APIUsage
|
|
203
|
+
res := db.Where("target_type = ? AND target_id = ? AND api_path = ? AND tenant_db = ?", tt, tid, p, t).First(&old)
|
|
204
|
+
if res.Error == nil && old.UsageCount > 0 {
|
|
205
|
+
db.Create(&models.APIUsageHistory{
|
|
206
|
+
TargetType: tt,
|
|
207
|
+
TargetID: tid,
|
|
208
|
+
APIPath: p,
|
|
209
|
+
TenantDB: t,
|
|
210
|
+
FinalUsage: old.UsageCount,
|
|
211
|
+
MaxLimit: old.MaxLimit,
|
|
212
|
+
DurationDays: old.DurationDays,
|
|
213
|
+
WindowStart: old.WindowStart,
|
|
214
|
+
WindowEnd: time.Now(),
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
updateCols := []string{"usage_count", "last_accessed"}
|
|
220
|
+
if isNew {
|
|
221
|
+
updateCols = append(updateCols, "window_start")
|
|
222
|
+
}
|
|
223
|
+
db.Clauses(clause.OnConflict{
|
|
224
|
+
Columns: []clause.Column{{Name: "target_type"}, {Name: "target_id"}, {Name: "api_path"}, {Name: "tenant_db"}},
|
|
225
|
+
DoUpdates: clause.AssignmentColumns(updateCols),
|
|
226
|
+
}).Create(&models.APIUsage{
|
|
227
|
+
TargetType: tt,
|
|
228
|
+
TargetID: tid,
|
|
229
|
+
APIPath: p,
|
|
230
|
+
TenantDB: t,
|
|
231
|
+
UsageCount: count,
|
|
232
|
+
MaxLimit: lim,
|
|
233
|
+
DurationDays: dur,
|
|
234
|
+
AutoReset: ar,
|
|
235
|
+
WindowStart: ws,
|
|
236
|
+
LastAccessed: time.Now(),
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
}(targetType, targetID, path, tenantStr, currentUsage, maxLimit, durationDays, autoReset, isNewWindow, windowStart)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
c.Next()
|
|
243
|
+
return
|
|
79
244
|
}
|
|
80
245
|
|
|
81
246
|
c.Next()
|
|
@@ -83,15 +248,21 @@ func MeteringMiddleware(db *gorm.DB) gin.HandlerFunc {
|
|
|
83
248
|
}
|
|
84
249
|
`;
|
|
85
250
|
|
|
86
|
-
|
|
251
|
+
const meteringController = `
|
|
87
252
|
package controllers
|
|
88
253
|
|
|
89
254
|
import (
|
|
255
|
+
"context"
|
|
256
|
+
"fmt"
|
|
90
257
|
"net/http"
|
|
258
|
+
"time"
|
|
259
|
+
|
|
260
|
+
"{{app_name}}/cache"
|
|
91
261
|
"{{app_name}}/models"
|
|
92
262
|
|
|
93
263
|
"github.com/gin-gonic/gin"
|
|
94
264
|
"gorm.io/gorm"
|
|
265
|
+
"gorm.io/gorm/clause"
|
|
95
266
|
)
|
|
96
267
|
|
|
97
268
|
type MeteringController struct {
|
|
@@ -99,10 +270,18 @@ type MeteringController struct {
|
|
|
99
270
|
}
|
|
100
271
|
|
|
101
272
|
func (mc *MeteringController) SetLimit(c *gin.Context) {
|
|
273
|
+
if mc.DB == nil {
|
|
274
|
+
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Infrastructure DB not initialized. SaaS Metering persistence requires a SQL database."})
|
|
275
|
+
return
|
|
276
|
+
}
|
|
102
277
|
var req struct {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
278
|
+
TargetType string \`json:"targetType" binding:"required"\` // "user" or "role"
|
|
279
|
+
TargetID string \`json:"targetId" binding:"required"\` // The UUID or the Realm Role
|
|
280
|
+
APIPath string \`json:"apiPath" binding:"required"\`
|
|
281
|
+
TenantDB string \`json:"tenantDb" binding:"required"\`
|
|
282
|
+
MaxLimit int64 \`json:"maxLimit" binding:"required"\`
|
|
283
|
+
DurationDays int \`json:"durationDays" binding:"required"\` // Duration Reset
|
|
284
|
+
AutoReset *bool \`json:"autoReset" binding:"required"\` // Paywall Lock!
|
|
106
285
|
}
|
|
107
286
|
|
|
108
287
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
@@ -110,34 +289,87 @@ func (mc *MeteringController) SetLimit(c *gin.Context) {
|
|
|
110
289
|
return
|
|
111
290
|
}
|
|
112
291
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
292
|
+
if req.TargetType != "user" && req.TargetType != "role" {
|
|
293
|
+
c.JSON(http.StatusBadRequest, gin.H{"error": "targetType must be 'user' or 'role'"})
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
var old models.APIUsage
|
|
298
|
+
res := mc.DB.Where("target_type = ? AND target_id = ? AND api_path = ? AND tenant_db = ?", req.TargetType, req.TargetID, req.APIPath, req.TenantDB).First(&old)
|
|
299
|
+
if res.Error == nil {
|
|
300
|
+
mc.DB.Create(&models.APIUsageHistory{
|
|
301
|
+
TargetType: old.TargetType,
|
|
302
|
+
TargetID: old.TargetID,
|
|
303
|
+
APIPath: old.APIPath,
|
|
304
|
+
TenantDB: old.TenantDB,
|
|
305
|
+
FinalUsage: old.UsageCount,
|
|
306
|
+
MaxLimit: old.MaxLimit,
|
|
307
|
+
DurationDays: old.DurationDays,
|
|
308
|
+
WindowStart: old.WindowStart,
|
|
309
|
+
WindowEnd: time.Now(),
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
mc.DB.Clauses(clause.OnConflict{
|
|
314
|
+
Columns: []clause.Column{{Name: "target_type"}, {Name: "target_id"}, {Name: "api_path"}, {Name: "tenant_db"}},
|
|
315
|
+
DoUpdates: clause.AssignmentColumns([]string{"max_limit", "duration_days", "auto_reset", "usage_count", "window_start"}),
|
|
316
|
+
}).Create(&models.APIUsage{
|
|
317
|
+
TargetType: req.TargetType,
|
|
318
|
+
TargetID: req.TargetID,
|
|
319
|
+
APIPath: req.APIPath,
|
|
320
|
+
TenantDB: req.TenantDB,
|
|
321
|
+
MaxLimit: req.MaxLimit,
|
|
322
|
+
DurationDays: req.DurationDays,
|
|
323
|
+
AutoReset: *req.AutoReset,
|
|
324
|
+
UsageCount: 0,
|
|
325
|
+
WindowStart: time.Now(), // Reset the usage window explicit start date!
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
if cache.RedisClient != nil {
|
|
329
|
+
autoResetStr := "true"
|
|
330
|
+
if !*req.AutoReset {
|
|
331
|
+
autoResetStr = "false"
|
|
122
332
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
333
|
+
|
|
334
|
+
limitKey := fmt.Sprintf("metering_limit:%s:%s:%s:%s", req.TargetType, req.TargetID, req.TenantDB, req.APIPath)
|
|
335
|
+
usageKey := fmt.Sprintf("metering_usage:%s:%s:%s:%s", req.TargetType, req.TargetID, req.TenantDB, req.APIPath)
|
|
336
|
+
cache.RedisClient.Set(context.Background(), limitKey, fmt.Sprintf("%d:%d:%s", req.MaxLimit, req.DurationDays, autoResetStr), 0)
|
|
337
|
+
cache.RedisClient.Del(context.Background(), usageKey)
|
|
126
338
|
}
|
|
127
339
|
|
|
128
|
-
c.JSON(http.StatusOK, gin.H{"message": "
|
|
340
|
+
c.JSON(http.StatusOK, gin.H{"message": "Quota, Duration & Paywall Lock updated globally."})
|
|
129
341
|
}
|
|
130
342
|
|
|
131
343
|
func (mc *MeteringController) GetUsage(c *gin.Context) {
|
|
344
|
+
if mc.DB == nil {
|
|
345
|
+
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Infrastructure DB not initialized."})
|
|
346
|
+
return
|
|
347
|
+
}
|
|
132
348
|
var usages []models.APIUsage
|
|
133
349
|
mc.DB.Find(&usages)
|
|
134
350
|
c.JSON(http.StatusOK, usages)
|
|
135
351
|
}
|
|
352
|
+
|
|
353
|
+
func (mc *MeteringController) GetHistory(c *gin.Context) {
|
|
354
|
+
if mc.DB == nil {
|
|
355
|
+
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Infrastructure DB not initialized."})
|
|
356
|
+
return
|
|
357
|
+
}
|
|
358
|
+
targetID := c.Query("targetId")
|
|
359
|
+
if targetID == "" {
|
|
360
|
+
c.JSON(http.StatusBadRequest, gin.H{"error": "targetId query parameter is required"})
|
|
361
|
+
return
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
var history []models.APIUsageHistory
|
|
365
|
+
mc.DB.Where("target_id = ?", targetID).Order("window_end desc").Find(&history)
|
|
366
|
+
c.JSON(http.StatusOK, history)
|
|
367
|
+
}
|
|
136
368
|
`;
|
|
137
369
|
|
|
138
370
|
await fs.writeFile(path.join(modelsDir, 'api_usage.go'), meteringModel);
|
|
139
|
-
await fs.writeFile(path.join(middlewareDir, 'metering_middleware.go'), meteringMiddleware.replace(
|
|
140
|
-
await fs.writeFile(path.join(controllersDir, 'metering_controller.go'), meteringController.replace(
|
|
371
|
+
await fs.writeFile(path.join(middlewareDir, 'metering_middleware.go'), meteringMiddleware.replace(/{{app_name}}/g, config.name));
|
|
372
|
+
await fs.writeFile(path.join(controllersDir, 'metering_controller.go'), meteringController.replace(/{{app_name}}/g, config.name));
|
|
141
373
|
|
|
142
374
|
console.log(chalk.gray(' Generated Metering Model, Middleware & Controller'));
|
|
143
375
|
};
|