From 44620614b64d962eae760b912ea065f86db423aa Mon Sep 17 00:00:00 2001 From: agent-company Date: Fri, 27 Mar 2026 20:09:11 +0000 Subject: [PATCH] feat: generate TypeScript API client from OpenAPI spec and add CI freshness check Closes leeworks-agents/SPARC#426 - Generate schema.d.ts from committed openapi.json using openapi-typescript - Rewrite types/index.ts to derive all application types from the generated schema - Add CI step in both build.yaml and test.yaml to verify schema.d.ts stays in sync - TypeScript compilation passes with zero errors Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitea/workflows/build.yaml | 6 + .gitea/workflows/test.yaml | 10 + frontend/src/api/schema.d.ts | 975 +++++++++++++++++++++++++++++++++++ frontend/src/types/index.ts | 70 +-- 4 files changed, 1019 insertions(+), 42 deletions(-) create mode 100644 frontend/src/api/schema.d.ts diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index 7e42585..beb2354 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -39,6 +39,12 @@ jobs: apk add --no-cache nodejs npm cd frontend npm ci + npm run generate:local + if ! git diff --quiet src/api/schema.d.ts; then + echo "ERROR: src/api/schema.d.ts is out of date. Run 'npm run generate:local' and commit the result." + git diff src/api/schema.d.ts + exit 1 + fi npx tsc --noEmit - name: Run pytest diff --git a/.gitea/workflows/test.yaml b/.gitea/workflows/test.yaml index 49db9b9..71173d3 100644 --- a/.gitea/workflows/test.yaml +++ b/.gitea/workflows/test.yaml @@ -40,6 +40,16 @@ jobs: apk add --no-cache nodejs npm cd frontend && npm ci + - name: Verify generated API types are up to date + shell: sh + run: | + cd frontend && npm run generate:local + if ! git diff --quiet src/api/schema.d.ts; then + echo "ERROR: src/api/schema.d.ts is out of date. Run 'npm run generate:local' and commit the result." + git diff src/api/schema.d.ts + exit 1 + fi + - name: Run TypeScript type check shell: sh run: | diff --git a/frontend/src/api/schema.d.ts b/frontend/src/api/schema.d.ts new file mode 100644 index 0000000..0c4772e --- /dev/null +++ b/frontend/src/api/schema.d.ts @@ -0,0 +1,975 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/auth/register": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Register + * @description Register a new user. + * + * The first registered user automatically becomes an admin. + */ + post: operations["register_auth_register_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/login": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Login + * @description Authenticate user and return JWT tokens. + */ + post: operations["login_auth_login_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/refresh": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Refresh Token + * @description Refresh access token using refresh token. + */ + post: operations["refresh_token_auth_refresh_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Me + * @description Get current authenticated user. + */ + get: operations["get_me_auth_me_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/admin/users": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Users + * @description List all users (admin only). + */ + get: operations["list_users_admin_users_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/admin/users/{user_id}/role": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * Update User Role + * @description Update a user's role (admin only). + */ + patch: operations["update_user_role_admin_users__user_id__role_patch"]; + trace?: never; + }; + "/admin/users/{user_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Delete User + * @description Delete a user (admin only). + */ + delete: operations["delete_user_admin_users__user_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/analytics": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Analytics + * @description Get analytics data (authenticated users only). + */ + get: operations["get_analytics_analytics_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/health": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Health Check + * @description Check API health status. + */ + get: operations["health_check_health_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/analyze/{company_name}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Analyze Company + * @description Analyze a single company's patent portfolio. + * + * This endpoint retrieves recent patents for the specified company, + * parses them, and uses AI to generate a comprehensive analysis. + * + * Args: + * company_name: Name of the company to analyze (e.g., "nvidia", "intel") + * + * Returns: + * Analysis results including patent count, AI insights, and success status + */ + get: operations["analyze_company_analyze__company_name__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/analyze/batch": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Analyze Companies Batch + * @description Analyze multiple companies' patent portfolios. + * + * Processes companies concurrently for improved performance. + * Limited to 20 companies per request. + * + * Args: + * request: List of company names and optional worker count + * + * Returns: + * Batch results with individual company analyses and summary statistics + */ + post: operations["analyze_companies_batch_analyze_batch_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/analyze/batch/async": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Analyze Companies Async + * @description Start an asynchronous batch analysis job. + * + * Returns immediately with a job ID that can be used to poll for status. + * Useful for large batch analyses that may take a long time. + * + * Args: + * request: List of company names and optional worker count + * + * Returns: + * Job status with job_id for polling + */ + post: operations["analyze_companies_async_analyze_batch_async_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/jobs/{job_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Job Status + * @description Get the status of a background analysis job. + * + * Args: + * job_id: The job ID returned from the async batch endpoint + * + * Returns: + * Current job status including progress and results when complete + */ + get: operations["get_job_status_jobs__job_id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/jobs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Jobs + * @description List all analysis jobs. + * + * Args: + * status: Optional filter by job status + * limit: Maximum number of jobs to return (default 10, max 100) + * + * Returns: + * List of job statuses + */ + get: operations["list_jobs_jobs_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + /** + * AnalyticsResponse + * @description Analytics response model. + */ + AnalyticsResponse: { + /** Total Messages */ + total_messages: number; + /** By Company */ + by_company: { + [key: string]: unknown; + }[]; + /** By Type */ + by_type: { + [key: string]: unknown; + }[]; + /** Period Days */ + period_days: number; + }; + /** + * BatchAnalysisRequest + * @description Request model for batch company analysis. + */ + BatchAnalysisRequest: { + /** + * Companies + * @description List of company names to analyze + */ + companies: string[]; + /** + * Max Workers + * @description Max concurrent analyses + * @default 3 + */ + max_workers: number; + }; + /** + * BatchAnalysisResponse + * @description Response model for batch company analysis. + */ + BatchAnalysisResponse: { + /** Results */ + results: components["schemas"]["CompanyAnalysisResponse"][]; + /** Total Companies */ + total_companies: number; + /** Successful */ + successful: number; + /** Failed */ + failed: number; + /** + * Timestamp + * Format: date-time + */ + timestamp: string; + }; + /** + * CompanyAnalysisResponse + * @description Response model for single company analysis. + */ + CompanyAnalysisResponse: { + /** Company Name */ + company_name: string; + /** Analysis */ + analysis: string; + /** Patent Count */ + patent_count: number; + /** Success */ + success: boolean; + /** Error */ + error?: string | null; + /** + * Timestamp + * Format: date-time + */ + timestamp: string; + }; + /** HTTPValidationError */ + HTTPValidationError: { + /** Detail */ + detail?: components["schemas"]["ValidationError"][]; + }; + /** + * HealthResponse + * @description Health check response. + */ + HealthResponse: { + /** Status */ + status: string; + /** Version */ + version: string; + /** + * Timestamp + * Format: date-time + */ + timestamp: string; + }; + /** + * JobStatus + * @description Status of a background analysis job. + */ + JobStatus: { + /** Job Id */ + job_id: string; + /** Status */ + status: string; + /** Progress */ + progress: number; + /** Total Companies */ + total_companies: number; + /** Completed Companies */ + completed_companies: number; + result?: components["schemas"]["BatchAnalysisResponse"] | null; + /** Error */ + error?: string | null; + }; + /** + * LoginRequest + * @description User login request. + */ + LoginRequest: { + /** + * Email + * Format: email + */ + email: string; + /** Password */ + password: string; + }; + /** + * RefreshRequest + * @description Token refresh request. + */ + RefreshRequest: { + /** Refresh Token */ + refresh_token: string; + }; + /** + * RegisterRequest + * @description User registration request. + */ + RegisterRequest: { + /** + * Email + * Format: email + */ + email: string; + /** + * Password + * @description Password (min 8 characters) + */ + password: string; + }; + /** + * TokenResponse + * @description Token response model. + */ + TokenResponse: { + /** Access Token */ + access_token: string; + /** Refresh Token */ + refresh_token: string; + /** + * Token Type + * @default bearer + */ + token_type: string; + }; + /** + * UpdateRoleRequest + * @description Update user role request. + */ + UpdateRoleRequest: { + /** Role */ + role: string; + }; + /** + * UserResponse + * @description User response model. + */ + UserResponse: { + /** Id */ + id: number; + /** Email */ + email: string; + /** Role */ + role: string; + /** + * Created At + * Format: date-time + */ + created_at: string; + }; + /** ValidationError */ + ValidationError: { + /** Location */ + loc: (string | number)[]; + /** Message */ + msg: string; + /** Error Type */ + type: string; + /** Input */ + input?: unknown; + /** Context */ + ctx?: Record; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + register_auth_register_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["RegisterRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + login_auth_login_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["LoginRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TokenResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + refresh_token_auth_refresh_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["RefreshRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TokenResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_me_auth_me_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserResponse"]; + }; + }; + }; + }; + list_users_admin_users_get: { + parameters: { + query?: { + limit?: number; + offset?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserResponse"][]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + update_user_role_admin_users__user_id__role_patch: { + parameters: { + query?: never; + header?: never; + path: { + user_id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateRoleRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_user_admin_users__user_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + user_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_analytics_analytics_get: { + parameters: { + query?: { + days?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AnalyticsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + health_check_health_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HealthResponse"]; + }; + }; + }; + }; + analyze_company_analyze__company_name__get: { + parameters: { + query?: never; + header?: never; + path: { + company_name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CompanyAnalysisResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + analyze_companies_batch_analyze_batch_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BatchAnalysisRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BatchAnalysisResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + analyze_companies_async_analyze_batch_async_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BatchAnalysisRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["JobStatus"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_job_status_jobs__job_id__get: { + parameters: { + query?: never; + header?: never; + path: { + job_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["JobStatus"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_jobs_jobs_get: { + parameters: { + query?: { + /** @description Filter by status: pending, running, completed, failed */ + status?: string | null; + limit?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["JobStatus"][]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; +} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 01df9b1..000f263 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -1,46 +1,32 @@ -export interface User { - id: number; - email: string; - role: 'admin' | 'user'; - created_at: string; -} +/** + * Application types derived from the auto-generated OpenAPI schema. + * + * Run `npm run generate:local` (or `npm run generate` with the API running) + * to regenerate `src/api/schema.d.ts` from the backend OpenAPI spec. + * + * These aliases keep the rest of the codebase stable while the source of + * truth lives in the generated file. + */ -export interface TokenResponse { - access_token: string; - refresh_token: string; - token_type: string; -} +import type { components } from '../api/schema'; -export interface CompanyAnalysis { - company_name: string; - analysis: string; - patent_count: number; - success: boolean; - error: string | null; - timestamp: string; -} - -export interface BatchAnalysisResult { - results: CompanyAnalysis[]; - total_companies: number; - successful: number; - failed: number; - timestamp: string; -} - -export interface JobStatus { - job_id: string; - status: 'pending' | 'running' | 'completed' | 'failed'; - progress: number; - total_companies: number; - completed_companies: number; - result: BatchAnalysisResult | null; - error: string | null; -} - -export interface Analytics { - total_messages: number; +// Re-export schema types under the names the rest of the app expects. +export type User = components['schemas']['UserResponse']; +export type TokenResponse = components['schemas']['TokenResponse']; +export type CompanyAnalysis = components['schemas']['CompanyAnalysisResponse']; +export type BatchAnalysisResult = components['schemas']['BatchAnalysisResponse']; +export type JobStatus = components['schemas']['JobStatus']; +export type Analytics = Omit & { by_company: Array<{ company_name: string; count: number }>; by_type: Array<{ analysis_type: string; count: number }>; - period_days: number; -} +}; + +// Additional generated types that may be useful elsewhere. +export type RegisterRequest = components['schemas']['RegisterRequest']; +export type LoginRequest = components['schemas']['LoginRequest']; +export type RefreshRequest = components['schemas']['RefreshRequest']; +export type UpdateRoleRequest = components['schemas']['UpdateRoleRequest']; +export type HealthResponse = components['schemas']['HealthResponse']; +export type BatchAnalysisRequest = components['schemas']['BatchAnalysisRequest']; +export type ValidationError = components['schemas']['ValidationError']; +export type HTTPValidationError = components['schemas']['HTTPValidationError'];