go-duck-cli 1.0.0

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 (49) hide show
  1. package/README.md +130 -0
  2. package/generators/cache.js +107 -0
  3. package/generators/config.js +173 -0
  4. package/generators/devops.js +212 -0
  5. package/generators/docs.js +74 -0
  6. package/generators/graphql.js +38 -0
  7. package/generators/kratos.js +157 -0
  8. package/generators/logger.js +68 -0
  9. package/generators/metering.js +143 -0
  10. package/generators/migrations.js +240 -0
  11. package/generators/mqtt.js +87 -0
  12. package/generators/multitenancy.js +130 -0
  13. package/generators/postgrest.js +115 -0
  14. package/generators/repository.js +28 -0
  15. package/generators/resilience.js +69 -0
  16. package/generators/security.js +168 -0
  17. package/generators/swagger.js +145 -0
  18. package/generators/telemetry.js +121 -0
  19. package/generators/websocket.js +162 -0
  20. package/index.js +592 -0
  21. package/package.json +23 -0
  22. package/parser/gdl.js +162 -0
  23. package/templates/application.yml.hbs +18 -0
  24. package/templates/docs/gin_bottle.png +0 -0
  25. package/templates/docs/index.html.hbs +226 -0
  26. package/templates/docs/intro.mp4 +0 -0
  27. package/templates/docs/kratos_mark.png +0 -0
  28. package/templates/docs/layout.hbs +106 -0
  29. package/templates/docs/logo.png +0 -0
  30. package/templates/docs/pages/audit.hbs +39 -0
  31. package/templates/docs/pages/cli.hbs +83 -0
  32. package/templates/docs/pages/gdl.hbs +223 -0
  33. package/templates/docs/pages/graphql.hbs +51 -0
  34. package/templates/docs/pages/grpc.hbs +100 -0
  35. package/templates/docs/pages/index.hbs +181 -0
  36. package/templates/docs/pages/integrations.hbs +83 -0
  37. package/templates/docs/pages/observability.hbs +34 -0
  38. package/templates/docs/pages/realtime.hbs +43 -0
  39. package/templates/docs/pages/rest.hbs +149 -0
  40. package/templates/docs/pages/security.hbs +31 -0
  41. package/templates/go/controller.go.hbs +236 -0
  42. package/templates/go/entity.go.hbs +34 -0
  43. package/templates/go/enum.go.hbs +7 -0
  44. package/templates/go/main.go.hbs +186 -0
  45. package/templates/graphql/resolver.go.hbs +50 -0
  46. package/templates/graphql/schema.graphql.hbs +64 -0
  47. package/templates/kratos/service.go.hbs +104 -0
  48. package/templates/proto/entity.proto.hbs +95 -0
  49. package/test_parser.js +9 -0
@@ -0,0 +1,145 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+
5
+ export const generateSwaggerDocs = async (config, entities, outputDir) => {
6
+ const docsDir = path.join(outputDir, 'docs');
7
+ await fs.ensureDir(docsDir);
8
+
9
+ const swagger = {
10
+ openapi: '3.0.0',
11
+ info: {
12
+ title: `${config.name} API`,
13
+ version: '1.0.0',
14
+ description: `Generated documentation for ${config.name} microservice`
15
+ },
16
+ servers: [
17
+ { url: 'http://localhost:8080/api', description: 'Local Development Server' }
18
+ ],
19
+ paths: {},
20
+ components: {
21
+ schemas: {
22
+ Error: {
23
+ type: 'object',
24
+ properties: {
25
+ error: { type: 'string' }
26
+ }
27
+ }
28
+ }
29
+ }
30
+ };
31
+
32
+ // 1. Add Entity Paths
33
+ for (const entity of entities) {
34
+ const name = entity.name.toLowerCase();
35
+ const capitalized = entity.name;
36
+
37
+ // Add Schema for Entity
38
+ swagger.components.schemas[capitalized] = {
39
+ type: 'object',
40
+ properties: {
41
+ id: { type: 'integer' },
42
+ ...entity.fields.reduce((acc, field) => {
43
+ acc[field.name] = { type: mapToSwaggerType(field.type) };
44
+ return acc;
45
+ }, {}),
46
+ createdAt: { type: 'string', format: 'date-time' },
47
+ updatedAt: { type: 'string', format: 'date-time' }
48
+ }
49
+ };
50
+
51
+ // POST /entities
52
+ swagger.paths[`/${name}s`] = {
53
+ post: {
54
+ tags: [capitalized],
55
+ summary: `Create a new ${capitalized}`,
56
+ requestBody: {
57
+ required: true,
58
+ content: { 'application/json': { schema: { $ref: `#/components/schemas/${capitalized}` } } }
59
+ },
60
+ responses: {
61
+ 201: { description: 'Created', content: { 'application/json': { schema: { $ref: `#/components/schemas/${capitalized}` } } } }
62
+ }
63
+ },
64
+ get: {
65
+ tags: [capitalized],
66
+ summary: `Get all ${capitalized}s`,
67
+ parameters: [
68
+ { name: 'page', in: 'query', schema: { type: 'integer' } },
69
+ { name: 'size', in: 'query', schema: { type: 'integer' } },
70
+ { name: 'eager', in: 'query', schema: { type: 'boolean' } }
71
+ ],
72
+ responses: {
73
+ 200: { description: 'OK', content: { 'application/json': { schema: { type: 'array', items: { $ref: `#/components/schemas/${capitalized}` } } } } }
74
+ }
75
+ }
76
+ };
77
+
78
+ // GET/PUT/PATCH/DELETE /entities/:id
79
+ swagger.paths[`/${name}s/{id}`] = {
80
+ get: {
81
+ tags: [capitalized],
82
+ summary: `Get ${capitalized} by ID`,
83
+ parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'integer' } }],
84
+ responses: {
85
+ 200: { description: 'OK', content: { 'application/json': { schema: { $ref: `#/components/schemas/${capitalized}` } } } }
86
+ }
87
+ },
88
+ put: {
89
+ tags: [capitalized],
90
+ summary: `Update ${capitalized}`,
91
+ parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'integer' } }],
92
+ responses: {
93
+ 200: { description: 'Updated', content: { 'application/json': { schema: { $ref: `#/components/schemas/${capitalized}` } } } }
94
+ }
95
+ },
96
+ delete: {
97
+ tags: [capitalized],
98
+ summary: `Delete ${capitalized}`,
99
+ parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'integer' } }],
100
+ responses: {
101
+ 204: { description: 'No Content' }
102
+ }
103
+ }
104
+ };
105
+ }
106
+
107
+ // 2. Add System Paths
108
+ swagger.paths['/rpc/{table}'] = {
109
+ get: {
110
+ tags: ['Search'],
111
+ summary: 'Generic PostgREST-like Search',
112
+ parameters: [
113
+ { name: 'table', in: 'path', required: true, schema: { type: 'string' } },
114
+ { name: 'order', in: 'query', schema: { type: 'string' } },
115
+ { name: 'limit', in: 'query', schema: { type: 'integer' } }
116
+ ],
117
+ responses: { 200: { description: 'OK' } }
118
+ }
119
+ };
120
+
121
+ swagger.paths['/audit'] = {
122
+ get: {
123
+ tags: ['Audit'],
124
+ summary: 'View Audit Logs',
125
+ responses: { 200: { description: 'OK' } }
126
+ }
127
+ };
128
+
129
+ await fs.writeJson(path.join(docsDir, 'swagger.json'), swagger, { spaces: 2 });
130
+ console.log(chalk.gray(' Generated Swagger Documentation: swagger.json'));
131
+ };
132
+
133
+ const mapToSwaggerType = (type) => {
134
+ const types = {
135
+ 'String': 'string',
136
+ 'Integer': 'integer',
137
+ 'Float': 'number',
138
+ 'Boolean': 'boolean',
139
+ 'Long': 'integer',
140
+ 'BigDecimal': 'number',
141
+ 'LocalDate': 'string',
142
+ 'Instant': 'string'
143
+ };
144
+ return types[type] || 'string';
145
+ };
@@ -0,0 +1,121 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+
5
+ export const generateTelemetryCode = async (config, outputDir) => {
6
+ const telemetryDir = path.join(outputDir, 'internal/telemetry');
7
+ const k8sDir = path.join(outputDir, 'k8s');
8
+
9
+ await fs.ensureDir(telemetryDir);
10
+ await fs.ensureDir(k8sDir);
11
+
12
+ const otelGo = `
13
+ package telemetry
14
+
15
+ import (
16
+ "context"
17
+ "fmt"
18
+ "log"
19
+ "time"
20
+
21
+ "{{app_name}}/config"
22
+ "go.opentelemetry.io/otel"
23
+ "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
24
+ "go.opentelemetry.io/otel/propagation"
25
+ "go.opentelemetry.io/otel/sdk/resource"
26
+ sdktrace "go.opentelemetry.io/otel/sdk/trace"
27
+ semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
28
+ "google.golang.org/grpc"
29
+ "google.golang.org/grpc/credentials/insecure"
30
+ )
31
+
32
+ // InitTelemetry initializes OpenTelemetry SDK
33
+ func InitTelemetry(cfg *config.Config) (func(context.Context) error, error) {
34
+ if !cfg.GoDuck.Telemetry.OTel.Enabled {
35
+ log.Println("OpenTelemetry is disabled.")
36
+ return func(context.Context) error { return nil }, nil
37
+ }
38
+
39
+ ctx := context.Background()
40
+
41
+ // 1. Setup Resource
42
+ res, err := resource.New(ctx,
43
+ resource.WithAttributes(
44
+ semconv.ServiceNameKey.String(cfg.GoDuck.Name),
45
+ semconv.ServiceVersionKey.String(cfg.GoDuck.Version),
46
+ ),
47
+ )
48
+ if err != nil {
49
+ return nil, fmt.Errorf("failed to create resource: %w", err)
50
+ }
51
+
52
+ // 2. Setup OTLP Exporter (gRPC)
53
+ conn, err := grpc.DialContext(ctx, cfg.GoDuck.Telemetry.OTel.Endpoint,
54
+ grpc.WithTransportCredentials(insecure.NewCredentials()),
55
+ grpc.WithBlock(),
56
+ )
57
+ if err != nil {
58
+ return nil, fmt.Errorf("failed to create gRPC connection to OTel collector: %w", err)
59
+ }
60
+
61
+ traceExporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithGRPCConn(conn))
62
+ if err != nil {
63
+ return nil, fmt.Errorf("failed to create trace exporter: %w", err)
64
+ }
65
+
66
+ // 3. Setup Tracer Provider
67
+ bsp := sdktrace.NewBatchSpanProcessor(traceExporter)
68
+ tp := sdktrace.NewTracerProvider(
69
+ sdktrace.WithSampler(sdktrace.TraceIDRatioBased(cfg.GoDuck.Telemetry.OTel.SamplerRatio)),
70
+ sdktrace.WithResource(res),
71
+ sdktrace.WithSpanProcessor(bsp),
72
+ )
73
+ otel.SetTracerProvider(tp)
74
+
75
+ // 4. Setup Text Map Propagator
76
+ otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
77
+
78
+ log.Printf("OpenTelemetry initialized with endpoint: %s", cfg.GoDuck.Telemetry.OTel.Endpoint)
79
+
80
+ return tp.Shutdown, nil
81
+ }
82
+ `;
83
+
84
+ const otelCollectorK8s = `
85
+ apiVersion: v1
86
+ kind: ConfigMap
87
+ metadata:
88
+ name: otel-collector-conf
89
+ labels:
90
+ app: {{app_name}}
91
+ data:
92
+ otel-collector-config.yaml: |
93
+ receivers:
94
+ otlp:
95
+ protocols:
96
+ grpc:
97
+ http:
98
+ processors:
99
+ batch:
100
+ resourcedetection:
101
+ detectors: [env, system]
102
+ exporters:
103
+ logging:
104
+ loglevel: debug
105
+ otlp:
106
+ endpoint: "jaeger-collector:4317"
107
+ tls:
108
+ insecure: true
109
+ service:
110
+ pipelines:
111
+ traces:
112
+ receivers: [otlp]
113
+ processors: [batch, resourcedetection]
114
+ exporters: [logging, otlp]
115
+ `;
116
+
117
+ await fs.writeFile(path.join(telemetryDir, 'otel.go'), otelGo.replace(/{{app_name}}/g, config.name));
118
+ await fs.writeFile(path.join(k8sDir, 'otel-collector.yml'), otelCollectorK8s.replace(/{{app_name}}/g, config.name));
119
+
120
+ console.log(chalk.gray(' Generated OpenTelemetry Telemetry Package & K8s Config'));
121
+ };
@@ -0,0 +1,162 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+
5
+ export const generateWebSocketCode = async (config, entities, outputDir) => {
6
+ const wsDir = path.join(outputDir, 'ws');
7
+ await fs.ensureDir(wsDir);
8
+
9
+ const wsHandler = `
10
+ package ws
11
+
12
+ import (
13
+ "context"
14
+ "crypto/hmac"
15
+ "crypto/sha256"
16
+ "encoding/hex"
17
+ "encoding/json"
18
+ "fmt"
19
+ "log"
20
+ "net/http"
21
+ "sync"
22
+ "{{app_name}}/controllers"
23
+
24
+ "github.com/gin-gonic/gin"
25
+ "github.com/gorilla/websocket"
26
+ "go.opentelemetry.io/otel"
27
+ "go.opentelemetry.io/otel/propagation"
28
+ "go.opentelemetry.io/otel/trace"
29
+ "gorm.io/gorm"
30
+ )
31
+
32
+ var upgrader = websocket.Upgrader{
33
+ CheckOrigin: func(r *http.Request) bool { return true },
34
+ }
35
+
36
+ type WSMessage struct {
37
+ Action string \`json:"action"\`
38
+ Payload json.RawMessage \`json:"payload"\`
39
+ Signature string \`json:"signature"\`
40
+ TraceParent string \`json:"traceparent,omitempty"\` // W3C TraceParent
41
+ }
42
+
43
+ type WSResponse struct {
44
+ Action string \`json:"action"\`
45
+ Data interface{} \`json:"data"\`
46
+ Error string \`json:"error,omitempty"\`
47
+ TraceParent string \`json:"traceparent,omitempty"\`
48
+ }
49
+
50
+ // RESToverWSDispatcher handles the mapping from WS actions to Controller logic
51
+ type RESToverWSDispatcher struct {
52
+ DB *gorm.DB
53
+ SecretKey []byte
54
+ Tracer trace.Tracer
55
+ }
56
+
57
+ func NewDispatcher(db *gorm.DB) *RESToverWSDispatcher {
58
+ return &RESToverWSDispatcher{
59
+ DB: db,
60
+ SecretKey: []byte("go-duck-super-secret-key"),
61
+ Tracer: otel.Tracer("ws-dispatcher"),
62
+ }
63
+ }
64
+
65
+ func (d *RESToverWSDispatcher) HandleConnection(c *gin.Context) {
66
+ conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
67
+ if err != nil {
68
+ log.Printf("WS Upgrade error: %v", err)
69
+ return
70
+ }
71
+ defer conn.Close()
72
+
73
+ for {
74
+ _, message, err := conn.ReadMessage()
75
+ if err != nil {
76
+ break
77
+ }
78
+
79
+ var wsMsg WSMessage
80
+ if err := json.Unmarshal(message, &wsMsg); err != nil {
81
+ d.sendError(conn, "Invalid JSON format", wsMsg.Action, "")
82
+ continue
83
+ }
84
+
85
+ // 1. Extract Parent Trace Context
86
+ ctx := context.Background()
87
+ if wsMsg.TraceParent != "" {
88
+ propagator := otel.GetTextMapPropagator()
89
+ ctx = propagator.Extract(ctx, propagation.HeaderCarrier{"traceparent": []string{wsMsg.TraceParent}})
90
+ }
91
+
92
+ // 2. Start WS-Action Span
93
+ childCtx, span := d.Tracer.Start(ctx, fmt.Sprintf("WS Action: %s", wsMsg.Action))
94
+
95
+ // 3. Verify Message Signature (Payload Integrity)
96
+ if !d.verifySignature(wsMsg.Payload, wsMsg.Signature) {
97
+ d.sendError(conn, "Invalid signature: Payload compromised", wsMsg.Action, wsMsg.TraceParent)
98
+ span.End()
99
+ continue
100
+ }
101
+
102
+ // 4. Dispatch to MVC Controllers
103
+ d.dispatch(childCtx, conn, wsMsg)
104
+ span.End()
105
+ }
106
+ }
107
+
108
+ func (d *RESToverWSDispatcher) verifySignature(payload []byte, signature string) bool {
109
+ h := hmac.New(sha256.New, d.SecretKey)
110
+ h.Write(payload)
111
+ expectedSignature := hex.EncodeToString(h.Sum(nil))
112
+ return hmac.Equal([]byte(expectedSignature), []byte(signature))
113
+ }
114
+
115
+ func (d *RESToverWSDispatcher) dispatch(ctx context.Context, conn *websocket.Conn, msg WSMessage) {
116
+ // Inject current span into response header for client to trace back
117
+ carrier := propagation.HeaderCarrier{}
118
+ otel.GetTextMapPropagator().Inject(ctx, carrier)
119
+ tp := carrier.Get("traceparent")
120
+
121
+ switch msg.Action {
122
+ default:
123
+ d.sendError(conn, "Unknown action", msg.Action, tp)
124
+ }
125
+ }
126
+
127
+ func (d *RESToverWSDispatcher) sendResponse(conn *websocket.Conn, action string, data interface{}, tp string) {
128
+ resp := WSResponse{Action: action, Data: data, TraceParent: tp}
129
+ conn.WriteJSON(resp)
130
+ }
131
+
132
+ func (d *RESToverWSDispatcher) sendError(conn *websocket.Conn, err string, action string, tp string) {
133
+ resp := WSResponse{Action: action, Error: err, TraceParent: tp}
134
+ conn.WriteJSON(resp)
135
+ }
136
+ `;
137
+
138
+ const wsContent = wsHandler.replace(/{{app_name}}/g, config.name);
139
+ let finalContent = wsContent;
140
+ let entitiesBlock = "";
141
+ for (const entity of entities) {
142
+ entitiesBlock += `
143
+ case "GET_${entity.name.toUpperCase()}S":
144
+ var list${entity.name} []map[string]interface{}
145
+ // Note: In a production app, we'd use gorm statement timeout and tracing hooks
146
+ d.DB.WithContext(ctx).Table("${entity.name.toLowerCase()}").Find(&list${entity.name})
147
+ d.sendResponse(conn, msg.Action, list${entity.name}, tp)
148
+ case "CREATE_${entity.name.toUpperCase()}":
149
+ d.sendResponse(conn, msg.Action, "${entity.name} creation processing...", tp)
150
+ `;
151
+ }
152
+
153
+ finalContent = finalContent.replace("{{#each entities}}", "").replace("{{/each}}", "");
154
+ const startTag = "switch msg.Action {";
155
+ const endTag = "default:";
156
+ const parts = finalContent.split(startTag);
157
+ const endParts = parts[1].split(endTag);
158
+ finalContent = parts[0] + startTag + entitiesBlock + endTag + endParts[1];
159
+
160
+ await fs.writeFile(path.join(wsDir, 'handler.go'), finalContent);
161
+ console.log(chalk.gray(' Generated REST-over-WS Dispatcher with Traced-Envelope'));
162
+ };