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/kratos.js
CHANGED
|
@@ -14,63 +14,87 @@ export const generateKratosCode = async (entities, projectRootDir, projectName,
|
|
|
14
14
|
await fs.ensureDir(serviceDir);
|
|
15
15
|
await fs.ensureDir(serverDir);
|
|
16
16
|
|
|
17
|
-
const
|
|
17
|
+
const __dirname = path.dirname(import.meta.url.replace('file://', ''));
|
|
18
|
+
const templateBase = path.resolve(__dirname, '..', 'templates');
|
|
19
|
+
|
|
20
|
+
const protoTemplateSource = await fs.readFile(path.join(templateBase, 'proto', 'entity.proto.hbs'), 'utf8');
|
|
18
21
|
const protoTemplate = Handlebars.compile(protoTemplateSource);
|
|
19
22
|
|
|
20
|
-
const serviceTemplateSource = await fs.readFile(path.join(
|
|
23
|
+
const serviceTemplateSource = await fs.readFile(path.join(templateBase, 'kratos', 'service.go.hbs'), 'utf8');
|
|
21
24
|
const serviceTemplate = Handlebars.compile(serviceTemplateSource);
|
|
22
25
|
|
|
23
26
|
// Helpers for Proto types
|
|
24
27
|
Handlebars.registerHelper('toProtoType', (type) => {
|
|
25
|
-
const
|
|
26
|
-
|
|
28
|
+
const t = (type || 'string').toLowerCase();
|
|
29
|
+
const isEnum = enums.some(e => e.name.toLowerCase() === t);
|
|
27
30
|
if (isEnum) return 'string';
|
|
28
31
|
|
|
29
32
|
const map = {
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
'
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
'
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
'
|
|
40
|
-
'
|
|
33
|
+
'string': 'string',
|
|
34
|
+
'int': 'int32',
|
|
35
|
+
'integer': 'int32',
|
|
36
|
+
'long': 'int64',
|
|
37
|
+
'float': 'float',
|
|
38
|
+
'bigdecimal': 'double',
|
|
39
|
+
'bool': 'bool',
|
|
40
|
+
'boolean': 'bool',
|
|
41
|
+
'localdate': 'string',
|
|
42
|
+
'instant': 'string',
|
|
43
|
+
'datetime': 'string',
|
|
44
|
+
'text': 'string',
|
|
45
|
+
'json': 'string',
|
|
46
|
+
'jsonb': 'string'
|
|
41
47
|
};
|
|
42
|
-
return map[
|
|
48
|
+
return map[t] || 'string';
|
|
43
49
|
});
|
|
44
50
|
|
|
45
51
|
Handlebars.registerHelper('toGoCast', (type) => {
|
|
46
|
-
const
|
|
47
|
-
|
|
52
|
+
const t = (type || '').toLowerCase();
|
|
53
|
+
const isEnum = enums.some(e => e.name.toLowerCase() === t);
|
|
54
|
+
if (isEnum) {
|
|
55
|
+
const en = enums.find(e => e.name.toLowerCase() === t);
|
|
56
|
+
return `models.${en.name}`;
|
|
57
|
+
}
|
|
48
58
|
|
|
49
59
|
const map = {
|
|
50
|
-
'
|
|
51
|
-
'
|
|
52
|
-
'
|
|
53
|
-
'
|
|
54
|
-
'
|
|
55
|
-
'
|
|
56
|
-
'
|
|
60
|
+
'int': 'int',
|
|
61
|
+
'integer': 'int',
|
|
62
|
+
'long': 'int64',
|
|
63
|
+
'float': 'float64',
|
|
64
|
+
'bigdecimal': 'float64',
|
|
65
|
+
'bool': 'bool',
|
|
66
|
+
'boolean': 'bool',
|
|
67
|
+
'json': 'datatypes.JSON',
|
|
68
|
+
'jsonb': 'datatypes.JSON'
|
|
57
69
|
};
|
|
58
|
-
return map[
|
|
70
|
+
return map[t] || '';
|
|
59
71
|
});
|
|
60
72
|
|
|
61
73
|
Handlebars.registerHelper('toProtoCast', (type) => {
|
|
62
|
-
const
|
|
74
|
+
const t = (type || '').toLowerCase();
|
|
75
|
+
const isEnum = enums.some(e => e.name.toLowerCase() === t);
|
|
63
76
|
if (isEnum) return 'string';
|
|
64
77
|
|
|
65
78
|
const map = {
|
|
66
|
-
'
|
|
67
|
-
'
|
|
68
|
-
'
|
|
69
|
-
'
|
|
70
|
-
'
|
|
71
|
-
'
|
|
79
|
+
'int': 'int32',
|
|
80
|
+
'integer': 'int32',
|
|
81
|
+
'long': 'int64',
|
|
82
|
+
'float': 'float32',
|
|
83
|
+
'bigdecimal': 'float64',
|
|
84
|
+
'json': 'string',
|
|
85
|
+
'jsonb': 'string'
|
|
72
86
|
};
|
|
73
|
-
return map[
|
|
87
|
+
return map[t] || '';
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
Handlebars.registerHelper('toProtoFieldName', (str) => {
|
|
91
|
+
if (typeof str !== 'string') return '';
|
|
92
|
+
return str.split('_').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join('');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
Handlebars.registerHelper('capitalize', (str) => {
|
|
96
|
+
if (typeof str !== 'string') return '';
|
|
97
|
+
return str.replace(/(?:^|_| )(\w)/g, (match, p1) => p1.toUpperCase());
|
|
74
98
|
});
|
|
75
99
|
|
|
76
100
|
Handlebars.registerHelper('add', (a, b) => {
|
|
@@ -79,10 +103,38 @@ export const generateKratosCode = async (entities, projectRootDir, projectName,
|
|
|
79
103
|
|
|
80
104
|
Handlebars.registerHelper('hasJson', (fields) => {
|
|
81
105
|
if (!fields || !Array.isArray(fields)) return false;
|
|
82
|
-
return fields.some(f =>
|
|
106
|
+
return fields.some(f => {
|
|
107
|
+
const t = (f.type || '').toLowerCase();
|
|
108
|
+
return t === 'json' || t === 'jsonb';
|
|
109
|
+
});
|
|
83
110
|
});
|
|
84
111
|
|
|
85
|
-
Handlebars.registerHelper('
|
|
112
|
+
Handlebars.registerHelper('hasNested', (fields) => {
|
|
113
|
+
if (!fields || !Array.isArray(fields)) return false;
|
|
114
|
+
return fields.some(f => f.isNested);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
Handlebars.registerHelper('isJson', (type) => {
|
|
118
|
+
const t = (type || '').toLowerCase();
|
|
119
|
+
return t === 'json' || t === 'jsonb';
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
Handlebars.registerHelper('or', function() {
|
|
123
|
+
return Array.prototype.slice.call(arguments, 0, -1).some(Boolean);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
Handlebars.registerHelper('hasDate', (fields) => {
|
|
127
|
+
if (!fields || !Array.isArray(fields)) return false;
|
|
128
|
+
return fields.some(f => {
|
|
129
|
+
const t = (f.type || '').toLowerCase();
|
|
130
|
+
return t === 'localdate' || t === 'instant' || t === 'datetime';
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
Handlebars.registerHelper('hasInstant', (fields) => {
|
|
135
|
+
if (!fields || !Array.isArray(fields)) return false;
|
|
136
|
+
return fields.some(f => (f.type || '').toLowerCase() === 'instant' || (f.type || '').toLowerCase() === 'datetime');
|
|
137
|
+
});
|
|
86
138
|
|
|
87
139
|
for (const entity of entities) {
|
|
88
140
|
const context = {
|
|
@@ -90,6 +142,9 @@ export const generateKratosCode = async (entities, projectRootDir, projectName,
|
|
|
90
142
|
capitalize: (s) => s.charAt(0).toUpperCase() + s.slice(1),
|
|
91
143
|
lower: (s) => s.toLowerCase(),
|
|
92
144
|
fields: entity.fields,
|
|
145
|
+
annotation: entity.annotation,
|
|
146
|
+
isDocument: entity.isDocument || false,
|
|
147
|
+
isAudited: entity.isAudited || false,
|
|
93
148
|
projectName,
|
|
94
149
|
enums
|
|
95
150
|
};
|
|
@@ -106,6 +161,62 @@ export const generateKratosCode = async (entities, projectRootDir, projectName,
|
|
|
106
161
|
// 3. Generate Auth Middleware & gRPC Server with Kratos
|
|
107
162
|
await generateKratosServer(serverDir, projectName, entities);
|
|
108
163
|
|
|
164
|
+
// 4. Generate utils.go for shared helpers
|
|
165
|
+
const utilsContent = `package service
|
|
166
|
+
|
|
167
|
+
import "time"
|
|
168
|
+
|
|
169
|
+
func parseDate(s string) time.Time {
|
|
170
|
+
t, _ := time.Parse("2006-01-02", s)
|
|
171
|
+
return t
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
func parseInstant(s string) time.Time {
|
|
175
|
+
t, _ := time.Parse(time.RFC3339, s)
|
|
176
|
+
return t
|
|
177
|
+
}
|
|
178
|
+
`;
|
|
179
|
+
await fs.writeFile(path.join(serviceDir, 'utils.go'), utilsContent);
|
|
180
|
+
|
|
181
|
+
// 5. Setup third_party dependencies (google/api) - Automated for all new projects
|
|
182
|
+
const tpDir = path.join(projectRootDir, 'third_party', 'google', 'api');
|
|
183
|
+
await fs.ensureDir(tpDir);
|
|
184
|
+
|
|
185
|
+
// 6. Add duck.go package marker - This stops the "package not in std" error instantly
|
|
186
|
+
await fs.writeFile(path.join(apiDir, 'duck.go'), 'package v1\n');
|
|
187
|
+
|
|
188
|
+
// 7. We provide a script to download these since we want to keep the CLI lightweight but the app functional
|
|
189
|
+
const generateSh = `#!/bin/bash
|
|
190
|
+
# GO-DUCK Protos Generator
|
|
191
|
+
set -e
|
|
192
|
+
|
|
193
|
+
# 1. Detect Path
|
|
194
|
+
export GOBIN=$(go env GOPATH)/bin
|
|
195
|
+
export PATH=$PATH:$GOBIN
|
|
196
|
+
|
|
197
|
+
echo "🦆 Syncing Protobuf Dependencies..."
|
|
198
|
+
mkdir -p third_party/google/api
|
|
199
|
+
curl -sSL https://raw.githubusercontent.com/googleapis/googleapis/master/google/api/annotations.proto > third_party/google/api/annotations.proto
|
|
200
|
+
curl -sSL https://raw.githubusercontent.com/googleapis/googleapis/master/google/api/http.proto > third_party/google/api/http.proto
|
|
201
|
+
|
|
202
|
+
echo "🏗️ Compiling API Layer..."
|
|
203
|
+
if ! command -v protoc &> /dev/null; then
|
|
204
|
+
echo "❌ Error: 'protoc' not found. Please install: brew install protobuf"
|
|
205
|
+
exit 1
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
find api -name "*.proto" -exec protoc --proto_path=. \\
|
|
209
|
+
--proto_path=./api \\
|
|
210
|
+
--proto_path=./third_party \\
|
|
211
|
+
--go_out=paths=source_relative:. \\
|
|
212
|
+
--go-grpc_out=paths=source_relative:. \\
|
|
213
|
+
{} +
|
|
214
|
+
|
|
215
|
+
echo "✅ Protos compiled successfully!"
|
|
216
|
+
`;
|
|
217
|
+
await fs.writeFile(path.join(projectRootDir, 'generate.sh'), generateSh);
|
|
218
|
+
await fs.chmod(path.join(projectRootDir, 'generate.sh'), 0o755);
|
|
219
|
+
|
|
109
220
|
console.log(chalk.green('✅ Kratos gRPC code generated successfully!'));
|
|
110
221
|
};
|
|
111
222
|
|
|
@@ -114,23 +225,100 @@ const generateKratosServer = async (serverDir, projectName, entities) => {
|
|
|
114
225
|
|
|
115
226
|
import (
|
|
116
227
|
"context"
|
|
117
|
-
"github.com/go-kratos/kratos/v2/middleware/auth/jwt"
|
|
228
|
+
kjwt "github.com/go-kratos/kratos/v2/middleware/auth/jwt"
|
|
118
229
|
"github.com/go-kratos/kratos/v2/middleware/recovery"
|
|
119
230
|
"github.com/go-kratos/kratos/v2/transport/grpc"
|
|
120
|
-
"github.com/
|
|
231
|
+
"github.com/go-kratos/kratos/v2/middleware"
|
|
232
|
+
"github.com/go-kratos/kratos/v2/metadata"
|
|
233
|
+
"github.com/golang-jwt/jwt/v5"
|
|
234
|
+
{{#if entities}}
|
|
121
235
|
v1 "{{projectName}}/api/v1"
|
|
122
236
|
"{{projectName}}/internal/service"
|
|
237
|
+
{{/if}}
|
|
123
238
|
"{{projectName}}/internal/repository"
|
|
124
239
|
"{{projectName}}/config"
|
|
240
|
+
"{{projectName}}/models"
|
|
241
|
+
my_middleware "{{projectName}}/middleware"
|
|
242
|
+
"gorm.io/gorm"
|
|
243
|
+
"strings"
|
|
244
|
+
"fmt"
|
|
125
245
|
)
|
|
126
246
|
|
|
247
|
+
func TenantServerInterceptor(conf *config.Config, db *gorm.DB) middleware.Middleware {
|
|
248
|
+
mgr := my_middleware.GetTenantManager(db, conf)
|
|
249
|
+
fallbackDB := "go_duck_fallback" // Standard fallback
|
|
250
|
+
|
|
251
|
+
return func(handler middleware.Handler) middleware.Handler {
|
|
252
|
+
return func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
253
|
+
token, ok := kjwt.FromContext(ctx)
|
|
254
|
+
if !ok { return handler(ctx, req) }
|
|
255
|
+
|
|
256
|
+
claims, ok := token.(jwt.MapClaims)
|
|
257
|
+
if !ok { return handler(ctx, req) }
|
|
258
|
+
|
|
259
|
+
ra, ok := claims["realm_access"].(map[string]interface{})
|
|
260
|
+
if !ok { return handler(ctx, req) }
|
|
261
|
+
|
|
262
|
+
rolesInterface, ok := ra["roles"].([]interface{})
|
|
263
|
+
if !ok { return handler(ctx, req) }
|
|
264
|
+
|
|
265
|
+
// Normalize roles (lowercase)
|
|
266
|
+
var lowerRoles []string
|
|
267
|
+
isAdmin := false
|
|
268
|
+
for _, r := range rolesInterface {
|
|
269
|
+
roleStr := strings.ToLower(fmt.Sprintf("%v", r))
|
|
270
|
+
lowerRoles = append(lowerRoles, roleStr)
|
|
271
|
+
if roleStr == "admin" || roleStr == "role_admin" { isAdmin = true }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
var requestedTenant string
|
|
275
|
+
if md, ok := metadata.FromServerContext(ctx); ok {
|
|
276
|
+
requestedTenant = strings.ToLower(md.Get("x-tenant-id"))
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
siloConnections := make(map[string]*gorm.DB)
|
|
280
|
+
if requestedTenant != "" {
|
|
281
|
+
var mapping models.TenantRole
|
|
282
|
+
if err := db.Raw("SELECT role_name, db_name FROM tenant_roles WHERE role_name IN ? AND tenant_id = ? LIMIT 1", lowerRoles, requestedTenant).Scan(&mapping).Error; err == nil && mapping.DBName != "" {
|
|
283
|
+
if mapping.DBName == "admin_db" && !isAdmin {
|
|
284
|
+
conn, _ := mgr.GetDB(fallbackDB)
|
|
285
|
+
siloConnections["fallback"] = conn
|
|
286
|
+
} else {
|
|
287
|
+
conn, _ := mgr.GetDB(mapping.DBName)
|
|
288
|
+
siloConnections[mapping.RoleName] = conn
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
} else {
|
|
292
|
+
var mappings []models.TenantRole
|
|
293
|
+
db.Raw("SELECT role_name, db_name FROM tenant_roles WHERE role_name IN ?", lowerRoles).Scan(&mappings)
|
|
294
|
+
for _, m := range mappings {
|
|
295
|
+
if m.DBName == "admin_db" && !isAdmin { continue }
|
|
296
|
+
if conn, err := mgr.GetDB(m.DBName); err == nil {
|
|
297
|
+
siloConnections[m.RoleName] = conn
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if len(siloConnections) == 0 {
|
|
303
|
+
conn, _ := mgr.GetDB(fallbackDB)
|
|
304
|
+
siloConnections["fallback"] = conn
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Inject Federated Bundle
|
|
308
|
+
ctx = context.WithValue(ctx, "tenantSiloConns", siloConnections)
|
|
309
|
+
return handler(ctx, req)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
127
314
|
func NewGRPCServer(conf *config.Config, repo *repository.Repository) *grpc.Server {
|
|
128
315
|
var opts = []grpc.ServerOption{
|
|
129
316
|
grpc.Middleware(
|
|
130
317
|
recovery.Recovery(),
|
|
131
|
-
|
|
132
|
-
return []byte(conf.GoDuck.Security.
|
|
133
|
-
}),
|
|
318
|
+
kjwt.Server(func(token *jwt.Token) (interface{}, error) {
|
|
319
|
+
return []byte(conf.GoDuck.Security.KeycloakAppClientSecret), nil
|
|
320
|
+
}, kjwt.WithClaims(func() jwt.Claims { return &jwt.MapClaims{} })),
|
|
321
|
+
TenantServerInterceptor(conf, repo.DB),
|
|
134
322
|
),
|
|
135
323
|
}
|
|
136
324
|
if conf.GoDuck.Server.GRPC.Addr != "" {
|