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/devops.js
CHANGED
|
@@ -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,
|
|
6
|
-
const
|
|
7
|
-
const
|
|
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
|
|
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:
|
|
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
|
-
|
|
65
|
+
|
|
66
|
+
EXPOSE 8080
|
|
32
67
|
ENV GO_PROFILE=prod
|
|
33
68
|
ENTRYPOINT ["/app/server"]
|
|
34
69
|
`;
|
|
35
70
|
|
|
36
|
-
// --- 2.
|
|
37
|
-
const
|
|
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:
|
|
61
|
-
POSTGRES_PASSWORD:
|
|
62
|
-
POSTGRES_DB: ${
|
|
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:
|
|
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:
|
|
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:
|
|
144
|
+
image: quay.io/keycloak/keycloak:26.2.3
|
|
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.
|
|
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
|
-
// ---
|
|
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.
|
|
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:
|
|
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
|
-
|
|
206
|
-
|
|
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,
|
|
378
|
+
console.log(chalk.gray(' Generated devops/Dockerfile, devops/services.yml, devops/app.yml, devops/docker-compose.yml & GitHub Actions CI/CD'));
|
|
212
379
|
};
|
package/generators/docs.js
CHANGED
|
@@ -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: '
|
|
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: '
|
|
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
|
+
};
|