go-duck-cli 1.0.8 → 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
@@ -2,64 +2,82 @@ import fs from 'fs-extra';
2
2
  import path from 'path';
3
3
  import chalk from 'chalk';
4
4
 
5
- export const generateDeploymentArtifacts = async (config, outputDir) => {
6
- const k8sDir = path.join(outputDir, 'k8s');
7
- const githubDir = path.join(outputDir, '.github/workflows');
5
+ export const generateDeploymentArtifacts = async (config, projectRootDir) => {
6
+ const devopsDir = path.join(projectRootDir, 'devops');
7
+ const k8sDir = path.join(devopsDir, 'k8s');
8
+ const keycloakDir = path.join(devopsDir, 'keycloak');
9
+ const githubDir = path.join(projectRootDir, '.github', 'workflows');
10
+
11
+ await fs.ensureDir(devopsDir);
8
12
  await fs.ensureDir(k8sDir);
13
+ await fs.ensureDir(keycloakDir);
9
14
  await fs.ensureDir(githubDir);
10
15
 
11
- const appName = config.name || 'go-duck-app';
12
- const appPort = 8080;
16
+ const appName = config.name || 'go-duck';
17
+ const appPort = config.server?.port || 8080;
13
18
 
14
19
  // --- 1. Dockerfile (Multi-stage, lean production image) ---
15
20
  const dockerfile = `
16
21
  # ---- Build Stage ----
17
- FROM golang:1.22-alpine AS builder
22
+ FROM golang:alpine AS builder
18
23
  WORKDIR /app
24
+
25
+ # Install dependencies for protoc and Kratos
26
+ RUN apk add --no-cache protoc git make curl protobuf-dev
27
+
28
+ # Install Kratos and Protoc plugins
29
+ RUN go install github.com/go-kratos/kratos/cmd/kratos/v2@latest
30
+ RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
31
+ RUN go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
32
+ RUN go install github.com/go-kratos/kratos/cmd/protoc-gen-go-http/v2@latest
33
+ RUN go install github.com/go-kratos/kratos/cmd/protoc-gen-go-errors/v2@latest
34
+ RUN go install github.com/google/gnostic/cmd/protoc-gen-openapi@latest
35
+
36
+ # Download standard google protos
37
+ RUN mkdir -p third_party/google/api && \\
38
+ curl -sSL https://raw.githubusercontent.com/googleapis/googleapis/master/google/api/annotations.proto > third_party/google/api/annotations.proto && \\
39
+ curl -sSL https://raw.githubusercontent.com/googleapis/googleapis/master/google/api/http.proto > third_party/google/api/http.proto
40
+
19
41
  COPY go.mod go.sum ./
20
42
  RUN go mod download
21
43
  COPY . .
44
+
45
+ # Generate gRPC and HTTP client/server code
46
+ RUN find api -name "*.proto" -exec protoc --proto_path=. \\
47
+ --proto_path=./api \\
48
+ --proto_path=./third_party \\
49
+ --proto_path=/usr/include \\
50
+ --go_out=paths=source_relative:. \\
51
+ --go-grpc_out=paths=source_relative:. \\
52
+ --go-http_out=paths=source_relative:. \\
53
+ {} +
54
+
22
55
  RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server .
23
56
 
24
57
  # ---- Final Stage ----
25
58
  FROM gcr.io/distroless/static-debian12
26
59
  WORKDIR /app
60
+
27
61
  COPY --from=builder /app/server .
28
62
  COPY --from=builder /app/application.yml .
29
63
  COPY --from=builder /app/application-dev.yml .
30
64
  COPY --from=builder /app/application-prod.yml .
31
- EXPOSE ${appPort}
65
+
66
+ EXPOSE 8080
32
67
  ENV GO_PROFILE=prod
33
68
  ENTRYPOINT ["/app/server"]
34
69
  `;
35
70
 
36
- // --- 2. Docker Compose (Full local dev environment) ---
37
- const dockerCompose = `
38
- version: '3.9'
39
-
71
+ // --- 2. services.yml (Infrastructure Services Only - Hard-Pinned Versions for Docker Desktop Stability) ---
72
+ const servicesYaml = `
40
73
  services:
41
- app:
42
- build: .
43
- container_name: ${appName}
44
- ports:
45
- - "${appPort}:${appPort}"
46
- environment:
47
- - GO_PROFILE=dev
48
- depends_on:
49
- - postgres
50
- - redis
51
- - mosquitto
52
- - otel-collector
53
- networks:
54
- - go-duck-net
55
-
56
74
  postgres:
57
- image: postgres:15-alpine
75
+ image: postgres:15.6-alpine
58
76
  container_name: ${appName}-postgres
59
77
  environment:
60
- POSTGRES_USER: go_duck_user
61
- POSTGRES_PASSWORD: go_duck_pass
62
- POSTGRES_DB: ${appName}
78
+ POSTGRES_USER: ${config.datasource?.username || 'postgres'}
79
+ POSTGRES_PASSWORD: ${config.datasource?.password || 'password'}
80
+ POSTGRES_DB: ${config.datasource?.database || 'go_duck_master'}
63
81
  ports:
64
82
  - "5432:5432"
65
83
  volumes:
@@ -68,7 +86,7 @@ services:
68
86
  - go-duck-net
69
87
 
70
88
  redis:
71
- image: redis:7-alpine
89
+ image: redis:7.2.4-alpine
72
90
  container_name: ${appName}-redis
73
91
  ports:
74
92
  - "6379:6379"
@@ -77,7 +95,7 @@ services:
77
95
  - go-duck-net
78
96
 
79
97
  mosquitto:
80
- image: eclipse-mosquitto:2
98
+ image: eclipse-mosquitto:2.0.18
81
99
  container_name: ${appName}-mqtt
82
100
  ports:
83
101
  - "1883:1883"
@@ -88,7 +106,7 @@ services:
88
106
  - go-duck-net
89
107
 
90
108
  otel-collector:
91
- image: otel/opentelemetry-collector-contrib:latest
109
+ image: otel/opentelemetry-collector-contrib:0.96.0
92
110
  container_name: ${appName}-otel
93
111
  command: ["--config=/etc/otel-collector-config.yaml"]
94
112
  volumes:
@@ -102,7 +120,7 @@ services:
102
120
  - go-duck-net
103
121
 
104
122
  jaeger:
105
- image: jaegertracing/all-in-one:latest
123
+ image: jaegertracing/all-in-one:1.55
106
124
  container_name: ${appName}-jaeger
107
125
  ports:
108
126
  - "16686:16686"
@@ -110,13 +128,27 @@ services:
110
128
  networks:
111
129
  - go-duck-net
112
130
 
131
+ elasticsearch:
132
+ image: docker.elastic.co/elasticsearch/elasticsearch:8.12.2
133
+ container_name: ${appName}-elasticsearch
134
+ environment:
135
+ - discovery.type=single-node
136
+ - xpack.security.enabled=false
137
+ - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
138
+ ports:
139
+ - "9200:9200"
140
+ networks:
141
+ - go-duck-net
142
+
113
143
  keycloak:
114
- image: quay.io/keycloak/keycloak:23.0
144
+ image: quay.io/keycloak/keycloak:23.0.7
115
145
  container_name: ${appName}-keycloak
116
- command: start-dev
146
+ command: start-dev --import-realm
117
147
  environment:
118
148
  KEYCLOAK_ADMIN: admin
119
149
  KEYCLOAK_ADMIN_PASSWORD: admin
150
+ volumes:
151
+ - ./keycloak/realm-config:/opt/keycloak/data/import
120
152
  ports:
121
153
  - "8180:8080"
122
154
  networks:
@@ -130,7 +162,83 @@ networks:
130
162
  driver: bridge
131
163
  `;
132
164
 
133
- // --- 3. MQTT Broker Config ---
165
+ // --- 3. app.yml (App Service Only - To run the built image) ---
166
+ const appYaml = `
167
+ services:
168
+ app:
169
+ build:
170
+ context: ..
171
+ dockerfile: devops/Dockerfile
172
+ image: ${appName}:latest
173
+ container_name: ${appName}
174
+ ports:
175
+ - "${appPort}:${appPort}"
176
+ environment:
177
+ - GO_PROFILE=dev
178
+ - GO_DUCK_DATASOURCE_HOST=postgres
179
+ - GO_DUCK_DATASOURCE_USERNAME=${config.datasource?.username || 'postgres'}
180
+ - GO_DUCK_DATASOURCE_PASSWORD=${config.datasource?.password || 'password'}
181
+ - GO_DUCK_DATASOURCE_DATABASE=${config.datasource?.database || 'go_duck_master'}
182
+ - GO_DUCK_DATASOURCE_PORT=5432
183
+ - GO_DUCK_CACHE_REDIS_HOST=redis:6379
184
+ - GO_DUCK_MESSAGING_MQTT_BROKER=tcp://mosquitto:1883
185
+ - GO_DUCK_TELEMETRY_OTEL_ENDPOINT=otel-collector:4317
186
+ - GO_DUCK_ELASTICSEARCH_ADDRESSES=http://elasticsearch:9200
187
+ restart: always
188
+ networks:
189
+ - go-duck-net
190
+
191
+ networks:
192
+ go-duck-net:
193
+ external: true
194
+ name: devops_go-duck-net
195
+ `;
196
+
197
+ // --- 4. docker-compose.yml (The Main Entry Point - Links app + services) ---
198
+ const dockerCompose = `
199
+ include:
200
+ - path: services.yml
201
+
202
+ services:
203
+ app:
204
+ build:
205
+ context: ..
206
+ dockerfile: devops/Dockerfile
207
+ container_name: ${appName}
208
+ ports:
209
+ - "${appPort}:${appPort}"
210
+ environment:
211
+ - GO_PROFILE=dev
212
+ - GO_DUCK_DATASOURCE_HOST=postgres
213
+ - GO_DUCK_DATASOURCE_USERNAME=${config.datasource?.username || 'postgres'}
214
+ - GO_DUCK_DATASOURCE_PASSWORD=${config.datasource?.password || 'password'}
215
+ - GO_DUCK_DATASOURCE_DATABASE=${config.datasource?.database || 'go_duck_master'}
216
+ - GO_DUCK_DATASOURCE_PORT=5432
217
+ - GO_DUCK_CACHE_REDIS_HOST=redis:6379
218
+ - GO_DUCK_MESSAGING_MQTT_BROKER=tcp://mosquitto:1883
219
+ - GO_DUCK_TELEMETRY_OTEL_ENDPOINT=otel-collector:4317
220
+ - GO_DUCK_ELASTICSEARCH_ADDRESSES=http://elasticsearch:9200
221
+ depends_on:
222
+ postgres:
223
+ condition: service_started
224
+ redis:
225
+ condition: service_started
226
+ mosquitto:
227
+ condition: service_started
228
+ otel-collector:
229
+ condition: service_started
230
+ elasticsearch:
231
+ condition: service_started
232
+ networks:
233
+ - go-duck-net
234
+
235
+ networks:
236
+ go-duck-net:
237
+ external: true
238
+ name: devops_go-duck-net
239
+ `;
240
+
241
+ // --- 5. MQTT Broker Config ---
134
242
  const mosquittoConf = `
135
243
  listener 1883
136
244
  listener 9001
@@ -138,7 +246,7 @@ protocol websockets
138
246
  allow_anonymous true
139
247
  `;
140
248
 
141
- // --- 4. GitHub Actions CI/CD ---
249
+ // --- 6. GitHub Actions CI/CD ---
142
250
  const ciWorkflow = `
143
251
  name: CI - Build & Test
144
252
 
@@ -157,7 +265,7 @@ jobs:
157
265
  - name: Set up Go
158
266
  uses: actions/setup-go@v5
159
267
  with:
160
- go-version: '1.22'
268
+ go-version: '1.24'
161
269
 
162
270
  - name: Cache Go modules
163
271
  uses: actions/cache@v3
@@ -172,7 +280,7 @@ jobs:
172
280
  run: go build -v ./...
173
281
 
174
282
  - name: Run Tests
175
- run: go test -v ./...
283
+ run: test -v ./...
176
284
  `;
177
285
 
178
286
  const cdWorkflow = `
@@ -202,11 +310,70 @@ jobs:
202
310
  tags: \${{ secrets.DOCKER_USERNAME }}/${appName}:latest,\${{ secrets.DOCKER_USERNAME }}/${appName}:\${{ github.sha }}
203
311
  `;
204
312
 
205
- await fs.writeFile(path.join(outputDir, 'Dockerfile'), dockerfile);
206
- await fs.writeFile(path.join(outputDir, 'docker-compose.yml'), dockerCompose);
313
+ // --- 7. Keycloak Realm Export ---
314
+ const realmName = config.security?.['keycloak-realm'] || 'master';
315
+ const realmJson = JSON.stringify({
316
+ "id": realmName,
317
+ "realm": realmName,
318
+ "enabled": true,
319
+ "clients": [
320
+ {
321
+ "clientId": `${config.name}-app`,
322
+ "enabled": true,
323
+ "publicClient": true,
324
+ "standardFlowEnabled": true,
325
+ "directAccessGrantsEnabled": true,
326
+ "protocol": "openid-connect",
327
+ "redirectUris": ["*"],
328
+ "webOrigins": ["*"],
329
+ "fullScopeAllowed": true
330
+ },
331
+ {
332
+ "clientId": `${config.name}-service`,
333
+ "enabled": true,
334
+ "clientAuthenticatorType": "client-secret",
335
+ "secret": "service-secret-123",
336
+ "serviceAccountsEnabled": true,
337
+ "publicClient": false,
338
+ "protocol": "openid-connect",
339
+ "fullScopeAllowed": true
340
+ }
341
+ ],
342
+ "users": [
343
+ {
344
+ "username": "admin",
345
+ "email": "admin@go-duck.io",
346
+ "emailVerified": true,
347
+ "enabled": true,
348
+ "requiredActions": [],
349
+ "credentials": [
350
+ { "type": "password", "value": "admin", "temporary": false }
351
+ ],
352
+ "realmRoles": ["admin", "offline_access"]
353
+ },
354
+ {
355
+ "username": `service-account-${config.name}-service`,
356
+ "enabled": true,
357
+ "serviceAccountClientId": `${config.name}-service`,
358
+ "realmRoles": ["admin"],
359
+ "clientRoles": {
360
+ "realm-management": ["manage-users", "view-users", "query-groups", "query-users", "realm-admin"]
361
+ }
362
+ }
363
+ ]
364
+ }, null, 4);
365
+
366
+ const realmConfigDir = path.join(devopsDir, 'keycloak', 'realm-config');
367
+ await fs.ensureDir(realmConfigDir);
368
+ await fs.writeFile(path.join(realmConfigDir, `${realmName}-realm.json`), realmJson);
369
+
370
+ await fs.writeFile(path.join(devopsDir, 'Dockerfile'), dockerfile);
371
+ await fs.writeFile(path.join(devopsDir, 'services.yml'), servicesYaml);
372
+ await fs.writeFile(path.join(devopsDir, 'app.yml'), appYaml);
373
+ await fs.writeFile(path.join(devopsDir, 'docker-compose.yml'), dockerCompose);
207
374
  await fs.writeFile(path.join(k8sDir, 'mosquitto.conf'), mosquittoConf);
208
375
  await fs.writeFile(path.join(githubDir, 'ci.yml'), ciWorkflow);
209
376
  await fs.writeFile(path.join(githubDir, 'cd.yml'), cdWorkflow);
210
377
 
211
- console.log(chalk.gray(' Generated Dockerfile, Docker Compose & GitHub Actions CI/CD'));
378
+ console.log(chalk.gray(' Generated devops/Dockerfile, devops/services.yml, devops/app.yml, devops/docker-compose.yml & GitHub Actions CI/CD'));
212
379
  };
@@ -4,7 +4,7 @@ import Handlebars from 'handlebars';
4
4
  import chalk from 'chalk';
5
5
  import { fileURLToPath } from 'url';
6
6
 
7
- export const generateDocumentation = async (config, entities, outputDir, enums = []) => {
7
+ export const generateDocumentation = async (config, entities, outputDir, enums = [], openEntities = []) => {
8
8
  console.log(chalk.cyan('Generating Multi-Page Developer Guide Web App...'));
9
9
 
10
10
  const docsDir = path.join(outputDir, 'docs', 'web');
@@ -30,23 +30,42 @@ export const generateDocumentation = async (config, entities, outputDir, enums =
30
30
 
31
31
  const pages = [
32
32
  { file: 'index', title: 'Home' },
33
- { file: 'gdl', title: 'GDL Reference' },
33
+ { file: 'legend', title: 'The Legend of GO-DUCK' },
34
+ { file: 'gdl', title: 'Introduction to GDL' },
35
+ { file: 'gdl-entities', title: 'GDL: Entities & Fields' },
36
+ { file: 'gdl-relationships', title: 'GDL: Relationships' },
37
+ { file: 'gdl-annotations', title: 'GDL: Annotations' },
38
+ { file: 'gdl-advanced', title: 'GDL: Enums & Open Entities' },
34
39
  { file: 'cli', title: 'CLI & Code Injection' },
40
+ { file: 'configuration', title: 'Master Configuration' },
41
+ { file: 'wizard', title: '✨ Config Wizard' },
35
42
  { file: 'rest', title: 'REST & Search API' },
36
43
  { file: 'multitenancy', title: 'Multi-Tenancy' },
44
+ { file: 'federation', title: 'Federated Architecture' },
45
+ { file: 'hybrid-store', title: 'Hybrid-Store Architecture' },
37
46
  { file: 'grpc', title: 'Kratos gRPC API' },
38
47
  { file: 'graphql', title: 'GraphQL Framework' },
39
48
  { file: 'realtime', title: 'WebSockets & MQTT' },
40
49
  { file: 'audit', title: 'Audit & Metering' },
41
50
  { file: 'security', title: 'Security & Auth' },
42
51
  { file: 'observability', title: 'Observability' },
43
- { file: 'integrations', title: 'Client Integrations' }
52
+ { file: 'datadog', title: 'Datadog' },
53
+ { file: 'mosquitto', title: 'Mosquitto' },
54
+ { file: 'redis', title: 'Redis' },
55
+ { file: 'otel', title: 'OpenTelemetry & Jaeger' },
56
+ { file: 'keycloak', title: 'Keycloak' },
57
+ { file: 'saga', title: 'Distributed Saga & Outbox' },
58
+ { file: 'storage', title: 'Native Object Storage' },
59
+ { file: 'elasticsearch', title: 'Elasticsearch Search' },
60
+ { file: 'integrations', title: 'Client Integrations' },
61
+ { file: 'serverless', title: 'Serverless Transformation' }
44
62
  ];
45
63
 
46
64
  const context = {
47
65
  appName: config.name || 'GO-DUCK App',
48
66
  entities: entities,
49
- enums: enums
67
+ enums: enums,
68
+ openEntities: openEntities
50
69
  };
51
70
 
52
71
  for (const page of pages) {
@@ -0,0 +1,263 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+
5
+ export const generateElasticsearchLayer = async (config, entities, outputDir) => {
6
+ console.log(chalk.cyan('Generating Elasticsearch Search Engine Layer...'));
7
+
8
+ const searchDir = path.join(outputDir, 'internal', 'search');
9
+ await fs.ensureDir(searchDir);
10
+
11
+ const clientGo = `package search
12
+
13
+ import (
14
+ "fmt"
15
+ "log"
16
+
17
+ "github.com/elastic/go-elasticsearch/v8"
18
+ "{{appName}}/config"
19
+ )
20
+
21
+ var Client *elasticsearch.Client
22
+
23
+ func InitElasticsearch(cfg *config.Config) {
24
+ if !cfg.GoDuck.Elasticsearch.Enabled {
25
+ return
26
+ }
27
+
28
+ esCfg := elasticsearch.Config{
29
+ Addresses: cfg.GoDuck.Elasticsearch.Addresses,
30
+ Username: cfg.GoDuck.Elasticsearch.Username,
31
+ Password: cfg.GoDuck.Elasticsearch.Password,
32
+ }
33
+
34
+ client, err := elasticsearch.NewClient(esCfg)
35
+ if err != nil {
36
+ log.Fatalf("Error creating the Elasticsearch client: %s", err)
37
+ }
38
+
39
+ res, err := client.Info()
40
+ if err != nil {
41
+ log.Fatalf("Error getting response from Elasticsearch: %s", err)
42
+ }
43
+ defer res.Body.Close()
44
+
45
+ if res.IsError() {
46
+ log.Fatalf("Error response from Elasticsearch: %s", res.String())
47
+ }
48
+
49
+ Client = client
50
+ fmt.Printf("✅ Elasticsearch initialized at %v\\n", cfg.GoDuck.Elasticsearch.Addresses)
51
+ }
52
+ `;
53
+
54
+ const syncGo = `package search
55
+
56
+ import (
57
+ "bytes"
58
+ "context"
59
+ "encoding/json"
60
+ "fmt"
61
+ "strings"
62
+
63
+ "github.com/elastic/go-elasticsearch/v8/esapi"
64
+ "{{appName}}/config"
65
+ )
66
+
67
+ func SyncToElasticsearch(ctx context.Context, entityName string, id interface{}, data interface{}, cfg *config.Config) error {
68
+ if Client == nil {
69
+ return nil
70
+ }
71
+
72
+ indexName := strings.ToLower(fmt.Sprintf("%s%s", cfg.GoDuck.Elasticsearch.IndexPrefix, entityName))
73
+
74
+ payload, err := json.Marshal(data)
75
+ if err != nil {
76
+ return err
77
+ }
78
+
79
+ req := esapi.IndexRequest{
80
+ Index: indexName,
81
+ DocumentID: fmt.Sprintf("%v", id),
82
+ Body: bytes.NewReader(payload),
83
+ Refresh: "true",
84
+ }
85
+
86
+ res, err := req.Do(ctx, Client)
87
+ if err != nil {
88
+ return err
89
+ }
90
+ defer res.Body.Close()
91
+
92
+ if res.IsError() {
93
+ return fmt.Errorf("error indexing document: %s", res.String())
94
+ }
95
+
96
+ return nil
97
+ }
98
+
99
+ func DeleteFromElasticsearch(ctx context.Context, entityName string, id interface{}, cfg *config.Config) error {
100
+ if Client == nil {
101
+ return nil
102
+ }
103
+
104
+ indexName := strings.ToLower(fmt.Sprintf("%s%s", cfg.GoDuck.Elasticsearch.IndexPrefix, entityName))
105
+
106
+ req := esapi.DeleteRequest{
107
+ Index: indexName,
108
+ DocumentID: fmt.Sprintf("%v", id),
109
+ Refresh: "true",
110
+ }
111
+
112
+ res, err := req.Do(ctx, Client)
113
+ if err != nil {
114
+ return err
115
+ }
116
+ defer res.Body.Close()
117
+
118
+ return nil
119
+ }
120
+ `;
121
+
122
+ const queryGo = `package search
123
+
124
+ import (
125
+ "bytes"
126
+ "context"
127
+ "encoding/json"
128
+ "fmt"
129
+ "strings"
130
+
131
+ "github.com/elastic/go-elasticsearch/v8/esapi"
132
+ "{{appName}}/config"
133
+ )
134
+
135
+ type SearchResult struct {
136
+ Total int64 \`json:"total"\`
137
+ Hits []interface{} \`json:"hits"\`
138
+ }
139
+
140
+ // Spring-style Search Execution
141
+ func ExecuteSearch(ctx context.Context, entityName string, queryStr string, cfg *config.Config) (*SearchResult, error) {
142
+ if Client == nil {
143
+ return nil, fmt.Errorf("elasticsearch client not initialized")
144
+ }
145
+
146
+ indexName := strings.ToLower(fmt.Sprintf("%s%s", cfg.GoDuck.Elasticsearch.IndexPrefix, entityName))
147
+
148
+ // Simplified Spring-style Query (Fuzzy Match All Fields or Specific Term)
149
+ var query map[string]interface{}
150
+ if queryStr == "" {
151
+ query = map[string]interface{}{
152
+ "query": map[string]interface{}{
153
+ "match_all": map[string]interface{}{},
154
+ },
155
+ }
156
+ } else {
157
+ query = map[string]interface{}{
158
+ "query": map[string]interface{}{
159
+ "multi_match": map[string]interface{}{
160
+ "query": queryStr,
161
+ "fields": []string{"*"},
162
+ "fuzziness": "AUTO",
163
+ },
164
+ },
165
+ }
166
+ }
167
+
168
+ var buf bytes.Buffer
169
+ if err := json.NewEncoder(&buf).Encode(query); err != nil {
170
+ return nil, err
171
+ }
172
+
173
+ req := esapi.SearchRequest{
174
+ Index: []string{indexName},
175
+ Body: &buf,
176
+ }
177
+
178
+ res, err := req.Do(ctx, Client)
179
+ if err != nil {
180
+ return nil, err
181
+ }
182
+ defer res.Body.Close()
183
+
184
+ if res.IsError() {
185
+ return nil, fmt.Errorf("search error: %s", res.String())
186
+ }
187
+
188
+ var r map[string]interface{}
189
+ if err := json.NewDecoder(res.Body).Decode(&r); err != nil {
190
+ return nil, err
191
+ }
192
+
193
+ hits := r["hits"].(map[string]interface{})["hits"].([]interface{})
194
+ total := int64(r["hits"].(map[string]interface{})["total"].(map[string]interface{})["value"].(float64))
195
+
196
+ results := make([]interface{}, len(hits))
197
+ for i, hit := range hits {
198
+ results[i] = hit.(map[string]interface{})["_source"]
199
+ }
200
+
201
+ return &SearchResult{
202
+ Total: total,
203
+ Hits: results,
204
+ }, nil
205
+ }
206
+ `;
207
+
208
+ const appName = config.name || 'app';
209
+ const replaceAppName = (content) => content.replace(/{{appName}}/g, appName);
210
+
211
+ await fs.writeFile(path.join(searchDir, 'client.go'), replaceAppName(clientGo));
212
+ await fs.writeFile(path.join(searchDir, 'sync.go'), replaceAppName(syncGo));
213
+ await fs.writeFile(path.join(searchDir, 'query.go'), replaceAppName(queryGo));
214
+
215
+ // Generate Search Controller
216
+ const controllerDir = path.join(outputDir, 'controllers');
217
+ await fs.ensureDir(controllerDir);
218
+
219
+ const searchControllerGo = `package controllers
220
+
221
+ import (
222
+ "net/http"
223
+
224
+ "github.com/gin-gonic/gin"
225
+ "{{appName}}/config"
226
+ "{{appName}}/internal/search"
227
+ )
228
+
229
+ type ESSearchController struct {
230
+ Config *config.Config
231
+ }
232
+
233
+ func NewESSearchController(cfg *config.Config) *ESSearchController {
234
+ return &ESSearchController{Config: cfg}
235
+ }
236
+
237
+ // Search godoc
238
+ // @Summary Search entities in Elasticsearch
239
+ // @Description High-performance full-text search with fuzzy matching
240
+ // @Tags Search
241
+ // @Param entity path string true "Entity Name (e.g., car)"
242
+ // @Param q query string false "Search query (Spring-style)"
243
+ // @Produce json
244
+ // @Success 200 {object} search.SearchResult
245
+ // @Router /api/search/{entity} [get]
246
+ func (sc *ESSearchController) GlobalSearch(c *gin.Context) {
247
+ entity := c.Param("entity")
248
+ query := c.Query("q")
249
+
250
+ result, err := search.ExecuteSearch(c.Request.Context(), entity, query, sc.Config)
251
+ if err != nil {
252
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
253
+ return
254
+ }
255
+
256
+ c.JSON(http.StatusOK, result)
257
+ }
258
+ `;
259
+
260
+ await fs.writeFile(path.join(controllerDir, 'es_search_controller.go'), replaceAppName(searchControllerGo));
261
+
262
+ console.log(chalk.green('✅ Elasticsearch Search Engine Layer generated successfully.'));
263
+ };