go-duck-cli 1.0.9 → 1.1.1

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.
Files changed (70) hide show
  1. package/README.md +30 -15
  2. package/generators/ai_docs.js +130 -0
  3. package/generators/broker.js +63 -0
  4. package/generators/config.js +149 -7
  5. package/generators/devops.js +210 -43
  6. package/generators/docs.js +23 -4
  7. package/generators/elasticsearch.js +263 -0
  8. package/generators/kratos.js +229 -41
  9. package/generators/metering.js +280 -48
  10. package/generators/migrations.js +92 -198
  11. package/generators/mqtt.js +2 -39
  12. package/generators/multitenancy.js +274 -71
  13. package/generators/nats.js +39 -0
  14. package/generators/outbox.js +171 -0
  15. package/generators/postgrest.js +7 -3
  16. package/generators/postman.js +405 -0
  17. package/generators/repository.js +27 -0
  18. package/generators/router.js +27 -0
  19. package/generators/security.js +95 -14
  20. package/generators/serverless.js +147 -0
  21. package/generators/storage.js +589 -0
  22. package/generators/swagger.js +84 -60
  23. package/generators/telemetry.js +23 -32
  24. package/generators/websocket.js +55 -21
  25. package/index.js +481 -116
  26. package/package.json +6 -4
  27. package/parser/gdl.js +163 -24
  28. package/templates/docs/index.html.hbs +5 -5
  29. package/templates/docs/layout.hbs +221 -62
  30. package/templates/docs/pages/audit.hbs +83 -35
  31. package/templates/docs/pages/cli.hbs +18 -0
  32. package/templates/docs/pages/configuration.hbs +241 -0
  33. package/templates/docs/pages/datadog.hbs +46 -0
  34. package/templates/docs/pages/elasticsearch.hbs +121 -0
  35. package/templates/docs/pages/federation.hbs +241 -0
  36. package/templates/docs/pages/gdl-advanced.hbs +91 -0
  37. package/templates/docs/pages/gdl-annotations.hbs +137 -0
  38. package/templates/docs/pages/gdl-entities.hbs +134 -0
  39. package/templates/docs/pages/gdl-relationships.hbs +80 -0
  40. package/templates/docs/pages/gdl.hbs +60 -204
  41. package/templates/docs/pages/graphql.hbs +58 -44
  42. package/templates/docs/pages/grpc.hbs +53 -90
  43. package/templates/docs/pages/hybrid-store.hbs +127 -0
  44. package/templates/docs/pages/index.hbs +418 -149
  45. package/templates/docs/pages/keycloak.hbs +43 -0
  46. package/templates/docs/pages/legend.hbs +116 -0
  47. package/templates/docs/pages/mosquitto.hbs +39 -0
  48. package/templates/docs/pages/multitenancy.hbs +139 -71
  49. package/templates/docs/pages/otel.hbs +40 -0
  50. package/templates/docs/pages/realtime.hbs +38 -12
  51. package/templates/docs/pages/redis.hbs +40 -0
  52. package/templates/docs/pages/rest.hbs +120 -202
  53. package/templates/docs/pages/saga.hbs +94 -0
  54. package/templates/docs/pages/security.hbs +150 -44
  55. package/templates/docs/pages/serverless.hbs +157 -0
  56. package/templates/docs/pages/storage.hbs +127 -0
  57. package/templates/docs/pages/wizard.hbs +683 -0
  58. package/templates/docs/triple_identity_registry.png +0 -0
  59. package/templates/go/controller.go.hbs +287 -283
  60. package/templates/go/entity.go.hbs +17 -15
  61. package/templates/go/main.go.hbs +47 -180
  62. package/templates/go/migrator.go.hbs +65 -0
  63. package/templates/go/router.go.hbs +272 -0
  64. package/templates/graphql/resolver.go.hbs +53 -34
  65. package/templates/graphql/schema.graphql.hbs +17 -5
  66. package/templates/kratos/service.go.hbs +169 -34
  67. package/templates/proto/entity.proto.hbs +10 -14
  68. package/test_nested.gdl +21 -0
  69. package/templates/docs/intro.mp4 +0 -0
  70. package/test_parser.js +0 -9
@@ -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 protoTemplateSource = await fs.readFile(path.join(process.cwd(), 'templates', 'proto', 'entity.proto.hbs'), 'utf8');
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(process.cwd(), 'templates', 'kratos', 'service.go.hbs'), 'utf8');
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 isEnum = enums.some(e => e.name === type);
26
- // Map enums to string in proto to match the Go string enums for simpler POC
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
- 'String': 'string',
31
- 'Integer': 'int32',
32
- 'Long': 'int64',
33
- 'Float': 'float',
34
- 'BigDecimal': 'double',
35
- 'Boolean': 'bool',
36
- 'LocalDate': 'string',
37
- 'Instant': 'string',
38
- 'Text': 'string',
39
- 'JSON': 'string',
40
- 'JSONB': 'string'
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[type] || 'string';
48
+ return map[t] || 'string';
43
49
  });
44
50
 
45
51
  Handlebars.registerHelper('toGoCast', (type) => {
46
- const isEnum = enums.some(e => e.name === type);
47
- if (isEnum) return `models.${type}`;
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
- 'Integer': 'int',
51
- 'Long': 'int64',
52
- 'Float': 'float64',
53
- 'BigDecimal': 'float64',
54
- 'Boolean': 'bool',
55
- 'JSON': 'datatypes.JSON',
56
- 'JSONB': 'datatypes.JSON'
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[type] || '';
70
+ return map[t] || '';
59
71
  });
60
72
 
61
73
  Handlebars.registerHelper('toProtoCast', (type) => {
62
- const isEnum = enums.some(e => e.name === type);
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
- 'Integer': 'int32',
67
- 'Long': 'int64',
68
- 'Float': 'float32',
69
- 'BigDecimal': 'double',
70
- 'JSON': 'string',
71
- 'JSONB': 'string'
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[type] || '';
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 => f.type === 'JSON' || f.type === 'JSONB');
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('isJson', (type) => type === 'JSON' || type === 'JSONB');
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/golang-jwt/jwt/v4"
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
- jwt.Server(func(token *jwt.Token) (interface{}, error) {
132
- return []byte(conf.GoDuck.Security.KeycloakSecret), nil
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 != "" {