go-duck-cli 1.3.25 → 1.3.35
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 +4 -1
- package/generators/ai_docs.js +14 -11
- package/generators/postman.js +21 -18
- package/generators/router.js +8 -1
- package/generators/swagger.js +8 -5
- package/package.json +1 -1
- package/templates/docs/pages/index.hbs +12 -2
- package/templates/docs/pages/integrations.hbs +6 -1
- package/templates/go/router.go.hbs +213 -15
package/README.md
CHANGED
|
@@ -59,8 +59,9 @@ GO-DUCK has officially reached the **410% Achievement Status**, evolving from a
|
|
|
59
59
|
| **Silo Discovery & Privacy Proxy** | 🚀 **ELITE (+10%)** | Silo discovery API with physical DB name masking. |
|
|
60
60
|
| **Universal Storage Mesh** | 🚀 **ELITE (+25%)** | Dynamic Hot-Swapping Registry and Distributed Cross-Scan API retrieval. |
|
|
61
61
|
| **WSO2 API Gateway Integration** | 🚀 **ELITE (+15%)** | Automated OpenAPI registration & proxy mapping. |
|
|
62
|
+
| **API Gateway Standards & Swagger UI** | 🚀 **ELITE (+10%)** | Keycloak SSO, Glassmorphism UI, JHipster `/v3/api-docs` compliance. |
|
|
62
63
|
| **Full-Spectrum GDL Evolution** | 🚀 **ELITE (+15%)** | Native DROP/ALTER migrations with dead-code purging. |
|
|
63
|
-
| **TOTAL ACHIEVEMENT STATUS** | 🏆 **
|
|
64
|
+
| **TOTAL ACHIEVEMENT STATUS** | 🏆 **420%** | **ELITE STATUS CONFIRMED.** 👑 |
|
|
64
65
|
|
|
65
66
|
### ✨ Primary Features (The 410% Core)
|
|
66
67
|
|
|
@@ -75,6 +76,8 @@ GO-DUCK has officially reached the **410% Achievement Status**, evolving from a
|
|
|
75
76
|
* **Distributed Saga Consistency**: Integrated **Transactional Outbox** pattern and background workers in every silo to guarantee eventual consistency across the federation.
|
|
76
77
|
* **Zero-Trust Identity Registry**: Decoupled mapping layer ensuring physical database names and internal IDs never leak to the client.
|
|
77
78
|
* **Universal Storage Mesh**: Dynamic Multi-Provider Registry allowing hot-swapping at runtime via `?provider=` queries, alongside Distributed Cross-Scan endpoints to auto-locate files across AWS, GCS, SFTP, and GitHub lakes.
|
|
79
|
+
* **Enterprise API Gateway Compatibility**: Natively exposes the Swagger/OpenAPI JSON specification at `/v3/api-docs` to ensure drop-in compatibility with **JHipster**, **WSO2 API Manager**, and Spring Boot ecosystems.
|
|
80
|
+
* **Next-Gen Swagger UI**: A fully glassmorphism-styled Swagger UI with seamless Keycloak SSO integration. Features automatic JWT token refresh, dynamic `X-Tenant-ID` header injection, and CLI metadata branding.
|
|
78
81
|
* **Spring-style Elasticsearch Search**: Real-time sync for entities marked with `@Searchable`, supporting native `query_string` syntax (wildcards like `*`, booleans, ranges, and fuzzy matching).
|
|
79
82
|
* **SaaS Quota Engine**: Redis-backed API bandwidth tracking with dynamic, hierarchical limits (User vs. Role mapping).
|
|
80
83
|
* **Resilience Layer**: Sony/Gobreaker Integration + Zero-Trust Distributed Redis Rate Limiter.
|
package/generators/ai_docs.js
CHANGED
|
@@ -5,6 +5,9 @@ export const generateAIDocs = async (config, entities, outputDir, enums, openEnt
|
|
|
5
5
|
const aiDocsDir = path.join(outputDir, 'docs', 'ai');
|
|
6
6
|
await fs.ensureDir(aiDocsDir);
|
|
7
7
|
|
|
8
|
+
const apiPrefixRaw = config.server?.rest?.['api-path-prefix'] || '/api';
|
|
9
|
+
const apiPrefix = apiPrefixRaw.endsWith('/') ? apiPrefixRaw.slice(0, -1) : apiPrefixRaw;
|
|
10
|
+
|
|
8
11
|
// 1. ARCHITECTURE.md
|
|
9
12
|
const appName = config.name || 'go-duck-app';
|
|
10
13
|
const hasMongo = config.datasource?.mongodb?.enabled;
|
|
@@ -32,7 +35,7 @@ export const generateAIDocs = async (config, entities, outputDir, enums, openEnt
|
|
|
32
35
|
archContent += `- **Active**: ${storageActive ? 'Yes' : 'No'}\n`;
|
|
33
36
|
if (storageActive) {
|
|
34
37
|
archContent += `- **Enabled Nodes**: ${Object.keys(config.storage).filter(k => config.storage[k]?.enabled).join(', ')}\n`;
|
|
35
|
-
archContent += `- **Endpoints**: \n - Upload: \`POST /
|
|
38
|
+
archContent += `- **Endpoints**: \n - Upload: \`POST ${apiPrefix}/storage/upload?provider=\`\n - Exact Retrieve: \`GET ${apiPrefix}/storage/download/*key?provider=\`\n - Cross-Scan Locate: \`GET ${apiPrefix}/storage/scan/*key\`\n`;
|
|
36
39
|
}
|
|
37
40
|
const bootstrapActive = config.storage?.bootstrap?.enabled;
|
|
38
41
|
if (bootstrapActive) {
|
|
@@ -53,25 +56,25 @@ export const generateAIDocs = async (config, entities, outputDir, enums, openEnt
|
|
|
53
56
|
|
|
54
57
|
// 2. ENDPOINTS.md
|
|
55
58
|
let endpointsContent = `# REST & gRPC API Surface\n\n`;
|
|
56
|
-
endpointsContent += `## Base Path:
|
|
59
|
+
endpointsContent += `## Base Path: \`${apiPrefix}\`\n\n`;
|
|
57
60
|
endpointsContent += `### Standard Entity Endpoints\n`;
|
|
58
61
|
for (const entity of entities) {
|
|
59
62
|
const routeName = entity.name.toLowerCase() + 's';
|
|
60
63
|
endpointsContent += `\n#### ${entity.name}\n`;
|
|
61
|
-
endpointsContent += `- \`GET
|
|
62
|
-
endpointsContent += `- \`GET
|
|
63
|
-
endpointsContent += `- \`POST
|
|
64
|
-
endpointsContent += `- \`PUT
|
|
65
|
-
endpointsContent += `- \`DELETE
|
|
66
|
-
endpointsContent += `- \`POST
|
|
64
|
+
endpointsContent += `- \`GET ${apiPrefix}/${routeName}\` (Pagination & dynamic sorting, e.g. \`?page=1&size=10&eager=true&sort=id,asc\`)\n`;
|
|
65
|
+
endpointsContent += `- \`GET ${apiPrefix}/${routeName}/:id\`\n`;
|
|
66
|
+
endpointsContent += `- \`POST ${apiPrefix}/${routeName}\`\n`;
|
|
67
|
+
endpointsContent += `- \`PUT ${apiPrefix}/${routeName}/:id\`\n`;
|
|
68
|
+
endpointsContent += `- \`DELETE ${apiPrefix}/${routeName}/:id\`\n`;
|
|
69
|
+
endpointsContent += `- \`POST ${apiPrefix}/${routeName}/bulk\` (Bulk Create/Update)\n`;
|
|
67
70
|
if (entity.isSearchable) {
|
|
68
|
-
endpointsContent += `- \`GET /
|
|
71
|
+
endpointsContent += `- \`GET ${apiPrefix}/search/${routeName}?q={query}\` (Elasticsearch)\n`;
|
|
69
72
|
}
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
endpointsContent += `\n## Infrastructure & Management APIs (SuperAdmin Protected)\n`;
|
|
73
|
-
endpointsContent += `- \`GET /
|
|
74
|
-
endpointsContent += `- \`POST /
|
|
76
|
+
endpointsContent += `- \`GET ${apiPrefix}/admin/audit\` (Fetches Global Delta logs from the Centralized Audit Engine)\n`;
|
|
77
|
+
endpointsContent += `- \`POST ${apiPrefix}/admin/metering/limit\` (Set SaaS Quota limits)\n`;
|
|
75
78
|
endpointsContent += `- \`POST /management/tenant/assign\` (Dynamically initialize new Tenant DBs)\n`;
|
|
76
79
|
|
|
77
80
|
endpointsContent += `\n## Open APIs (No JWT required)\n`;
|
package/generators/postman.js
CHANGED
|
@@ -4,6 +4,9 @@ import chalk from 'chalk';
|
|
|
4
4
|
|
|
5
5
|
export const generatePostmanCollection = async (config, entities, outputDir, openEntities = []) => {
|
|
6
6
|
const docsDir = path.join(outputDir, 'docs');
|
|
7
|
+
const apiPrefixRaw = config.server?.rest?.['api-path-prefix'] || '/api';
|
|
8
|
+
const apiPrefix = apiPrefixRaw.endsWith('/') ? apiPrefixRaw.slice(0, -1) : apiPrefixRaw;
|
|
9
|
+
const apiPathArr = apiPrefix.split('/').filter(p => p !== '');
|
|
7
10
|
await fs.ensureDir(docsDir);
|
|
8
11
|
|
|
9
12
|
const collection = {
|
|
@@ -140,11 +143,11 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
|
|
|
140
143
|
{ key: "X-Tenant-ID", value: "{{tenant}}" }
|
|
141
144
|
],
|
|
142
145
|
url: {
|
|
143
|
-
raw:
|
|
146
|
+
raw: `http://{{host}}:{{port}}${apiPrefix}/admin/audit`,
|
|
144
147
|
protocol: "http",
|
|
145
148
|
host: ["{{host}}"],
|
|
146
149
|
port: "{{port}}",
|
|
147
|
-
path: [
|
|
150
|
+
path: [...apiPathArr, "admin", "audit"]
|
|
148
151
|
}
|
|
149
152
|
}
|
|
150
153
|
},
|
|
@@ -157,11 +160,11 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
|
|
|
157
160
|
{ key: "X-Tenant-ID", value: "{{tenant}}" }
|
|
158
161
|
],
|
|
159
162
|
url: {
|
|
160
|
-
raw:
|
|
163
|
+
raw: `http://{{host}}:{{port}}${apiPrefix}/metering/usage`,
|
|
161
164
|
protocol: "http",
|
|
162
165
|
host: ["{{host}}"],
|
|
163
166
|
port: "{{port}}",
|
|
164
|
-
path: [
|
|
167
|
+
path: [...apiPathArr, "metering", "usage"]
|
|
165
168
|
}
|
|
166
169
|
}
|
|
167
170
|
}
|
|
@@ -390,7 +393,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
|
|
|
390
393
|
request: {
|
|
391
394
|
method: "GET",
|
|
392
395
|
header: [ { key: "X-Tenant-ID", value: "{{tenant}}" } ],
|
|
393
|
-
url: { raw: `http://{{host}}:{{port}}/open
|
|
396
|
+
url: { raw: `http://{{host}}:{{port}}/open${apiPrefix}/${name}s?page=1&size=10&eager=true&sort=id,asc`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["open", ...apiPathArr, `${name}s`], query: [ { key: "page", value: "1" }, { key: "size", value: "10" }, { key: "eager", value: "true" }, { key: "sort", value: "id,asc" } ] }
|
|
394
397
|
}
|
|
395
398
|
});
|
|
396
399
|
publicItems.push({
|
|
@@ -398,7 +401,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
|
|
|
398
401
|
request: {
|
|
399
402
|
method: "GET",
|
|
400
403
|
header: [ { key: "X-Tenant-ID", value: "{{tenant}}" } ],
|
|
401
|
-
url: { raw: `http://{{host}}:{{port}}/open
|
|
404
|
+
url: { raw: `http://{{host}}:{{port}}/open${apiPrefix}/${name}s/1`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["open", ...apiPathArr, `${name}s`, "1"] }
|
|
402
405
|
}
|
|
403
406
|
});
|
|
404
407
|
}
|
|
@@ -409,7 +412,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
|
|
|
409
412
|
method: "POST",
|
|
410
413
|
header: [ { key: "X-Tenant-ID", value: "{{tenant}}" }, { key: "Content-Type", value: "application/json" } ],
|
|
411
414
|
body: { mode: "raw", raw: generateDummyJson(entity.fields) },
|
|
412
|
-
url: { raw: `http://{{host}}:{{port}}/open
|
|
415
|
+
url: { raw: `http://{{host}}:{{port}}/open${apiPrefix}/${name}s`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["open", ...apiPathArr, `${name}s`] }
|
|
413
416
|
}
|
|
414
417
|
});
|
|
415
418
|
}
|
|
@@ -425,7 +428,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
|
|
|
425
428
|
request: {
|
|
426
429
|
method: "GET",
|
|
427
430
|
header: [ { key: "Authorization", value: "Bearer {{token}}" }, { key: "X-Tenant-ID", value: "{{tenant}}" } ],
|
|
428
|
-
url: { raw: `http://{{host}}:{{port}}
|
|
431
|
+
url: { raw: `http://{{host}}:{{port}}${apiPrefix}/${name}s?page=1&size=10&eager=true&sort=id,asc`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: [...apiPathArr, `${name}s`], query: [ { key: "page", value: "1" }, { key: "size", value: "10" }, { key: "eager", value: "true" }, { key: "sort", value: "id,asc" } ] }
|
|
429
432
|
}
|
|
430
433
|
},
|
|
431
434
|
{
|
|
@@ -434,7 +437,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
|
|
|
434
437
|
method: "POST",
|
|
435
438
|
header: [ { key: "Authorization", value: "Bearer {{token}}" }, { key: "X-Tenant-ID", value: "{{tenant}}" }, { key: "Content-Type", value: "application/json" } ],
|
|
436
439
|
body: { mode: "raw", raw: generateDummyJson(entity.fields) },
|
|
437
|
-
url: { raw: `http://{{host}}:{{port}}
|
|
440
|
+
url: { raw: `http://{{host}}:{{port}}${apiPrefix}/${name}s`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: [...apiPathArr, `${name}s`] }
|
|
438
441
|
}
|
|
439
442
|
},
|
|
440
443
|
{
|
|
@@ -442,7 +445,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
|
|
|
442
445
|
request: {
|
|
443
446
|
method: "GET",
|
|
444
447
|
header: [ { key: "Authorization", value: "Bearer {{token}}" }, { key: "X-Tenant-ID", value: "{{tenant}}" } ],
|
|
445
|
-
url: { raw: `http://{{host}}:{{port}}
|
|
448
|
+
url: { raw: `http://{{host}}:{{port}}${apiPrefix}/${name}s/1`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: [...apiPathArr, `${name}s`, "1"] }
|
|
446
449
|
}
|
|
447
450
|
},
|
|
448
451
|
{
|
|
@@ -450,7 +453,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
|
|
|
450
453
|
request: {
|
|
451
454
|
method: "GET",
|
|
452
455
|
header: [ { key: "Authorization", value: "Bearer {{token}}" }, { key: "X-Tenant-ID", value: "{{tenant}}" } ],
|
|
453
|
-
url: { raw: `http://{{host}}:{{port}}/
|
|
456
|
+
url: { raw: `http://{{host}}:{{port}}${apiPrefix}/rpc/${name}?id=gt.0`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: [...apiPathArr, "rpc", name], query: [ { key: "id", value: "gt.0" } ] }
|
|
454
457
|
}
|
|
455
458
|
}
|
|
456
459
|
];
|
|
@@ -461,7 +464,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
|
|
|
461
464
|
request: {
|
|
462
465
|
method: "GET",
|
|
463
466
|
header: [ { key: "Authorization", value: "Bearer {{token}}" }, { key: "X-Tenant-ID", value: "{{tenant}}" } ],
|
|
464
|
-
url: { raw: `http://{{host}}:{{port}}/
|
|
467
|
+
url: { raw: `http://{{host}}:{{port}}${apiPrefix}/search/${name}?q=Sample`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: [...apiPathArr, "search", name], query: [ { key: "q", value: "Sample" } ] }
|
|
465
468
|
}
|
|
466
469
|
});
|
|
467
470
|
}
|
|
@@ -513,11 +516,11 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
|
|
|
513
516
|
]
|
|
514
517
|
},
|
|
515
518
|
url: {
|
|
516
|
-
raw:
|
|
519
|
+
raw: `http://{{host}}:{{port}}${apiPrefix}/storage/upload?provider=sftp`,
|
|
517
520
|
protocol: "http",
|
|
518
521
|
host: ["{{host}}"],
|
|
519
522
|
port: "{{port}}",
|
|
520
|
-
path: [
|
|
523
|
+
path: [...apiPathArr, "storage", "upload"],
|
|
521
524
|
query: [{ key: "provider", value: "sftp" }]
|
|
522
525
|
}
|
|
523
526
|
}
|
|
@@ -531,11 +534,11 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
|
|
|
531
534
|
{ key: "X-Tenant-ID", value: "{{tenant}}" }
|
|
532
535
|
],
|
|
533
536
|
url: {
|
|
534
|
-
raw:
|
|
537
|
+
raw: `http://{{host}}:{{port}}${apiPrefix}/storage/download/farm/animals/photo.jpg?provider=sftp`,
|
|
535
538
|
protocol: "http",
|
|
536
539
|
host: ["{{host}}"],
|
|
537
540
|
port: "{{port}}",
|
|
538
|
-
path: [
|
|
541
|
+
path: [...apiPathArr, "storage", "download", "farm", "animals", "photo.jpg"],
|
|
539
542
|
query: [{ key: "provider", value: "sftp" }]
|
|
540
543
|
}
|
|
541
544
|
}
|
|
@@ -549,11 +552,11 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
|
|
|
549
552
|
{ key: "X-Tenant-ID", value: "{{tenant}}" }
|
|
550
553
|
],
|
|
551
554
|
url: {
|
|
552
|
-
raw:
|
|
555
|
+
raw: `http://{{host}}:{{port}}${apiPrefix}/storage/scan/farm/animals/photo.jpg`,
|
|
553
556
|
protocol: "http",
|
|
554
557
|
host: ["{{host}}"],
|
|
555
558
|
port: "{{port}}",
|
|
556
|
-
path: [
|
|
559
|
+
path: [...apiPathArr, "storage", "scan", "farm", "animals", "photo.jpg"]
|
|
557
560
|
}
|
|
558
561
|
}
|
|
559
562
|
}
|
package/generators/router.js
CHANGED
|
@@ -19,10 +19,17 @@ export const generateRouterCode = async (outputDir, config, entities, openEntiti
|
|
|
19
19
|
const templateSource = await fs.readFile(templatePath, 'utf8');
|
|
20
20
|
const template = Handlebars.compile(templateSource);
|
|
21
21
|
|
|
22
|
+
const packageJsonPath = path.resolve(__dirname, '../package.json');
|
|
23
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
24
|
+
const cliVersion = packageJson.version;
|
|
25
|
+
const generatedDate = new Date().toISOString();
|
|
26
|
+
|
|
22
27
|
const content = template({
|
|
23
28
|
app_name: config.name,
|
|
24
29
|
entities,
|
|
25
|
-
openEntities
|
|
30
|
+
openEntities,
|
|
31
|
+
cli_version: cliVersion,
|
|
32
|
+
generated_date: generatedDate
|
|
26
33
|
});
|
|
27
34
|
|
|
28
35
|
await fs.writeFile(path.join(routerDir, 'router.go'), content);
|
package/generators/swagger.js
CHANGED
|
@@ -6,6 +6,9 @@ export const generateSwaggerDocs = async (config, entities, outputDir, openEntit
|
|
|
6
6
|
const docsDir = path.join(outputDir, 'docs');
|
|
7
7
|
await fs.ensureDir(docsDir);
|
|
8
8
|
|
|
9
|
+
const apiPrefixRaw = config.server?.rest?.['api-path-prefix'] || '/api';
|
|
10
|
+
const apiPrefix = apiPrefixRaw.endsWith('/') ? apiPrefixRaw.slice(0, -1) : apiPrefixRaw;
|
|
11
|
+
|
|
9
12
|
const swagger = {
|
|
10
13
|
openapi: '3.0.0',
|
|
11
14
|
info: {
|
|
@@ -201,14 +204,14 @@ export const generateSwaggerDocs = async (config, entities, outputDir, openEntit
|
|
|
201
204
|
};
|
|
202
205
|
|
|
203
206
|
// 1a. Secured Paths
|
|
204
|
-
addEntityOperations(
|
|
207
|
+
addEntityOperations(apiPrefix, false);
|
|
205
208
|
|
|
206
209
|
// 1b. Public Paths (if marked as open)
|
|
207
|
-
addEntityOperations('/open
|
|
210
|
+
addEntityOperations('/open' + apiPrefix, true);
|
|
208
211
|
}
|
|
209
212
|
|
|
210
213
|
// 2. Add System Paths
|
|
211
|
-
swagger.paths[
|
|
214
|
+
swagger.paths[`${apiPrefix}/rpc/{table}`] = {
|
|
212
215
|
get: {
|
|
213
216
|
tags: ['Search Engine'],
|
|
214
217
|
summary: 'Generic PostgREST RPC Engine',
|
|
@@ -240,7 +243,7 @@ export const generateSwaggerDocs = async (config, entities, outputDir, openEntit
|
|
|
240
243
|
}
|
|
241
244
|
};
|
|
242
245
|
|
|
243
|
-
swagger.paths[
|
|
246
|
+
swagger.paths[`${apiPrefix}/admin/audit`] = {
|
|
244
247
|
get: {
|
|
245
248
|
tags: ['Observability'],
|
|
246
249
|
summary: 'Fetch Audit Trail',
|
|
@@ -281,7 +284,7 @@ export const generateSwaggerDocs = async (config, entities, outputDir, openEntit
|
|
|
281
284
|
};
|
|
282
285
|
|
|
283
286
|
if (config.elasticsearch?.enabled) {
|
|
284
|
-
swagger.paths[
|
|
287
|
+
swagger.paths[`${apiPrefix}/search/{entity}`] = {
|
|
285
288
|
get: {
|
|
286
289
|
tags: ['Search Engine'],
|
|
287
290
|
summary: 'Elasticsearch Global Search',
|
package/package.json
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
|
17
17
|
<span class="relative inline-flex rounded-full h-3 w-3 bg-emerald-500"></span>
|
|
18
18
|
</span>
|
|
19
|
-
<p class="text-[11px] font-black text-indigo-900 uppercase tracking-[0.25em]">The
|
|
19
|
+
<p class="text-[11px] font-black text-indigo-900 uppercase tracking-[0.25em]">The 420% Elite Milestone Surpassed</p>
|
|
20
20
|
</div>
|
|
21
21
|
|
|
22
22
|
<!-- Heroic Logo Anchor (Enhanced) -->
|
|
@@ -121,6 +121,16 @@
|
|
|
121
121
|
</div>
|
|
122
122
|
</div>
|
|
123
123
|
|
|
124
|
+
<div class="lg:col-span-12 bg-white p-10 rounded-[2.5rem] border border-amber-100 shadow-sm hover:shadow-2xl transition-all group relative overflow-hidden cursor-crosshair bg-gradient-to-br from-white to-amber-50/30">
|
|
125
|
+
<p class="text-[9px] font-bold text-amber-600 uppercase tracking-widest mb-4">Elite Extension (+10%)</p>
|
|
126
|
+
<h4 class="text-2xl font-black text-slate-900 mb-3 tracking-tight italic">API Gateway Standards & Swagger UI</h4>
|
|
127
|
+
<p class="text-slate-600 leading-relaxed mb-8">Natively exposes OpenAPI JSON at the JHipster-standard <code>/v3/api-docs</code> endpoint. Features a completely re-engineered, glassmorphism Swagger UI deeply integrated with Keycloak SSO and automatic token refreshing.</p>
|
|
128
|
+
<div class="flex gap-3">
|
|
129
|
+
<span class="px-4 py-1.5 bg-amber-100 text-amber-700 text-[9px] font-black rounded-lg">JHIPSTER COMPATIBLE</span>
|
|
130
|
+
<span class="px-4 py-1.5 bg-amber-100 text-amber-700 text-[9px] font-black rounded-lg">KEYCLOAK SSO UI</span>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
124
134
|
<div class="lg:col-span-12 bg-white p-10 rounded-[2.5rem] border border-fuchsia-100 shadow-sm hover:shadow-2xl transition-all group relative overflow-hidden cursor-crosshair bg-gradient-to-br from-white to-fuchsia-50/30">
|
|
125
135
|
<p class="text-[9px] font-bold text-fuchsia-600 uppercase tracking-widest mb-4">Elite Extension (+12%)</p>
|
|
126
136
|
<h4 class="text-2xl font-black text-slate-900 mb-3 tracking-tight italic">K8s Network Isolation & NodePorts</h4>
|
|
@@ -436,7 +446,7 @@
|
|
|
436
446
|
</div>
|
|
437
447
|
|
|
438
448
|
<div class="mt-24 text-slate-500 font-mono text-[10px] uppercase tracking-[0.6em] font-black relative z-10">
|
|
439
|
-
GO-DUCK • THE
|
|
449
|
+
GO-DUCK • THE 420% ELITE MILESTONE • SOVEREIGN CODE ORCHESTRATION
|
|
440
450
|
</div>
|
|
441
451
|
</div>
|
|
442
452
|
</section>
|
|
@@ -86,8 +86,13 @@ class ApiProvider {
|
|
|
86
86
|
<h2 class="text-2xl font-bold text-gray-800 mb-4 border-b pb-2">3. WSO2 API Manager Integration</h2>
|
|
87
87
|
<p class="mb-4">GO-DUCK generated APIs can automatically register themselves with a <strong>WSO2 API Manager</strong> instance on startup. This uses the WSO2 Publisher REST API to import your dynamically generated OpenAPI 3.0 specs.</p>
|
|
88
88
|
|
|
89
|
+
<div class="bg-indigo-50 border-l-4 border-indigo-500 p-4 mb-6 rounded-r">
|
|
90
|
+
<h4 class="font-bold text-indigo-900 mb-1">Standard API Gateway Discovery</h4>
|
|
91
|
+
<p class="text-sm text-indigo-800">For external gateways (Kong, Apigee) and ecosystems like <strong>JHipster</strong> and <strong>Spring Boot</strong>, your microservice natively exposes its OpenAPI JSON at the standard <code>/v3/api-docs</code> endpoint. Legacy systems can still use <code>/swagger.json</code>.</p>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
89
94
|
<div class="bg-blue-50 border-l-4 border-blue-500 p-4 mb-6 rounded-r">
|
|
90
|
-
<p class="text-blue-900"><strong>Configuration:</strong> Enable this by adding the <code>wso2</code> block to your <code>config.yaml</code>.</p>
|
|
95
|
+
<p class="text-blue-900"><strong>WSO2 Configuration:</strong> Enable this by adding the <code>wso2</code> block to your <code>config.yaml</code>.</p>
|
|
91
96
|
</div>
|
|
92
97
|
|
|
93
98
|
<pre><code class="language-yaml">go-duck:
|
|
@@ -5,6 +5,7 @@ import (
|
|
|
5
5
|
"fmt"
|
|
6
6
|
"log"
|
|
7
7
|
"net/http"
|
|
8
|
+
"strings"
|
|
8
9
|
"time"
|
|
9
10
|
|
|
10
11
|
"github.com/gin-gonic/gin"
|
|
@@ -162,41 +163,238 @@ func SetupRouter(appConfig *config.Config) *gin.Engine {
|
|
|
162
163
|
})
|
|
163
164
|
|
|
164
165
|
// Swagger Docs & UI
|
|
165
|
-
|
|
166
|
+
// Expose standard OpenAPI endpoints (JHipster / Spring / WSO2 compatibility)
|
|
167
|
+
r.StaticFile("/v3/api-docs", "./docs/swagger.json")
|
|
168
|
+
r.StaticFile("/swagger.json", "./docs/swagger.json") // Legacy fallback
|
|
169
|
+
r.StaticFile("/logo.png", "./docs/web/logo.png")
|
|
166
170
|
r.GET("/swagger", func(c *gin.Context) {
|
|
167
|
-
|
|
171
|
+
swaggerHTML := `<!DOCTYPE html>
|
|
168
172
|
<html lang="en">
|
|
169
173
|
<head>
|
|
170
174
|
<meta charset="UTF-8">
|
|
171
|
-
<title>Swagger UI</title>
|
|
175
|
+
<title>Swagger UI (Secured)</title>
|
|
172
176
|
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui.css" />
|
|
177
|
+
<script src="https://cdn.jsdelivr.net/npm/keycloak-js@24.0.4/dist/keycloak.min.js"></script>
|
|
173
178
|
<style>
|
|
174
179
|
html { box-sizing: border-box; overflow: -moz-scrollbars-vertical; overflow-y: scroll; }
|
|
175
180
|
*, *:before, *:after { box-sizing: inherit; }
|
|
176
|
-
body { margin:0; background: #fafafa; }
|
|
181
|
+
body { margin:0; background: #fafafa; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
|
|
182
|
+
|
|
183
|
+
.top-nav {
|
|
184
|
+
background: rgba(255, 255, 255, 0.8);
|
|
185
|
+
backdrop-filter: blur(10px);
|
|
186
|
+
-webkit-backdrop-filter: blur(10px);
|
|
187
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
|
188
|
+
padding: 12px 24px;
|
|
189
|
+
display: flex;
|
|
190
|
+
justify-content: space-between;
|
|
191
|
+
align-items: center;
|
|
192
|
+
position: sticky;
|
|
193
|
+
top: 0;
|
|
194
|
+
z-index: 1000;
|
|
195
|
+
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.nav-brand {
|
|
199
|
+
font-weight: 700;
|
|
200
|
+
font-size: 1.2rem;
|
|
201
|
+
color: #333;
|
|
202
|
+
display: flex;
|
|
203
|
+
align-items: center;
|
|
204
|
+
gap: 10px;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.nav-controls {
|
|
208
|
+
display: flex;
|
|
209
|
+
align-items: center;
|
|
210
|
+
gap: 16px;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.tenant-input {
|
|
214
|
+
padding: 8px 12px;
|
|
215
|
+
border: 1px solid #ddd;
|
|
216
|
+
border-radius: 6px;
|
|
217
|
+
font-size: 0.9rem;
|
|
218
|
+
outline: none;
|
|
219
|
+
transition: border-color 0.2s;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.tenant-input:focus {
|
|
223
|
+
border-color: #4a90e2;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.btn {
|
|
227
|
+
padding: 8px 16px;
|
|
228
|
+
border-radius: 6px;
|
|
229
|
+
border: none;
|
|
230
|
+
font-weight: 600;
|
|
231
|
+
cursor: pointer;
|
|
232
|
+
transition: all 0.2s;
|
|
233
|
+
font-size: 0.9rem;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.btn-login {
|
|
237
|
+
background: #4a90e2;
|
|
238
|
+
color: white;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.btn-login:hover {
|
|
242
|
+
background: #357abd;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.btn-logout {
|
|
246
|
+
background: #e74c3c;
|
|
247
|
+
color: white;
|
|
248
|
+
display: none;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.btn-logout:hover {
|
|
252
|
+
background: #c0392b;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.status-indicator {
|
|
256
|
+
display: flex;
|
|
257
|
+
align-items: center;
|
|
258
|
+
gap: 6px;
|
|
259
|
+
font-size: 0.85rem;
|
|
260
|
+
color: #666;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.dot {
|
|
264
|
+
width: 8px;
|
|
265
|
+
height: 8px;
|
|
266
|
+
border-radius: 50%;
|
|
267
|
+
background: #ccc;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.dot.active {
|
|
271
|
+
background: #2ecc71;
|
|
272
|
+
box-shadow: 0 0 8px #2ecc71;
|
|
273
|
+
}
|
|
177
274
|
</style>
|
|
178
275
|
</head>
|
|
179
276
|
<body>
|
|
277
|
+
<div class="top-nav">
|
|
278
|
+
<div class="nav-brand">
|
|
279
|
+
<img src="/logo.png" alt="GO-DUCK Logo" style="height: 32px; width: auto;" />
|
|
280
|
+
<div style="display: flex; flex-direction: column; line-height: 1.2;">
|
|
281
|
+
<span>{{app_name}} API Explorer</span>
|
|
282
|
+
<span style="font-size: 0.65em; font-weight: normal; color: #94a3b8;">
|
|
283
|
+
GO-DUCK v{{cli_version}} • Generated: {{generated_date}}
|
|
284
|
+
</span>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
<div class="nav-controls">
|
|
288
|
+
<div class="status-indicator">
|
|
289
|
+
<div class="dot" id="auth-dot"></div>
|
|
290
|
+
<span id="auth-status">Unauthenticated</span>
|
|
291
|
+
</div>
|
|
292
|
+
<input type="text" id="tenant-input" class="tenant-input" placeholder="X-Tenant-ID" value="tenant_1" title="Multi-tenant DB Target" />
|
|
293
|
+
<button id="login-btn" class="btn btn-login">Login with Keycloak</button>
|
|
294
|
+
<button id="logout-btn" class="btn btn-logout">Logout</button>
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
|
|
180
298
|
<div id="swagger-ui"></div>
|
|
299
|
+
|
|
181
300
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui-bundle.js"></script>
|
|
182
301
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui-standalone-preset.js"></script>
|
|
183
302
|
<script>
|
|
303
|
+
const KEYCLOAK_URL = 'KEYCLOAK_URL_PLACEHOLDER';
|
|
304
|
+
const KEYCLOAK_REALM = 'KEYCLOAK_REALM_PLACEHOLDER';
|
|
305
|
+
const KEYCLOAK_CLIENT = 'KEYCLOAK_CLIENT_PLACEHOLDER';
|
|
306
|
+
|
|
307
|
+
let keycloak = null;
|
|
308
|
+
|
|
309
|
+
function updateUI(authenticated) {
|
|
310
|
+
if (authenticated) {
|
|
311
|
+
document.getElementById('login-btn').style.display = 'none';
|
|
312
|
+
document.getElementById('logout-btn').style.display = 'block';
|
|
313
|
+
document.getElementById('auth-dot').classList.add('active');
|
|
314
|
+
document.getElementById('auth-status').innerText = 'Authenticated (' + (keycloak.tokenParsed?.preferred_username || 'User') + ')';
|
|
315
|
+
} else {
|
|
316
|
+
document.getElementById('login-btn').style.display = 'block';
|
|
317
|
+
document.getElementById('logout-btn').style.display = 'none';
|
|
318
|
+
document.getElementById('auth-dot').classList.remove('active');
|
|
319
|
+
document.getElementById('auth-status').innerText = 'Unauthenticated';
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
184
323
|
window.onload = function() {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
324
|
+
// Initialize Keycloak
|
|
325
|
+
keycloak = new Keycloak({
|
|
326
|
+
url: KEYCLOAK_URL,
|
|
327
|
+
realm: KEYCLOAK_REALM,
|
|
328
|
+
clientId: KEYCLOAK_CLIENT
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
keycloak.init({ onLoad: 'check-sso', checkLoginIframe: false }).then(authenticated => {
|
|
332
|
+
updateUI(authenticated);
|
|
333
|
+
|
|
334
|
+
document.getElementById('login-btn').onclick = () => keycloak.login();
|
|
335
|
+
document.getElementById('logout-btn').onclick = () => keycloak.logout();
|
|
336
|
+
|
|
337
|
+
if (authenticated) {
|
|
338
|
+
// Auto-refresh token logic
|
|
339
|
+
setInterval(() => {
|
|
340
|
+
keycloak.updateToken(70).then(refreshed => {
|
|
341
|
+
if (refreshed) {
|
|
342
|
+
console.log('Token refreshed automatically');
|
|
343
|
+
}
|
|
344
|
+
}).catch(() => {
|
|
345
|
+
console.error('Failed to refresh token');
|
|
346
|
+
updateUI(false);
|
|
347
|
+
});
|
|
348
|
+
}, 60000); // Check every minute
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 4. Initialize Swagger UI
|
|
352
|
+
const ui = SwaggerUIBundle({
|
|
353
|
+
url: "/v3/api-docs",
|
|
354
|
+
dom_id: '#swagger-ui',
|
|
355
|
+
deepLinking: true,
|
|
356
|
+
presets: [
|
|
357
|
+
SwaggerUIBundle.presets.apis,
|
|
358
|
+
SwaggerUIStandalonePreset
|
|
359
|
+
],
|
|
360
|
+
layout: "StandaloneLayout",
|
|
361
|
+
requestInterceptor: (req) => {
|
|
362
|
+
if (keycloak && keycloak.token) {
|
|
363
|
+
req.headers['Authorization'] = 'Bearer ' + keycloak.token;
|
|
364
|
+
}
|
|
365
|
+
const tenantId = document.getElementById('tenant-input').value;
|
|
366
|
+
if (tenantId) {
|
|
367
|
+
req.headers['X-Tenant-ID'] = tenantId;
|
|
368
|
+
}
|
|
369
|
+
return req;
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
}).catch(err => {
|
|
373
|
+
console.error("Keycloak initialization failed", err);
|
|
374
|
+
document.getElementById('auth-status').innerText = 'Keycloak init failed';
|
|
375
|
+
|
|
376
|
+
// Still load swagger without auth interceptor
|
|
377
|
+
window.ui = SwaggerUIBundle({
|
|
378
|
+
url: "/v3/api-docs",
|
|
379
|
+
dom_id: '#swagger-ui',
|
|
380
|
+
deepLinking: true,
|
|
381
|
+
presets: [
|
|
382
|
+
SwaggerUIBundle.presets.apis,
|
|
383
|
+
SwaggerUIStandalonePreset
|
|
384
|
+
],
|
|
385
|
+
layout: "StandaloneLayout"
|
|
386
|
+
});
|
|
194
387
|
});
|
|
195
|
-
window.ui = ui;
|
|
196
388
|
};
|
|
197
389
|
</script>
|
|
198
390
|
</body>
|
|
199
|
-
</html>`
|
|
391
|
+
</html>`
|
|
392
|
+
|
|
393
|
+
htmlStr := strings.ReplaceAll(swaggerHTML, "KEYCLOAK_URL_PLACEHOLDER", appConfig.GoDuck.Security.KeycloakHost)
|
|
394
|
+
htmlStr = strings.ReplaceAll(htmlStr, "KEYCLOAK_REALM_PLACEHOLDER", appConfig.GoDuck.Security.KeycloakRealm)
|
|
395
|
+
htmlStr = strings.ReplaceAll(htmlStr, "KEYCLOAK_CLIENT_PLACEHOLDER", appConfig.GoDuck.Security.KeycloakAppClientID)
|
|
396
|
+
|
|
397
|
+
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(htmlStr))
|
|
200
398
|
})
|
|
201
399
|
|
|
202
400
|
// Management APIs (Run-time DB onboarding)
|