From 8e4433ceaa4d226bbe94fc8dd1cb7d3571838691 Mon Sep 17 00:00:00 2001 From: Lucien Cartier-Tilet Date: Thu, 1 Jan 2026 23:29:31 +0100 Subject: [PATCH] feat(api): generate TypeScript API client from OpenAPI specification Create type-safe TypeScript API client automatically generated from the OpenAPI specification. Includes generated schema types and documented client wrapper for type-safe backend communication. Ref: T008 (specs/001-modbus-relay-control) --- README.md | 93 +++++++++++------- package.json | 5 +- pnpm-lock.yaml | 240 ++++++++++++++++++++++++++++++++++++++++++++++ src/api/README.md | 61 ++++++++++++ src/api/client.ts | 31 ++++++ src/api/schema.ts | 106 ++++++++++++++++++++ 6 files changed, 498 insertions(+), 38 deletions(-) create mode 100644 src/api/README.md create mode 100644 src/api/client.ts create mode 100644 src/api/schema.ts diff --git a/README.md b/README.md index a9d97b9..8986d36 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # STA - Smart Temperature & Appliance Control +> **🤖 AI-Assisted Development Notice**: This project uses Claude Code as a development assistant for task planning, code organization, and workflow management. However, all code is human-written, reviewed, and validated by the project maintainer. AI is used as a productivity tool, not as the author of the implementation. + Web-based Modbus relay control system for managing 8-channel relay modules over TCP. > **⚠️ Development Status**: This project is in early development. Core features are currently being implemented following a specification-driven approach. @@ -10,26 +12,33 @@ STA will provide a modern web interface for controlling Modbus-compatible relay ## Current Status -**Implemented:** -- ✅ Basic Rust web server with Poem framework +**Phase 1 Complete - Foundation:** +- ✅ Monorepo structure (backend + frontend at root) +- ✅ Rust web server with Poem 3.1 framework - ✅ Configuration system (YAML + environment variables) -- ✅ Modbus TCP settings structure +- ✅ Modbus TCP and relay settings structures - ✅ Health check and metadata API endpoints - ✅ OpenAPI documentation with Swagger UI - ✅ Rate limiting middleware +- ✅ SQLite schema and repository for relay labels +- ✅ Vue 3 + TypeScript frontend scaffolding with Vite +- ✅ Type-safe API client generation from OpenAPI specs -**In Progress:** -- 🚧 Domain model for relay control (Phase 2) -- 🚧 Modbus TCP client implementation (Phase 3) +**Phase 2 In Progress - Domain Layer:** +- 🚧 Domain types with Type-Driven Development (RelayId, RelayState, RelayLabel) +- 🚧 100% test coverage for domain layer -**Planned Features:** -- 📋 8-Channel Relay Control: Individual and bulk relay control (on/off/toggle) -- 📋 Real-Time Monitoring: Live relay state updates via HTTP polling -- 📋 Custom Labels: Name your relays for easy identification -- 📋 Health Monitoring: Connection status and device health tracking -- 📋 Vue 3 + TypeScript frontend +**Planned - Phases 3-8:** +- 📋 Modbus TCP client with tokio-modbus (Phase 3) +- 📋 Mock controller for testing (Phase 3) +- 📋 Health monitoring service (Phase 3) +- 📋 US1: Monitor & toggle relay states - MVP (Phase 4) +- 📋 US2: Bulk relay controls (Phase 5) +- 📋 US3: Health status display (Phase 6) +- 📋 US4: Relay labeling (Phase 7) +- 📋 Production deployment (Phase 8) -See [tasks.md](specs/001-modbus-relay-control/tasks.md) for detailed implementation roadmap. +See [tasks.md](specs/001-modbus-relay-control/tasks.md) for detailed implementation roadmap (94 tasks across 8 phases). ## Architecture @@ -111,31 +120,41 @@ The server provides OpenAPI documentation via Swagger UI: ## Project Structure -**Current:** +**Monorepo Layout:** ``` -src/ -├── lib.rs - Library entry point -├── main.rs - Binary entry point -├── startup.rs - Application builder and server configuration -├── settings.rs - Configuration management -├── telemetry.rs - Logging and tracing setup -├── route/ - HTTP endpoint handlers -│ ├── health.rs - Health check endpoints -│ └── meta.rs - Application metadata -└── middleware/ - Custom middleware - └── rate_limit.rs - -specs/ - Feature specifications and documentation -settings/ - YAML configuration files -``` - -**Planned (Hexagonal Architecture):** -``` -src/ -├── domain/ - Business logic and domain models (Phase 2) -├── application/ - Use cases and orchestration (Phase 3-4) -├── infrastructure/ - Modbus, persistence, external services (Phase 3) -└── presentation/ - API endpoints and DTOs (Phase 4) +sta/ # Repository root +├── backend/ # Rust backend workspace member +│ ├── src/ +│ │ ├── lib.rs - Library entry point +│ │ ├── main.rs - Binary entry point +│ │ ├── startup.rs - Application builder and server config +│ │ ├── settings.rs - Configuration management +│ │ ├── telemetry.rs - Logging and tracing setup +│ │ ├── domain/ - Business logic (Phase 2 in progress) +│ │ │ └── relay/ - Relay domain types and repository traits +│ │ ├── application/ - Use cases (planned Phase 3-4) +│ │ ├── infrastructure/ - External integrations (Phase 3) +│ │ │ └── persistence/ - SQLite repository implementation +│ │ ├── presentation/ - API layer (planned Phase 4) +│ │ ├── route/ - HTTP endpoint handlers +│ │ │ ├── health.rs - Health check endpoints +│ │ │ └── meta.rs - Application metadata +│ │ └── middleware/ - Custom middleware +│ │ └── rate_limit.rs +│ ├── settings/ - YAML configuration files +│ └── tests/ - Integration tests +├── src/ # Frontend source (Vue/TypeScript) +│ └── api/ - Type-safe API client +├── specs/ # Feature specifications and documentation +│ ├── constitution.md - Architectural principles +│ └── 001-modbus-relay-control/ +│ ├── spec.md - Feature specification +│ ├── plan.md - Implementation plan +│ ├── tasks.md - Task breakdown (94 tasks) +│ └── research-cors.md - CORS configuration research +├── package.json - Frontend dependencies +├── vite.config.ts - Vite build configuration +└── justfile - Build commands ``` ## Technology Stack diff --git a/package.json b/package.json index a488ff8..53de748 100644 --- a/package.json +++ b/package.json @@ -6,15 +6,18 @@ "scripts": { "dev": "vite", "build": "vue-tsc -b && vite build", - "preview": "vite preview" + "preview": "vite preview", + "generate:api": "curl -s http://localhost:3100/specs > openapi.yaml && openapi-typescript openapi.yaml -o src/api/schema.ts && rm openapi.yaml" }, "dependencies": { + "openapi-fetch": "^0.15.0", "vue": "^3.5.24" }, "devDependencies": { "@types/node": "^24.10.1", "@vitejs/plugin-vue": "^6.0.1", "@vue/tsconfig": "^0.8.1", + "openapi-typescript": "^7.10.1", "typescript": "~5.9.3", "vite": "^7.2.4", "vue-tsc": "^3.1.4" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7c62452..723e50e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + openapi-fetch: + specifier: ^0.15.0 + version: 0.15.0 vue: specifier: ^3.5.24 version: 3.5.26(typescript@5.9.3) @@ -21,6 +24,9 @@ importers: '@vue/tsconfig': specifier: ^0.8.1 version: 0.8.1(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)) + openapi-typescript: + specifier: ^7.10.1 + version: 7.10.1(typescript@5.9.3) typescript: specifier: ~5.9.3 version: 5.9.3 @@ -33,6 +39,10 @@ importers: packages: + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} @@ -209,6 +219,16 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@redocly/ajv@8.17.1': + resolution: {integrity: sha512-EDtsGZS964mf9zAUXAl9Ew16eYbeyAFWhsPr0fX6oaJxgd8rApYlPBf0joyhnUHz88WxrigyFtTaqqzXNzPgqw==} + + '@redocly/config@0.22.2': + resolution: {integrity: sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==} + + '@redocly/openapi-core@1.34.6': + resolution: {integrity: sha512-2+O+riuIUgVSuLl3Lyh5AplWZyVMNuG2F98/o6NrutKJfW4/GTZdPpZlIphS0HGgcOHgmWcCSHj+dWFlZaGSHw==} + engines: {node: '>=18.17.0', npm: '>=9.5.0'} + '@rolldown/pluginutils@1.0.0-beta.53': resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} @@ -387,12 +407,44 @@ packages: vue: optional: true + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + alien-signals@3.1.2: resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==} + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + change-case@5.4.4: + resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + + colorette@1.4.0: + resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + entities@7.0.0: resolution: {integrity: sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==} engines: {node: '>=0.12'} @@ -405,6 +457,12 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -419,9 +477,38 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + index-to-position@1.2.0: + resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} + engines: {node: '>=18'} + + js-levenshtein@1.1.6: + resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} + engines: {node: '>=0.10.0'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} @@ -430,6 +517,22 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + openapi-fetch@0.15.0: + resolution: {integrity: sha512-OjQUdi61WO4HYhr9+byCPMj0+bgste/LtSBEcV6FzDdONTs7x0fWn8/ndoYwzqCsKWIxEZwo4FN/TG1c1rI8IQ==} + + openapi-typescript-helpers@0.0.15: + resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==} + + openapi-typescript@7.10.1: + resolution: {integrity: sha512-rBcU8bjKGGZQT4K2ekSTY2Q5veOQbVG/lTKZ49DeCyT9z62hM2Vj/LLHjDHC9W7LJG8YMHcdXpRZDqC1ojB/lw==} + hasBin: true + peerDependencies: + typescript: ^5.x + + parse-json@8.3.0: + resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} + engines: {node: '>=18'} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -440,10 +543,18 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + rollup@4.54.0: resolution: {integrity: sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -453,10 +564,18 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -522,8 +641,21 @@ packages: typescript: optional: true + yaml-ast-parser@0.0.43: + resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + snapshots: + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/helper-string-parser@7.27.1': {} '@babel/helper-validator-identifier@7.28.5': {} @@ -617,6 +749,29 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.5': {} + '@redocly/ajv@8.17.1': + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + '@redocly/config@0.22.2': {} + + '@redocly/openapi-core@1.34.6(supports-color@10.2.2)': + dependencies: + '@redocly/ajv': 8.17.1 + '@redocly/config': 0.22.2 + colorette: 1.4.0 + https-proxy-agent: 7.0.6(supports-color@10.2.2) + js-levenshtein: 1.1.6 + js-yaml: 4.1.1 + minimatch: 5.1.6 + pluralize: 8.0.0 + yaml-ast-parser: 0.0.43 + transitivePeerDependencies: + - supports-color + '@rolldown/pluginutils@1.0.0-beta.53': {} '@rollup/rollup-android-arm-eabi@4.54.0': @@ -778,10 +933,32 @@ snapshots: typescript: 5.9.3 vue: 3.5.26(typescript@5.9.3) + agent-base@7.1.4: {} + alien-signals@3.1.2: {} + ansi-colors@4.1.3: {} + + argparse@2.0.1: {} + + balanced-match@1.0.2: {} + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + change-case@5.4.4: {} + + colorette@1.4.0: {} + csstype@3.2.3: {} + debug@4.4.3(supports-color@10.2.2): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 10.2.2 + entities@7.0.0: {} esbuild@0.27.2: @@ -815,6 +992,10 @@ snapshots: estree-walker@2.0.2: {} + fast-deep-equal@3.1.3: {} + + fast-uri@3.1.0: {} + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -822,26 +1003,77 @@ snapshots: fsevents@2.3.3: optional: true + https-proxy-agent@7.0.6(supports-color@10.2.2): + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + + index-to-position@1.2.0: {} + + js-levenshtein@1.1.6: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + json-schema-traverse@1.0.0: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.2 + + ms@2.1.3: {} + muggle-string@0.4.1: {} nanoid@3.3.11: {} + openapi-fetch@0.15.0: + dependencies: + openapi-typescript-helpers: 0.0.15 + + openapi-typescript-helpers@0.0.15: {} + + openapi-typescript@7.10.1(typescript@5.9.3): + dependencies: + '@redocly/openapi-core': 1.34.6(supports-color@10.2.2) + ansi-colors: 4.1.3 + change-case: 5.4.4 + parse-json: 8.3.0 + supports-color: 10.2.2 + typescript: 5.9.3 + yargs-parser: 21.1.1 + + parse-json@8.3.0: + dependencies: + '@babel/code-frame': 7.27.1 + index-to-position: 1.2.0 + type-fest: 4.41.0 + path-browserify@1.0.1: {} picocolors@1.1.1: {} picomatch@4.0.3: {} + pluralize@8.0.0: {} + postcss@8.5.6: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 + require-from-string@2.0.2: {} + rollup@4.54.0: dependencies: '@types/estree': 1.0.8 @@ -872,11 +1104,15 @@ snapshots: source-map-js@1.2.1: {} + supports-color@10.2.2: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + type-fest@4.41.0: {} + typescript@5.9.3: {} undici-types@7.16.0: {} @@ -910,3 +1146,7 @@ snapshots: '@vue/shared': 3.5.26 optionalDependencies: typescript: 5.9.3 + + yaml-ast-parser@0.0.43: {} + + yargs-parser@21.1.1: {} diff --git a/src/api/README.md b/src/api/README.md new file mode 100644 index 0000000..5516afc --- /dev/null +++ b/src/api/README.md @@ -0,0 +1,61 @@ +# API Client + +This directory contains the auto-generated TypeScript API client for the STA backend. + +## Files + +- `schema.ts` - Auto-generated OpenAPI type definitions (do not edit manually) +- `client.ts` - API client instance with type-safe methods + +## Regenerating the Client + +To regenerate the TypeScript client after backend API changes: + +1. Start the backend server: + ```bash + cargo run + ``` + +2. Download the OpenAPI spec: + ```bash + curl http://localhost:3100/specs > openapi.yaml + ``` + +3. Generate TypeScript types: + ```bash + pnpm exec openapi-typescript openapi.yaml -o src/api/schema.ts + ``` + +## Usage Example + +```typescript +import { apiClient } from '@/api/client'; + +// GET request +const { data, error } = await apiClient.GET('/api/health'); +if (error) { + console.error('Health check failed:', error); +} else { + console.log('Server is healthy'); +} + +// GET request with response data +const { data: meta, error: metaError } = await apiClient.GET('/api/meta'); +if (metaError) { + console.error('Failed to get metadata:', metaError); +} else { + console.log('App name:', meta.name); + console.log('App version:', meta.version); +} +``` + +## Configuration + +The API base URL can be configured via the `VITE_API_BASE_URL` environment variable. +Create a `.env` file in the project root: + +```env +VITE_API_BASE_URL=http://localhost:3100 +``` + +For production builds, set the environment variable to point to your deployed backend. diff --git a/src/api/client.ts b/src/api/client.ts new file mode 100644 index 0000000..d5a0376 --- /dev/null +++ b/src/api/client.ts @@ -0,0 +1,31 @@ +/** + * API client for the STA backend. + * + * This client is generated from the OpenAPI specification and provides + * type-safe access to all backend endpoints. + * + * Usage: + * ```typescript + * import { apiClient } from '@/api/client'; + * + * const { data, error } = await apiClient.GET('/api/health'); + * ``` + */ + +import createClient from 'openapi-fetch'; +import type { paths } from './schema'; + +// Get the API base URL from environment variables or default to localhost +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3100'; + +/** + * Typed API client instance. + * + * All requests are type-checked against the OpenAPI schema. + */ +export const apiClient = createClient({ baseUrl: API_BASE_URL }); + +/** + * Re-export the types for convenience + */ +export type { paths, components } from './schema'; diff --git a/src/api/schema.ts b/src/api/schema.ts new file mode 100644 index 0000000..0102262 --- /dev/null +++ b/src/api/schema.ts @@ -0,0 +1,106 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/api/health": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Success */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Too Many Requests - rate limit exceeded */ + 429: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/meta": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Success */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json; charset=utf-8": components["schemas"]["Meta"]; + }; + }; + /** @description Too Many Requests - rate limit exceeded */ + 429: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + /** Meta */ + Meta: { + version: string; + name: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export type operations = Record;