Skip to content

Commit 4bd2682

Browse files
Merge pull request #114 from ustaxs/swagger
Add OpenAPI (Swagger) generation, API Explorer
2 parents b5174c4 + 0af6aac commit 4bd2682

11 files changed

Lines changed: 317 additions & 4 deletions

File tree

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ NODE_ENV=development
33
PORT=3000
44
API_PREFIX=/api/v1
55

6+
# OpenAPI / Swagger
7+
# Swagger UI (/api/docs) and JSON export are always active in development.
8+
# In production they are disabled by default — set to "true" to opt in.
9+
SWAGGER_ENABLED=false
10+
611
# Database
712
DATABASE_URL=postgresql://user:password@localhost:5432/alianStructure.db
813

.github/workflows/build-check.yml

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,60 @@ jobs:
2424
run: npx tsc --noEmit
2525

2626
- name: Build project
27-
run: npm run build
27+
run: npm run build
28+
29+
openapi:
30+
name: Generate OpenAPI Spec & TypeScript Client
31+
runs-on: ubuntu-latest
32+
# Only run on pushes to main/master (not PRs) to avoid redundant artifact uploads.
33+
if: github.event_name == 'push'
34+
needs: build
35+
env:
36+
NODE_ENV: development
37+
# Provide minimal env vars so the app can bootstrap without a real DB.
38+
# The export script boots with abortOnError:false so it tolerates DB absence.
39+
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/openapi_gen
40+
JWT_SECRET: ci-openapi-export-secret
41+
PORT: 3001
42+
steps:
43+
- uses: actions/checkout@v4
44+
45+
- name: Set up Node.js
46+
uses: actions/setup-node@v4
47+
with:
48+
node-version: '20'
49+
cache: 'npm'
50+
51+
- name: Install dependencies
52+
run: npm ci
53+
54+
- name: Export OpenAPI JSON
55+
run: npm run openapi:export
56+
57+
- name: Set up Java (required by OpenAPI Generator CLI)
58+
uses: actions/setup-java@v4
59+
with:
60+
distribution: temurin
61+
java-version: '17'
62+
63+
- name: Generate TypeScript fetch client
64+
run: |
65+
npx @openapitools/openapi-generator-cli@2.13.4 generate \
66+
-i docs/openapi.json \
67+
-g typescript-fetch \
68+
-o client \
69+
--additional-properties=supportsES6=true,typescriptThreePlus=true
70+
71+
- name: Upload OpenAPI JSON artifact
72+
uses: actions/upload-artifact@v4
73+
with:
74+
name: openapi-spec
75+
path: docs/openapi.json
76+
retention-days: 30
77+
78+
- name: Upload TypeScript client artifact
79+
uses: actions/upload-artifact@v4
80+
with:
81+
name: typescript-client
82+
path: client/
83+
retention-days: 30

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,13 @@ dist
151151
# Vite logs files
152152
vite.config.js.timestamp-*
153153
vite.config.ts.timestamp-*
154+
155+
# Generated OpenAPI artifacts — produced by `npm run openapi:export`
156+
docs/openapi.json
157+
158+
# Generated TypeScript client — produced by `npm run openapi:client`
159+
client/
160+
161+
# OpenAPI Generator CLI cache
162+
.openapi-generator/
163+
openapitools.json

package-lock.json

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@
3434
"seed:audit": "ts-node src/seeds/seed-audit-data.ts",
3535
"docs:generate": "nest start --watch",
3636
"docs:serve": "nest start",
37-
"docs:build": "nest build && node dist/main.js"
37+
"docs:build": "nest build && node dist/main.js",
38+
"openapi:export": "cross-env NODE_ENV=development ts-node -r tsconfig-paths/register scripts/export-openapi.ts",
39+
"openapi:client": "npm run openapi:export && npx @openapitools/openapi-generator-cli generate -i docs/openapi.json -g typescript-fetch -o client --additional-properties=supportsES6=true,typescriptThreePlus=true"
3840
},
3941
"dependencies": {
4042
"@nestjs/axios": "^4.0.1",
@@ -127,6 +129,7 @@
127129
"eslint-plugin-import": "^2.32.0",
128130
"eslint-plugin-prettier": "^5.1.2",
129131
"jest": "^29.7.0",
132+
"cross-env": "^7.0.3",
130133
"nodemon": "^3.1.11",
131134
"pg": "^8.22.0",
132135
"pino-pretty": "^13.1.3",

scripts/export-openapi.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* Standalone script that boots the NestJS app just long enough to generate
3+
* and write the OpenAPI JSON document to docs/openapi.json, then exits.
4+
*
5+
* Usage:
6+
* npx ts-node -r tsconfig-paths/register scripts/export-openapi.ts
7+
* npm run openapi:export
8+
*/
9+
10+
import { NestFactory } from "@nestjs/core";
11+
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
12+
import { writeFileSync, mkdirSync } from "fs";
13+
import { join } from "path";
14+
import { AppModule } from "../src/app.module";
15+
16+
async function exportOpenApi() {
17+
// Silence NestJS bootstrap logs — we only want the artefact output
18+
const app = await NestFactory.create(AppModule, { logger: false, abortOnError: false });
19+
20+
const config = new DocumentBuilder()
21+
.setTitle("alian-structure Backend API")
22+
.setDescription(
23+
"Comprehensive API documentation for alian-structure backend services including " +
24+
"agent management, oracle submissions, compute operations, and audit trails.",
25+
)
26+
.setVersion("1.0.0")
27+
.setContact("alian-structure Team", "https://alian-structure.com", "api@alian-structure.com")
28+
.setLicense("Apache 2.0", "https://www.apache.org/licenses/LICENSE-2.0")
29+
.addServer("http://localhost:3001", "Development Server")
30+
.addServer("https://api.alian-structure.com", "Production Server")
31+
.addBearerAuth(
32+
{ type: "http", scheme: "bearer", bearerFormat: "JWT", name: "JWT", description: "Enter JWT token", in: "header" },
33+
"JWT-auth",
34+
)
35+
.addApiKey(
36+
{ type: "apiKey", name: "X-API-Key", in: "header", description: "API key for service-to-service communication" },
37+
"api-key",
38+
)
39+
.addTag("Health", "Liveness, readiness, and startup probes for Kubernetes orchestration")
40+
.addTag("Authentication", "User authentication and authorization")
41+
.addTag("Enhanced Authentication & KYC", "Enhanced auth with 2FA and KYC flows")
42+
.addTag("Users", "User management operations")
43+
.addTag("Oracle", "Oracle data submissions and payload management")
44+
.addTag("Price Feed", "Aggregated on-chain price data")
45+
.addTag("Audit", "Audit trail and logging")
46+
.addTag("Profile", "User profile management")
47+
.addTag("Info", "API health and meta-information")
48+
.build();
49+
50+
const document = SwaggerModule.createDocument(app, config, {
51+
deepScanRoutes: true,
52+
operationIdFactory: (_controllerKey: string, methodKey: string) => methodKey,
53+
});
54+
55+
// Write JSON
56+
const outDir = join(__dirname, "..", "docs");
57+
mkdirSync(outDir, { recursive: true });
58+
59+
const jsonPath = join(outDir, "openapi.json");
60+
writeFileSync(jsonPath, JSON.stringify(document, null, 2), "utf8");
61+
console.log(`✅ OpenAPI JSON written to ${jsonPath}`);
62+
63+
await app.close();
64+
process.exit(0);
65+
}
66+
67+
exportOpenApi().catch((err) => {
68+
console.error("Failed to export OpenAPI spec:", err);
69+
process.exit(1);
70+
});

src/blockchain/oracle/dto/payload-response.dto.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,65 @@
1+
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
12
import { PayloadStatus, PayloadType } from "../entities/signed-payload.entity";
23

34
/**
45
* Response DTO for payload operations
56
*/
67
export class PayloadResponseDto {
8+
@ApiProperty({ description: "Unique payload UUID", example: "a1b2c3d4-1234-5678-90ef-ghijklmnopqr" })
79
id: string;
10+
11+
@ApiProperty({ description: "Type of payload", enum: PayloadType, example: PayloadType.PRICE_FEED })
812
payloadType: PayloadType;
13+
14+
@ApiProperty({ description: "Ethereum address that signed this payload", example: "0xAbCd1234567890abcdef1234567890abcdef1234" })
915
signerAddress: string;
16+
17+
@ApiProperty({ description: "Submission nonce", example: "42" })
1018
nonce: string;
19+
20+
@ApiProperty({ description: "Raw payload data", type: "object", example: { token: "ETH", price: 3200.5 } })
1121
payload: Record<string, any>;
22+
23+
@ApiProperty({ description: "Keccak256 hash of the payload", example: "0xabc123..." })
1224
payloadHash: string;
25+
26+
@ApiProperty({ description: "EIP-712 structured data hash", example: "0xdef456..." })
1327
structuredDataHash: string;
28+
29+
@ApiPropertyOptional({ description: "ECDSA signature (0x-prefixed, 132 chars)", nullable: true, example: "0x..." })
1430
signature: string | null;
31+
32+
@ApiProperty({ description: "Payload expiry timestamp" })
1533
expiresAt: Date;
34+
35+
@ApiProperty({ description: "Current submission status", enum: PayloadStatus, example: PayloadStatus.PENDING })
1636
status: PayloadStatus;
37+
38+
@ApiPropertyOptional({ description: "On-chain transaction hash after submission", nullable: true, example: "0x..." })
1739
transactionHash: string | null;
40+
41+
@ApiPropertyOptional({ description: "Block number when confirmed on-chain", nullable: true, example: "18500000" })
1842
blockNumber: string | null;
43+
44+
@ApiProperty({ description: "Total number of submission attempts", example: 1 })
1945
submissionAttempts: number;
46+
47+
@ApiPropertyOptional({ description: "Error message if submission failed", nullable: true })
2048
errorMessage: string | null;
49+
50+
@ApiPropertyOptional({ description: "Optional metadata", nullable: true, type: "object" })
2151
metadata: Record<string, any> | null;
52+
53+
@ApiProperty({ description: "Record creation timestamp" })
2254
createdAt: Date;
55+
56+
@ApiProperty({ description: "Record last-updated timestamp" })
2357
updatedAt: Date;
58+
59+
@ApiPropertyOptional({ description: "When submitted to blockchain", nullable: true })
2460
submittedAt: Date | null;
61+
62+
@ApiPropertyOptional({ description: "When confirmed on-chain", nullable: true })
2563
confirmedAt: Date | null;
2664
}
2765

src/blockchain/oracle/dto/sign-payload.dto.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
import { IsString, IsNotEmpty, Matches } from "class-validator";
2+
import { ApiProperty } from "@nestjs/swagger";
23

34
/**
45
* DTO for signing a payload with a private key
56
*/
67
export class SignPayloadDto {
8+
@ApiProperty({
9+
description: "UUID of the payload to sign",
10+
example: "a1b2c3d4-1234-5678-90ef-ghijklmnopqr",
11+
})
712
@IsString()
813
@IsNotEmpty()
914
payloadId: string;
1015

16+
@ApiProperty({
17+
description: "Ethereum private key (0x-prefixed, 64 hex chars). NOTE: use client-side signing in production.",
18+
example: "0x4c0883a69102937d6231471b5dbb6e538eba2ef68e5fd63f36fe1ef7e9bb4d7f",
19+
pattern: "^0x[a-fA-F0-9]{64}$",
20+
})
1121
@IsString()
1222
@IsNotEmpty()
1323
@Matches(/^0x[a-fA-F0-9]{64}$/, {

src/blockchain/oracle/dto/verify-signature.dto.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,36 @@
11
import { IsString, IsNotEmpty, IsObject, Matches } from "class-validator";
2+
import { ApiProperty } from "@nestjs/swagger";
23

34
/**
45
* DTO for verifying a signature off-chain
56
*/
67
export class VerifySignatureDto {
8+
@ApiProperty({
9+
description: "Payload data that was originally signed",
10+
type: "object",
11+
example: { token: "ETH", price: 3200.5, timestamp: 1620000000000 },
12+
})
713
@IsObject()
814
@IsNotEmpty()
915
payload: Record<string, any>;
1016

17+
@ApiProperty({
18+
description: "ECDSA signature (0x-prefixed, 132 chars)",
19+
example: "0x1234567890abcdef....",
20+
pattern: "^0x[a-fA-F0-9]{130}$",
21+
})
1122
@IsString()
1223
@IsNotEmpty()
1324
@Matches(/^0x[a-fA-F0-9]{130}$/, {
1425
message: "Signature must be a valid hex string with 0x prefix (132 chars)",
1526
})
1627
signature: string;
1728

29+
@ApiProperty({
30+
description: "Expected signer Ethereum address (0x-prefixed, 40 hex chars)",
31+
example: "0xAbCd1234567890abcdef1234567890abcdef1234",
32+
pattern: "^0x[a-fA-F0-9]{40}$",
33+
})
1834
@IsString()
1935
@IsNotEmpty()
2036
@Matches(/^0x[a-fA-F0-9]{40}$/, {

0 commit comments

Comments
 (0)