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.
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 +493 -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
@@ -2,7 +2,7 @@ import fs from 'fs-extra';
2
2
  import path from 'path';
3
3
  import chalk from 'chalk';
4
4
 
5
- export const generateSwaggerDocs = async (config, entities, outputDir) => {
5
+ export const generateSwaggerDocs = async (config, entities, outputDir, openEntities = []) => {
6
6
  const docsDir = path.join(outputDir, 'docs');
7
7
  await fs.ensureDir(docsDir);
8
8
 
@@ -14,7 +14,7 @@ export const generateSwaggerDocs = async (config, entities, outputDir) => {
14
14
  description: `Generated documentation for ${config.name} microservice`
15
15
  },
16
16
  servers: [
17
- { url: 'http://localhost:8080/api', description: 'Local Development Server' }
17
+ { url: 'http://localhost:8080', description: 'Local Development Server' }
18
18
  ],
19
19
  paths: {},
20
20
  components: {
@@ -28,7 +28,7 @@ export const generateSwaggerDocs = async (config, entities, outputDir) => {
28
28
  type: 'apiKey',
29
29
  in: 'header',
30
30
  name: 'X-Tenant-ID',
31
- description: 'The unique identifier for the tenant dashboard context'
31
+ description: 'The unique identifier for the tenant context'
32
32
  }
33
33
  },
34
34
  schemas: {
@@ -40,6 +40,8 @@ export const generateSwaggerDocs = async (config, entities, outputDir) => {
40
40
  }
41
41
  }
42
42
  },
43
+ // Global security applies to /api by default in our implementation,
44
+ // but Swagger paths can override this.
43
45
  security: [
44
46
  { BearerAuth: [], TenantID: [] }
45
47
  ]
@@ -49,6 +51,25 @@ export const generateSwaggerDocs = async (config, entities, outputDir) => {
49
51
  { name: 'X-Tenant-ID', in: 'header', required: true, schema: { type: 'string', default: 'default' }, description: 'Multi-tenancy context identifier' }
50
52
  ];
51
53
 
54
+ const isOpen = (entityName, action) => {
55
+ if (!openEntities || !Array.isArray(openEntities)) return false;
56
+
57
+ // Check wildcard first
58
+ const wildcard = openEntities.find(e => e.name === '*');
59
+ if (wildcard) {
60
+ if (!action) return true;
61
+ if (wildcard.actions.includes(action.toLowerCase())) return true;
62
+ }
63
+
64
+ const entry = openEntities.find(e => e.name.toLowerCase() === entityName.toLowerCase());
65
+ if (entry) {
66
+ if (!action) return true;
67
+ if (entry.actions.includes(action.toLowerCase())) return true;
68
+ }
69
+
70
+ return false;
71
+ };
72
+
52
73
  // 1. Add Entity Paths
53
74
  for (const entity of entities) {
54
75
  const name = entity.name.toLowerCase();
@@ -68,22 +89,34 @@ export const generateSwaggerDocs = async (config, entities, outputDir) => {
68
89
  }
69
90
  };
70
91
 
71
- // POST /entities
72
- swagger.paths[`/${name}s`] = {
73
- post: {
74
- tags: [capitalized],
92
+ const addEntityOperations = (basePath, isPublic) => {
93
+ const security = isPublic ? [{ TenantID: [] }] : undefined; // undefined uses global security
94
+
95
+ // Unified helper for path registration based on permissions
96
+ const regPath = (path, method, op, action) => {
97
+ if (isPublic && !isOpen(capitalized, action)) return;
98
+
99
+ if (!swagger.paths[path]) swagger.paths[path] = {};
100
+ swagger.paths[path][method] = {
101
+ ...op,
102
+ tags: [isPublic ? `${capitalized} (Public)` : capitalized],
103
+ summary: `${op.summary} ${isPublic ? '(Public)' : ''}`,
104
+ security: security
105
+ };
106
+ };
107
+
108
+ // CRUD Operations
109
+ regPath(`${basePath}/${name}s`, 'post', {
75
110
  summary: `Create a new ${capitalized}`,
76
111
  parameters: [...commonHeaders],
77
112
  requestBody: {
78
113
  required: true,
79
114
  content: { 'application/json': { schema: { $ref: `#/components/schemas/${capitalized}` } } }
80
115
  },
81
- responses: {
82
- 201: { description: 'Created', content: { 'application/json': { schema: { $ref: `#/components/schemas/${capitalized}` } } } }
83
- }
84
- },
85
- get: {
86
- tags: [capitalized],
116
+ responses: { 201: { description: 'Created', content: { 'application/json': { schema: { $ref: `#/components/schemas/${capitalized}` } } } } }
117
+ }, 'create');
118
+
119
+ regPath(`${basePath}/${name}s`, 'get', {
87
120
  summary: `Get all ${capitalized}s`,
88
121
  parameters: [
89
122
  ...commonHeaders,
@@ -91,72 +124,59 @@ export const generateSwaggerDocs = async (config, entities, outputDir) => {
91
124
  { name: 'size', in: 'query', schema: { type: 'integer' }, description: 'Records per page' },
92
125
  { name: 'eager', in: 'query', schema: { type: 'boolean' }, description: 'If true, performs SQL Join to fetch relations' }
93
126
  ],
94
- responses: {
95
- 200: { description: 'OK', content: { 'application/json': { schema: { type: 'array', items: { $ref: `#/components/schemas/${capitalized}` } } } } }
96
- }
97
- }
98
- };
127
+ responses: { 200: { description: 'OK', content: { 'application/json': { schema: { type: 'array', items: { $ref: `#/components/schemas/${capitalized}` } } } } } }
128
+ }, 'read');
99
129
 
100
- // GET/PUT/PATCH/DELETE /entities/:id
101
- swagger.paths[`/${name}s/{id}`] = {
102
- get: {
103
- tags: [capitalized],
130
+ regPath(`${basePath}/${name}s/{id}`, 'get', {
104
131
  summary: `Get ${capitalized} by ID`,
105
132
  parameters: [
106
133
  ...commonHeaders,
107
134
  { name: 'id', in: 'path', required: true, schema: { type: 'integer' } },
108
135
  { name: 'eager', in: 'query', schema: { type: 'boolean' } }
109
136
  ],
110
- responses: {
111
- 200: { description: 'OK', content: { 'application/json': { schema: { $ref: `#/components/schemas/${capitalized}` } } } }
112
- }
113
- },
114
- put: {
115
- tags: [capitalized],
137
+ responses: { 200: { description: 'OK', content: { 'application/json': { schema: { $ref: `#/components/schemas/${capitalized}` } } } } }
138
+ }, 'read');
139
+
140
+ regPath(`${basePath}/${name}s/{id}`, 'put', {
116
141
  summary: `Update ${capitalized}`,
117
142
  parameters: [...commonHeaders, { name: 'id', in: 'path', required: true, schema: { type: 'integer' } }],
118
- responses: {
119
- 200: { description: 'Updated', content: { 'application/json': { schema: { $ref: `#/components/schemas/${capitalized}` } } } }
120
- }
121
- },
122
- delete: {
123
- tags: [capitalized],
143
+ responses: { 200: { description: 'Updated', content: { 'application/json': { schema: { $ref: `#/components/schemas/${capitalized}` } } } } }
144
+ }, 'update');
145
+
146
+ regPath(`${basePath}/${name}s/{id}`, 'patch', {
147
+ summary: `Patch ${capitalized}`,
148
+ parameters: [...commonHeaders, { name: 'id', in: 'path', required: true, schema: { type: 'integer' } }],
149
+ responses: { 200: { description: 'Patched' } }
150
+ }, 'update');
151
+
152
+ regPath(`${basePath}/${name}s/{id}`, 'delete', {
124
153
  summary: `Delete ${capitalized}`,
125
154
  parameters: [...commonHeaders, { name: 'id', in: 'path', required: true, schema: { type: 'integer' } }],
126
- responses: {
127
- 204: { description: 'No Content' }
128
- }
129
- }
130
- };
155
+ responses: { 204: { description: 'No Content' } }
156
+ }, 'delete');
131
157
 
132
- // BULK Operations /entities/bulk
133
- swagger.paths[`/${name}s/bulk`] = {
134
- post: {
135
- tags: [capitalized],
158
+ // Bulk Operations
159
+ regPath(`${basePath}/${name}s/bulk`, 'post', {
136
160
  summary: `Bulk Create ${capitalized}s`,
137
161
  parameters: [...commonHeaders],
138
162
  requestBody: {
139
163
  required: true,
140
164
  content: { 'application/json': { schema: { type: 'array', items: { $ref: `#/components/schemas/${capitalized}` } } } }
141
165
  },
142
- responses: {
143
- 201: { description: 'Created', content: { 'application/json': { schema: { type: 'array', items: { $ref: `#/components/schemas/${capitalized}` } } } } }
144
- }
145
- },
146
- put: {
147
- tags: [capitalized],
166
+ responses: { 201: { description: 'Created' } }
167
+ }, 'create');
168
+
169
+ regPath(`${basePath}/${name}s/bulk`, 'put', {
148
170
  summary: `Bulk Update ${capitalized}s`,
149
171
  parameters: [...commonHeaders],
150
172
  requestBody: {
151
173
  required: true,
152
174
  content: { 'application/json': { schema: { type: 'array', items: { $ref: `#/components/schemas/${capitalized}` } } } }
153
175
  },
154
- responses: {
155
- 200: { description: 'Updated', content: { 'application/json': { schema: { type: 'array', items: { $ref: `#/components/schemas/${capitalized}` } } } } }
156
- }
157
- },
158
- patch: {
159
- tags: [capitalized],
176
+ responses: { 200: { description: 'Updated' } }
177
+ }, 'update');
178
+
179
+ regPath(`${basePath}/${name}s/bulk`, 'patch', {
160
180
  summary: `Bulk Patch ${capitalized}s`,
161
181
  parameters: [...commonHeaders],
162
182
  requestBody: {
@@ -173,14 +193,18 @@ export const generateSwaggerDocs = async (config, entities, outputDir) => {
173
193
  }
174
194
  }
175
195
  }
176
- }
196
+ }
177
197
  }
178
198
  },
179
- responses: {
180
- 200: { description: 'Patched' }
181
- }
182
- }
199
+ responses: { 200: { description: 'Patched' } }
200
+ }, 'update');
183
201
  };
202
+
203
+ // 1a. Secured Paths
204
+ addEntityOperations('/api', false);
205
+
206
+ // 1b. Public Paths (if marked as open)
207
+ addEntityOperations('/open/api', true);
184
208
  }
185
209
 
186
210
  // 2. Add System Paths
@@ -4,7 +4,7 @@ import chalk from 'chalk';
4
4
 
5
5
  export const generateTelemetryCode = async (config, outputDir) => {
6
6
  const telemetryDir = path.join(outputDir, 'internal/telemetry');
7
- const k8sDir = path.join(outputDir, 'k8s');
7
+ const k8sDir = path.join(outputDir, 'devops/k8s');
8
8
 
9
9
  await fs.ensureDir(telemetryDir);
10
10
  await fs.ensureDir(k8sDir);
@@ -16,7 +16,6 @@ import (
16
16
  "context"
17
17
  "fmt"
18
18
  "log"
19
- "time"
20
19
 
21
20
  "{{app_name}}/config"
22
21
  "go.opentelemetry.io/otel"
@@ -82,36 +81,28 @@ func InitTelemetry(cfg *config.Config) (func(context.Context) error, error) {
82
81
  `;
83
82
 
84
83
  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]
84
+ receivers:
85
+ otlp:
86
+ protocols:
87
+ grpc:
88
+ http:
89
+ processors:
90
+ batch:
91
+ resourcedetection:
92
+ detectors: [env, system]
93
+ exporters:
94
+ logging:
95
+ loglevel: debug
96
+ otlp:
97
+ endpoint: "jaeger:4317"
98
+ tls:
99
+ insecure: true
100
+ service:
101
+ pipelines:
102
+ traces:
103
+ receivers: [otlp]
104
+ processors: [batch, resourcedetection]
105
+ exporters: [logging, otlp]
115
106
  `;
116
107
 
117
108
  await fs.writeFile(path.join(telemetryDir, 'otel.go'), otelGo.replace(/{{app_name}}/g, config.name));
@@ -18,8 +18,6 @@ import (
18
18
  "fmt"
19
19
  "log"
20
20
  "net/http"
21
- "sync"
22
- "{{app_name}}/controllers"
23
21
 
24
22
  "github.com/gin-gonic/gin"
25
23
  "github.com/gorilla/websocket"
@@ -98,9 +96,16 @@ func (d *RESToverWSDispatcher) HandleConnection(c *gin.Context) {
98
96
  span.End()
99
97
  continue
100
98
  }
99
+ siloConns, exists := c.Get("tenantSiloConns")
100
+ if !exists {
101
+ d.sendError(conn, "Unauthorized: No tenant context", wsMsg.Action, wsMsg.TraceParent)
102
+ span.End()
103
+ continue
104
+ }
105
+ conns := siloConns.(map[string]*gorm.DB)
101
106
 
102
- // 4. Dispatch to MVC Controllers
103
- d.dispatch(childCtx, conn, wsMsg)
107
+ // 4. Dispatch to MVC Controllers (Federated)
108
+ d.dispatch(childCtx, conn, wsMsg, conns)
104
109
  span.End()
105
110
  }
106
111
  }
@@ -112,13 +117,30 @@ func (d *RESToverWSDispatcher) verifySignature(payload []byte, signature string)
112
117
  return hmac.Equal([]byte(expectedSignature), []byte(signature))
113
118
  }
114
119
 
115
- func (d *RESToverWSDispatcher) dispatch(ctx context.Context, conn *websocket.Conn, msg WSMessage) {
120
+ func (d *RESToverWSDispatcher) dispatch(ctx context.Context, conn *websocket.Conn, msg WSMessage, conns map[string]*gorm.DB) {
116
121
  // Inject current span into response header for client to trace back
117
122
  carrier := propagation.HeaderCarrier{}
118
123
  otel.GetTextMapPropagator().Inject(ctx, carrier)
119
124
  tp := carrier.Get("traceparent")
120
125
 
121
126
  switch msg.Action {
127
+ {{#each entities}}
128
+ case "GET_{{upper name}}S":
129
+ federatedData := make(map[string]interface{})
130
+ for role, db := range conns {
131
+ var list []map[string]interface{}
132
+ db.WithContext(ctx).Table("{{lower name}}").Find(&list)
133
+ federatedData[role] = list
134
+ }
135
+ d.sendResponse(conn, msg.Action, federatedData, tp)
136
+ case "CREATE_{{upper name}}":
137
+ for role, db := range conns {
138
+ // Proxy creation to DB
139
+ db.WithContext(ctx).Exec("INSERT INTO {{lower name}} (data) VALUES (?)", string(msg.Payload))
140
+ log.Printf("Federated JS broadcast to silo: %s", role)
141
+ }
142
+ d.sendResponse(conn, msg.Action, "Federated broadcast complete", tp)
143
+ {{/each}}
122
144
  default:
123
145
  d.sendError(conn, "Unknown action", msg.Action, tp)
124
146
  }
@@ -135,28 +157,40 @@ func (d *RESToverWSDispatcher) sendError(conn *websocket.Conn, err string, actio
135
157
  }
136
158
  `;
137
159
 
160
+ // JavaScript Entity helper injection
161
+ let entitiesBlock = "";
162
+ // ... handled by template string above ...
163
+
138
164
  const wsContent = wsHandler.replace(/{{app_name}}/g, config.name);
139
165
  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
- }
166
+
167
+ // Handled via Replace logic to inject entities into the switch
168
+ let entitiesIter = entities.map(e => {
169
+ return `
170
+ case "GET_${e.name.toUpperCase()}S":
171
+ federatedData := make(map[string]interface{})
172
+ for role, db := range conns {
173
+ var list []map[string]interface{}
174
+ db.WithContext(ctx).Table("${e.name.toLowerCase()}").Find(&list)
175
+ federatedData[role] = list
176
+ }
177
+ d.sendResponse(conn, msg.Action, federatedData, tp)
178
+ case "CREATE_${e.name.toUpperCase()}":
179
+ for role, db := range conns {
180
+ _ = db // Acknowledge for POC
181
+ // Simplified broadcast for POC
182
+ d.sendResponse(conn, msg.Action, "Federated broadcast for ${e.name} to " + role, tp)
183
+ }
184
+ `;
185
+ }).join('\n');
152
186
 
153
- finalContent = finalContent.replace("{{#each entities}}", "").replace("{{/each}}", "");
154
- const startTag = "switch msg.Action {";
187
+ finalContent = finalContent.replace('{{#each entities}}', '').replace('{{/each}}', '');
188
+ const startTag = "switch msg.Action {";
155
189
  const endTag = "default:";
156
190
  const parts = finalContent.split(startTag);
157
191
  const endParts = parts[1].split(endTag);
158
- finalContent = parts[0] + startTag + entitiesBlock + endTag + endParts[1];
192
+ finalContent = parts[0] + startTag + entitiesIter + endTag + endParts[1];
159
193
 
160
194
  await fs.writeFile(path.join(wsDir, 'handler.go'), finalContent);
161
- console.log(chalk.gray(' Generated REST-over-WS Dispatcher with Traced-Envelope'));
195
+ console.log(chalk.green(' Federated REST-over-WS Dispatcher generated!'));
162
196
  };