Updating types
This commit is contained in:
parent
168de8a2b9
commit
f7e41c710c
@ -4,12 +4,12 @@ import { ThemeProvider } from '@mui/material/styles';
|
||||
|
||||
import { backstoryTheme } from './BackstoryTheme';
|
||||
|
||||
import { SeverityType } from './components/Snack';
|
||||
import { Query } from './types/types';
|
||||
import { ConversationHandle } from './components/Conversation';
|
||||
import { UserProvider } from './components/UserContext';
|
||||
import { UserRoute } from './routes/UserRoute';
|
||||
import { BackstoryLayout } from './components/BackstoryLayout';
|
||||
import { SeverityType } from 'components/Snack';
|
||||
import { Query } from 'types/types';
|
||||
import { ConversationHandle } from 'components/Conversation';
|
||||
import { UserProvider } from 'components/UserContext';
|
||||
import { UserRoute } from 'routes/UserRoute';
|
||||
import { BackstoryLayout } from 'components/layout/BackstoryLayout';
|
||||
|
||||
import './BackstoryApp.css';
|
||||
import '@fontsource/roboto/300.css';
|
||||
@ -17,7 +17,7 @@ import '@fontsource/roboto/400.css';
|
||||
import '@fontsource/roboto/500.css';
|
||||
import '@fontsource/roboto/700.css';
|
||||
|
||||
import { connectionBase } from './Global';
|
||||
import { connectionBase } from './utils/Global';
|
||||
|
||||
// Cookie handling functions
|
||||
const getCookie = (name: string) => {
|
||||
|
112
frontend/src/README.md
Normal file
112
frontend/src/README.md
Normal file
@ -0,0 +1,112 @@
|
||||
# Disk structure
|
||||
|
||||
Below is the general directory structure for the Backstory platform, prioritizing maintainability and developer experience:
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/ # Reusable UI components
|
||||
│ ├── common/ # Generic components (Button, Modal, etc.)
|
||||
│ ├── forms/ # Form-related components
|
||||
│ ├── layout/ # Layout components (Header, Sidebar, etc.)
|
||||
│ └── ui/ # MUI customizations and themed components
|
||||
├── pages/ # Page-level components (route components)
|
||||
│ ├── auth/
|
||||
│ ├── dashboard/
|
||||
│ ├── profile/
|
||||
│ └── settings/
|
||||
├── features/ # Feature-specific modules
|
||||
│ ├── authentication/
|
||||
│ │ ├── components/
|
||||
│ │ ├── hooks/
|
||||
│ │ ├── services/
|
||||
│ │ └── types/
|
||||
│ ├── user-management/
|
||||
│ └── analytics/
|
||||
├── hooks/ # Custom React hooks
|
||||
│ ├── api/ # API-related hooks
|
||||
│ ├── ui/ # UI state hooks
|
||||
│ └── utils/ # Utility hooks
|
||||
├── services/ # API calls and external services
|
||||
│ ├── api/
|
||||
│ ├── auth/
|
||||
│ └── storage/
|
||||
├── store/ # State management (Redux/Zustand/Context)
|
||||
│ ├── slices/ # If using Redux Toolkit
|
||||
│ ├── providers/ # Context providers
|
||||
│ └── types/
|
||||
├── utils/ # Pure utility functions
|
||||
│ ├── constants/
|
||||
│ ├── helpers/
|
||||
│ └── validators/
|
||||
├── styles/ # Global styles and theme
|
||||
│ ├── theme/ # MUI theme customization
|
||||
│ ├── globals.css
|
||||
│ └── variables.css
|
||||
├── types/ # TypeScript type definitions
|
||||
│ ├── api/
|
||||
│ ├── common/
|
||||
│ └── components/
|
||||
├── assets/ # Static assets
|
||||
│ ├── images/
|
||||
│ ├── icons/
|
||||
│ └── fonts/
|
||||
├── config/ # Configuration files
|
||||
│ ├── env.ts
|
||||
│ ├── routes.ts
|
||||
│ └── constants.ts
|
||||
└── __tests__/ # Test files mirroring src structure
|
||||
├── components/
|
||||
├── pages/
|
||||
└── utils/
|
||||
```
|
||||
|
||||
# Key organizational principles:
|
||||
|
||||
1. Feature-Based Architecture
|
||||
The features/ directory groups related functionality together, making it easy to find everything related to a specific feature in one place.
|
||||
|
||||
2. Clear Separation of Concerns
|
||||
|
||||
```
|
||||
components/ - Pure UI components
|
||||
pages/ - Route-level components
|
||||
services/ - Data fetching and external APIs
|
||||
hooks/ - Reusable logic
|
||||
utils/ - Pure functions
|
||||
```
|
||||
|
||||
3. Scalable Component Organization
|
||||
Components are organized by purpose rather than alphabetically, with subcategories that make sense as the app grows.
|
||||
|
||||
4. Centralized Configuration
|
||||
All app configuration lives in config/, making it easy to manage environment variables, routes, and constants.
|
||||
5. Type Safety First
|
||||
Dedicated types/ directory with clear categorization helps maintain type definitions as the app scales.
|
||||
|
||||
# Naming Conventions
|
||||
|
||||
* Use PascalCase for components (UserProfile.tsx)
|
||||
* Use camelCase for utilities and hooks (formatDate.ts, useLocalStorage.ts)
|
||||
* Use kebab-case for directories (user-management/)
|
||||
|
||||
# Index Files
|
||||
Create index.ts files in major directories to enable clean imports:
|
||||
|
||||
```typescript
|
||||
// components/common/index.ts
|
||||
export { Button } from './Button';
|
||||
export { Modal } from './Modal';
|
||||
|
||||
// Import usage
|
||||
import { Button, Modal } from '@/components/common';
|
||||
```
|
||||
|
||||
# Path Aliases
|
||||
Configure path aliases in your build tool:
|
||||
|
||||
```typescript
|
||||
// Instead of: ../../../../components/common/Button
|
||||
import { Button } from '@/components/common/Button';
|
||||
```
|
||||
|
||||
This structure scales well while keeping related code co-located and maintaining clear boundaries between different types of functionality.
|
@ -24,6 +24,8 @@ import PhoneInput from 'react-phone-number-input';
|
||||
import { E164Number } from 'libphonenumber-js/core';
|
||||
import './PhoneInput.css';
|
||||
|
||||
import { ApiClient } from 'types/api-client';
|
||||
|
||||
// Import conversion utilities
|
||||
import {
|
||||
formatApiRequest,
|
||||
@ -36,9 +38,8 @@ import {
|
||||
} from './types/conversion';
|
||||
|
||||
import {
|
||||
AuthResponse, BaseUser, Guest
|
||||
AuthResponse, BaseUser, Guest, Candidate
|
||||
} from './types/types'
|
||||
import { connectionBase } from 'Global';
|
||||
|
||||
interface LoginRequest {
|
||||
login: string;
|
||||
@ -54,9 +55,8 @@ interface RegisterRequest {
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
const API_BASE_URL = `${connectionBase}/api/1.0`;
|
||||
|
||||
const BackstoryTestApp: React.FC = () => {
|
||||
const apiClient = new ApiClient();
|
||||
const [currentUser, setCurrentUser] = useState<BaseUser | null>(null);
|
||||
const [guestSession, setGuestSession] = useState<Guest | null>(null);
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
@ -139,24 +139,7 @@ const BackstoryTestApp: React.FC = () => {
|
||||
setSuccess(null);
|
||||
|
||||
try {
|
||||
// Format request data for API (camelCase to snake_case)
|
||||
const requestData = formatApiRequest({
|
||||
login: loginForm.login,
|
||||
password: loginForm.password
|
||||
});
|
||||
|
||||
debugConversion(requestData, 'Login Request');
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
});
|
||||
|
||||
// Use conversion utility to handle response
|
||||
const authResponse = await handleApiResponse<AuthResponse>(response);
|
||||
const authResponse = await apiClient.login(loginForm.login, loginForm.password)
|
||||
|
||||
debugConversion(authResponse, 'Login Response');
|
||||
|
||||
@ -186,7 +169,7 @@ const BackstoryTestApp: React.FC = () => {
|
||||
setSuccess(null);
|
||||
|
||||
try {
|
||||
const candidateData = {
|
||||
const candidate: Candidate = {
|
||||
username: registerForm.username,
|
||||
email: registerForm.email,
|
||||
firstName: registerForm.firstName,
|
||||
@ -210,21 +193,7 @@ const BackstoryTestApp: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Format request data for API (camelCase to snake_case, dates to ISO strings)
|
||||
const requestData = formatApiRequest(candidateData);
|
||||
|
||||
debugConversion(requestData, 'Registration Request');
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/candidates`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
});
|
||||
|
||||
// Use conversion utility to handle response
|
||||
const result = await handleApiResponse<any>(response);
|
||||
const result = await apiClient.createCandidate(candidate);
|
||||
|
||||
debugConversion(result, 'Registration Response');
|
||||
|
||||
|
@ -9,13 +9,13 @@ import { SxProps, Theme } from '@mui/material';
|
||||
import PropagateLoader from "react-spinners/PropagateLoader";
|
||||
|
||||
import { Message, MessageList, BackstoryMessage, MessageRoles } from './Message';
|
||||
import { DeleteConfirmation } from './DeleteConfirmation';
|
||||
import { Query } from '../types/types';
|
||||
import { BackstoryTextField, BackstoryTextFieldRef } from './BackstoryTextField';
|
||||
import { DeleteConfirmation } from 'components/DeleteConfirmation';
|
||||
import { Query } from 'types/types';
|
||||
import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField';
|
||||
import { BackstoryElementProps } from './BackstoryTab';
|
||||
import { connectionBase } from '../Global';
|
||||
import { useUser } from "./UserContext";
|
||||
import { streamQueryResponse, StreamQueryController } from './streamQueryResponse';
|
||||
import { connectionBase } from 'utils/Global';
|
||||
import { useUser } from "components/UserContext";
|
||||
import { streamQueryResponse, StreamQueryController } from 'services/streamQueryResponse';
|
||||
|
||||
import './Conversation.css';
|
||||
|
||||
|
@ -2,10 +2,10 @@ import React, { useEffect, useState, useRef } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import PropagateLoader from 'react-spinners/PropagateLoader';
|
||||
import { Quote } from 'components/Quote';
|
||||
import { streamQueryResponse, StreamQueryController } from './streamQueryResponse';
|
||||
import { connectionBase } from 'Global';
|
||||
import { streamQueryResponse, StreamQueryController } from 'services/streamQueryResponse';
|
||||
import { connectionBase } from 'utils/Global';
|
||||
import { BackstoryElementProps } from 'components/BackstoryTab';
|
||||
import { useUser } from './UserContext';
|
||||
import { useUser } from 'components/UserContext';
|
||||
|
||||
interface GenerateImageProps extends BackstoryElementProps {
|
||||
prompt: string
|
||||
|
@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import mermaid, { MermaidConfig } from 'mermaid';
|
||||
import { SxProps } from '@mui/material/styles';
|
||||
import { Box } from '@mui/material';
|
||||
import { useResizeObserverAndMutationObserver } from './useAutoScrollToBottom';
|
||||
import { useResizeObserverAndMutationObserver } from '../hooks/useAutoScrollToBottom';
|
||||
|
||||
const defaultMermaidConfig : MermaidConfig = {
|
||||
startOnLoad: true,
|
||||
|
@ -76,7 +76,6 @@ type BackstoryMessage = {
|
||||
expandable?: boolean,
|
||||
};
|
||||
|
||||
|
||||
interface ChatBubbleProps {
|
||||
role: MessageRoles,
|
||||
isInfo?: boolean;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Box from '@mui/material/Box';
|
||||
import { SxProps, Theme } from '@mui/material';
|
||||
import { RefObject, useRef } from 'react';
|
||||
import { useAutoScrollToBottom } from './useAutoScrollToBottom';
|
||||
import { useAutoScrollToBottom } from '../hooks/useAutoScrollToBottom';
|
||||
|
||||
interface ScrollableProps {
|
||||
children?: React.ReactNode;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||
import { SetSnackType } from './Snack';
|
||||
import { connectionBase } from '../Global';
|
||||
import { connectionBase } from '../utils/Global';
|
||||
import { User } from '../types/types';
|
||||
|
||||
type UserContextType = {
|
||||
|
@ -17,7 +17,7 @@ import TableContainer from '@mui/material/TableContainer';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
|
||||
import { Scrollable } from './Scrollable';
|
||||
import { connectionBase } from '../Global';
|
||||
import { connectionBase } from '../utils/Global';
|
||||
|
||||
import './VectorVisualizer.css';
|
||||
import { BackstoryPageProps } from './BackstoryTab';
|
||||
|
@ -17,7 +17,7 @@ import { Footer } from 'components/layout/Footer';
|
||||
import { Snack, SetSnackType } from 'components/Snack';
|
||||
import { useUser } from 'components/UserContext';
|
||||
import { User } from 'types/types';
|
||||
import { getBackstoryDynamicRoutes } from 'components/BackstoryRoutes';
|
||||
import { getBackstoryDynamicRoutes } from 'components/layout/BackstoryRoutes';
|
||||
import { LoadingComponent } from "components/LoadingComponent";
|
||||
|
||||
type NavigationLinkType = {
|
||||
@ -73,9 +73,9 @@ const getNavigationLinks = (user: User | null): NavigationLinkType[] => {
|
||||
}
|
||||
|
||||
switch (user.userType) {
|
||||
case 'UserType.CANDIDATE':
|
||||
case 'candidate':
|
||||
return CandidateNavItems;
|
||||
case 'UserType.EMPLOYER':
|
||||
case 'employer':
|
||||
return EmployerNavItems;
|
||||
default:
|
||||
return DefaultNavItems;
|
||||
|
@ -2,8 +2,8 @@ import React, { Ref, ReactNode } from "react";
|
||||
import { Route } from "react-router-dom";
|
||||
import { Typography } from '@mui/material';
|
||||
|
||||
import { BackstoryPageProps } from './BackstoryTab';
|
||||
import { ConversationHandle } from './Conversation';
|
||||
import { BackstoryPageProps } from '../BackstoryTab';
|
||||
import { ConversationHandle } from '../Conversation';
|
||||
import { User } from 'types/types';
|
||||
|
||||
import { ChatPage } from 'pages/ChatPage';
|
||||
@ -59,7 +59,7 @@ const getBackstoryDynamicRoutes = (props: BackstoryDynamicRoutesProps, user?: Us
|
||||
|
||||
routes.push(<Route key={`${index++}`} path="/logout" element={<LogoutPage />} />);
|
||||
|
||||
if (user.userType === "UserType.CANDIDATE") {
|
||||
if (user.userType === 'candidate') {
|
||||
routes.splice(-1, 0, ...[
|
||||
<Route key={`${index++}`} path="/profile" element={<ProfilePage />} />,
|
||||
<Route key={`${index++}`} path="/backstory" element={<BackstoryPage />} />,
|
||||
@ -68,7 +68,7 @@ const getBackstoryDynamicRoutes = (props: BackstoryDynamicRoutesProps, user?: Us
|
||||
]);
|
||||
}
|
||||
|
||||
if (user.userType === "UserType.EMPLOYER") {
|
||||
if (user.userType === 'employer') {
|
||||
routes.splice(-1, 0, ...[
|
||||
<Route key={`${index++}`} path="/search" element={<SearchPage />} />,
|
||||
<Route key={`${index++}`} path="/saved" element={<SavedPage />} />,
|
@ -4,7 +4,7 @@ import { ThemeProvider } from '@mui/material/styles';
|
||||
import { backstoryTheme } from './BackstoryTheme';
|
||||
import { BrowserRouter as Router } from "react-router-dom";
|
||||
import { BackstoryApp } from './BackstoryApp';
|
||||
// import { BackstoryTestApp } from 'TestApp';
|
||||
import { BackstoryTestApp } from 'TestApp';
|
||||
|
||||
import './index.css';
|
||||
|
||||
@ -16,8 +16,8 @@ root.render(
|
||||
<React.StrictMode>
|
||||
<ThemeProvider theme={backstoryTheme}>
|
||||
<Router>
|
||||
<BackstoryApp />
|
||||
{/* <BackstoryTestApp /> */}
|
||||
{/* <BackstoryApp /> */}
|
||||
<BackstoryTestApp />
|
||||
</Router>
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>
|
||||
|
@ -5,7 +5,7 @@ import Box from '@mui/material/Box';
|
||||
|
||||
import { BackstoryPageProps } from '../components/BackstoryTab';
|
||||
import { CandidateInfo } from 'components/CandidateInfo';
|
||||
import { connectionBase } from '../Global';
|
||||
import { connectionBase } from '../utils/Global';
|
||||
import { Candidate } from "../types/types";
|
||||
|
||||
const CandidateListingPage = (props: BackstoryPageProps) => {
|
||||
|
@ -17,7 +17,7 @@ const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: Back
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const [questions, setQuestions] = useState<React.ReactElement[]>([]);
|
||||
const { user } = useUser();
|
||||
const candidate: Candidate | null = (user && user.userType === "UserType.CANDIDATE") ? user as Candidate : null;
|
||||
const candidate: Candidate | null = (user && user.userType === 'candidate') ? user as Candidate : null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!candidate) {
|
||||
|
@ -14,7 +14,7 @@ import Typography from '@mui/material/Typography';
|
||||
// import ResetIcon from '@mui/icons-material/History';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
|
||||
import { connectionBase } from '../Global';
|
||||
import { connectionBase } from '../utils/Global';
|
||||
import { BackstoryPageProps } from '../components/BackstoryTab';
|
||||
|
||||
interface ServerTunables {
|
||||
|
@ -8,15 +8,17 @@ import IconButton from '@mui/material/IconButton';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import SendIcon from '@mui/icons-material/Send';
|
||||
import PropagateLoader from 'react-spinners/PropagateLoader';
|
||||
import { jsonrepair } from 'jsonrepair';
|
||||
|
||||
|
||||
import { CandidateInfo } from '../components/CandidateInfo';
|
||||
import { Query } from '../types/types'
|
||||
import { Quote } from 'components/Quote';
|
||||
import { streamQueryResponse, StreamQueryController } from '../components/streamQueryResponse';
|
||||
import { connectionBase } from 'Global';
|
||||
import { streamQueryResponse, StreamQueryController } from 'services/streamQueryResponse';
|
||||
import { connectionBase } from 'utils/Global';
|
||||
import { Candidate } from '../types/types';
|
||||
import { BackstoryElementProps } from 'components/BackstoryTab';
|
||||
import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField';
|
||||
import { jsonrepair } from 'jsonrepair';
|
||||
import { StyledMarkdown } from 'components/StyledMarkdown';
|
||||
import { Scrollable } from '../components/Scrollable';
|
||||
import { Pulse } from 'components/Pulse';
|
||||
|
@ -3,7 +3,7 @@ import { useParams, useNavigate } from "react-router-dom";
|
||||
import { useUser } from "../components/UserContext";
|
||||
import { User } from "../types/types";
|
||||
import { Box } from "@mui/material";
|
||||
import { connectionBase } from "../Global";
|
||||
import { connectionBase } from "../utils/Global";
|
||||
import { SetSnackType } from '../components/Snack';
|
||||
import { LoadingComponent } from "../components/LoadingComponent";
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { BackstoryMessage } from './Message';
|
||||
import { Query } from '../types/types';
|
||||
import { BackstoryMessage } from 'components/Message';
|
||||
import { Query } from 'types/types';
|
||||
import { jsonrepair } from 'jsonrepair';
|
||||
|
||||
type StreamQueryOptions = {
|
@ -21,12 +21,17 @@ import {
|
||||
PaginatedRequest
|
||||
} from './conversion';
|
||||
|
||||
export class ApiClient {
|
||||
class ApiClient {
|
||||
private baseUrl: string;
|
||||
private defaultHeaders: Record<string, string>;
|
||||
|
||||
constructor(baseUrl: string, authToken?: string) {
|
||||
this.baseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash
|
||||
constructor(authToken?: string) {
|
||||
const loc = window.location;
|
||||
if (!loc.host.match(/.*battle-linux.*/)) {
|
||||
this.baseUrl = loc.protocol + "//" + loc.host + "/api/1.0";
|
||||
} else {
|
||||
this.baseUrl = loc.protocol + "//battle-linux.ketrenos.com:8912/api/1.0";
|
||||
}
|
||||
this.defaultHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
...(authToken && { 'Authorization': `Bearer ${authToken}` })
|
||||
@ -367,13 +372,6 @@ export class ApiClient {
|
||||
getBaseUrl(): string {
|
||||
return this.baseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update base URL
|
||||
*/
|
||||
setBaseUrl(url: string): void {
|
||||
this.baseUrl = url.replace(/\/$/, '');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================
|
||||
@ -382,7 +380,7 @@ export class ApiClient {
|
||||
|
||||
/*
|
||||
// Initialize API client
|
||||
const apiClient = new ApiClient('https://api.yourjobplatform.com');
|
||||
const apiClient = new ApiClient();
|
||||
|
||||
// Login and set auth token
|
||||
try {
|
||||
@ -573,4 +571,4 @@ function CandidateList() {
|
||||
}
|
||||
*/
|
||||
|
||||
export default ApiClient;
|
||||
export { ApiClient };
|
@ -1,6 +1,6 @@
|
||||
// Generated TypeScript types from Pydantic models
|
||||
// Source: src/models.py
|
||||
// Generated on: 2025-05-27T23:44:38.806039
|
||||
// Generated on: 2025-05-28T20:34:39.642452
|
||||
// DO NOT EDIT MANUALLY - This file is auto-generated
|
||||
|
||||
// ============================
|
||||
@ -181,7 +181,7 @@ export interface Candidate {
|
||||
lastLogin?: Date;
|
||||
profileImage?: string;
|
||||
status: "active" | "inactive" | "pending" | "banned";
|
||||
userType?: "UserType.CANDIDATE";
|
||||
userType?: "candidate";
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
fullName: string;
|
||||
@ -328,7 +328,7 @@ export interface Employer {
|
||||
lastLogin?: Date;
|
||||
profileImage?: string;
|
||||
status: "active" | "inactive" | "pending" | "banned";
|
||||
userType?: "UserType.EMPLOYER";
|
||||
userType?: "employer";
|
||||
companyName: string;
|
||||
industry: string;
|
||||
description?: string;
|
||||
|
@ -114,10 +114,18 @@ def python_type_to_typescript(python_type: Any) -> str:
|
||||
return f"Record<{key_type}, {value_type}>"
|
||||
return "Record<string, any>"
|
||||
|
||||
# Handle Literal types
|
||||
# Handle Literal types - UPDATED SECTION
|
||||
if hasattr(python_type, '__origin__') and str(python_type.__origin__).endswith('Literal'):
|
||||
if args:
|
||||
literal_values = [f'"{arg}"' if isinstance(arg, str) else str(arg) for arg in args]
|
||||
literal_values = []
|
||||
for arg in args:
|
||||
if isinstance(arg, Enum):
|
||||
# Handle enum values within literals
|
||||
literal_values.append(f'"{arg.value}"')
|
||||
elif isinstance(arg, str):
|
||||
literal_values.append(f'"{arg}"')
|
||||
else:
|
||||
literal_values.append(str(arg))
|
||||
return " | ".join(literal_values)
|
||||
|
||||
# Handle Enum types
|
||||
@ -125,6 +133,10 @@ def python_type_to_typescript(python_type: Any) -> str:
|
||||
enum_values = [f'"{v.value}"' for v in python_type]
|
||||
return " | ".join(enum_values)
|
||||
|
||||
# Handle individual enum instances
|
||||
if isinstance(python_type, Enum):
|
||||
return f'"{python_type.value}"'
|
||||
|
||||
# Handle datetime
|
||||
if python_type == datetime:
|
||||
return "Date"
|
||||
|
@ -1,461 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Enhanced Type Generator - Generate TypeScript types from Pydantic models
|
||||
Now with command line parameters, pre-test validation, and TypeScript compilation
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
import subprocess
|
||||
from typing import Any, Dict, List, Optional, Union, get_origin, get_args
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
|
||||
def run_command(command: str, description: str, cwd: str | None = None) -> bool:
|
||||
"""Run a command and return success status"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=cwd
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
print(f"✅ {description}")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ {description} failed:")
|
||||
if result.stderr.strip():
|
||||
print(f" Error: {result.stderr.strip()}")
|
||||
if result.stdout.strip():
|
||||
print(f" Output: {result.stdout.strip()}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ {description} failed with exception: {e}")
|
||||
return False
|
||||
|
||||
def run_focused_test() -> bool:
|
||||
"""Run the focused test to validate models before generating types"""
|
||||
print("🧪 Running focused test to validate models...")
|
||||
|
||||
# Get the directory of the currently executing script
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
test_file_path = os.path.join(script_dir, "focused_test.py")
|
||||
|
||||
if not os.path.exists(test_file_path):
|
||||
print("❌ focused_test.py not found - skipping model validation")
|
||||
return False
|
||||
|
||||
return run_command(f"python {test_file_path}", "Model validation")
|
||||
|
||||
def check_typescript_available() -> bool:
|
||||
"""Check if TypeScript compiler is available"""
|
||||
return run_command("npx tsc --version", "TypeScript version check")
|
||||
|
||||
# Add current directory to Python path so we can import models
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, current_dir)
|
||||
|
||||
try:
|
||||
from pydantic import BaseModel # type: ignore
|
||||
except ImportError as e:
|
||||
print(f"Error importing pydantic: {e}")
|
||||
print("Make sure pydantic is installed: pip install pydantic")
|
||||
sys.exit(1)
|
||||
|
||||
def python_type_to_typescript(python_type: Any) -> str:
|
||||
"""Convert a Python type to TypeScript type string"""
|
||||
|
||||
# Handle None/null
|
||||
if python_type is type(None):
|
||||
return "null"
|
||||
|
||||
# Handle basic types
|
||||
if python_type == str:
|
||||
return "string"
|
||||
elif python_type == int or python_type == float:
|
||||
return "number"
|
||||
elif python_type == bool:
|
||||
return "boolean"
|
||||
elif python_type == dict or python_type == Dict:
|
||||
return "Record<string, any>"
|
||||
elif python_type == list or python_type == List:
|
||||
return "Array<any>"
|
||||
|
||||
# Handle typing generics
|
||||
origin = get_origin(python_type)
|
||||
args = get_args(python_type)
|
||||
|
||||
if origin is Union:
|
||||
# Handle Optional (Union[T, None])
|
||||
if len(args) == 2 and type(None) in args:
|
||||
non_none_type = next(arg for arg in args if arg is not type(None))
|
||||
return python_type_to_typescript(non_none_type)
|
||||
|
||||
# Handle other unions
|
||||
union_types = [python_type_to_typescript(arg) for arg in args if arg is not type(None)]
|
||||
return " | ".join(union_types)
|
||||
|
||||
elif origin is list or origin is List:
|
||||
if args:
|
||||
item_type = python_type_to_typescript(args[0])
|
||||
return f"Array<{item_type}>"
|
||||
return "Array<any>"
|
||||
|
||||
elif origin is dict or origin is Dict:
|
||||
if len(args) == 2:
|
||||
key_type = python_type_to_typescript(args[0])
|
||||
value_type = python_type_to_typescript(args[1])
|
||||
return f"Record<{key_type}, {value_type}>"
|
||||
return "Record<string, any>"
|
||||
|
||||
# Handle Literal types
|
||||
if hasattr(python_type, '__origin__') and str(python_type.__origin__).endswith('Literal'):
|
||||
if args:
|
||||
literal_values = [f'"{arg}"' if isinstance(arg, str) else str(arg) for arg in args]
|
||||
return " | ".join(literal_values)
|
||||
|
||||
# Handle Enum types
|
||||
if isinstance(python_type, type) and issubclass(python_type, Enum):
|
||||
enum_values = [f'"{v.value}"' for v in python_type]
|
||||
return " | ".join(enum_values)
|
||||
|
||||
# Handle datetime
|
||||
if python_type == datetime:
|
||||
return "Date"
|
||||
|
||||
# Handle Pydantic models
|
||||
if isinstance(python_type, type) and issubclass(python_type, BaseModel):
|
||||
return python_type.__name__
|
||||
|
||||
# Handle string representations
|
||||
type_str = str(python_type)
|
||||
if "EmailStr" in type_str:
|
||||
return "string"
|
||||
elif "HttpUrl" in type_str:
|
||||
return "string"
|
||||
elif "UUID" in type_str:
|
||||
return "string"
|
||||
|
||||
# Default fallback
|
||||
return "any"
|
||||
|
||||
def snake_to_camel(snake_str: str) -> str:
|
||||
"""Convert snake_case to camelCase"""
|
||||
components = snake_str.split('_')
|
||||
return components[0] + ''.join(x.title() for x in components[1:])
|
||||
|
||||
def process_pydantic_model(model_class) -> Dict[str, Any]:
|
||||
"""Process a Pydantic model and return TypeScript interface definition"""
|
||||
interface_name = model_class.__name__
|
||||
properties = []
|
||||
|
||||
# Get fields from the model
|
||||
if hasattr(model_class, 'model_fields'):
|
||||
# Pydantic v2
|
||||
fields = model_class.model_fields
|
||||
for field_name, field_info in fields.items():
|
||||
ts_name = snake_to_camel(field_name)
|
||||
|
||||
# Check for alias
|
||||
if hasattr(field_info, 'alias') and field_info.alias:
|
||||
ts_name = field_info.alias
|
||||
|
||||
# Get type annotation
|
||||
field_type = getattr(field_info, 'annotation', str)
|
||||
ts_type = python_type_to_typescript(field_type)
|
||||
|
||||
# Check if optional
|
||||
is_optional = False
|
||||
if hasattr(field_info, 'is_required'):
|
||||
is_optional = not field_info.is_required()
|
||||
elif hasattr(field_info, 'default'):
|
||||
is_optional = field_info.default is not None
|
||||
|
||||
properties.append({
|
||||
'name': ts_name,
|
||||
'type': ts_type,
|
||||
'optional': is_optional
|
||||
})
|
||||
|
||||
elif hasattr(model_class, '__fields__'):
|
||||
# Pydantic v1
|
||||
fields = model_class.__fields__
|
||||
for field_name, field_info in fields.items():
|
||||
ts_name = snake_to_camel(field_name)
|
||||
|
||||
if hasattr(field_info, 'alias') and field_info.alias:
|
||||
ts_name = field_info.alias
|
||||
|
||||
field_type = getattr(field_info, 'annotation', getattr(field_info, 'type_', str))
|
||||
ts_type = python_type_to_typescript(field_type)
|
||||
|
||||
is_optional = not getattr(field_info, 'required', True)
|
||||
if hasattr(field_info, 'default') and field_info.default is not None:
|
||||
is_optional = True
|
||||
|
||||
properties.append({
|
||||
'name': ts_name,
|
||||
'type': ts_type,
|
||||
'optional': is_optional
|
||||
})
|
||||
|
||||
return {
|
||||
'name': interface_name,
|
||||
'properties': properties
|
||||
}
|
||||
|
||||
def process_enum(enum_class) -> Dict[str, Any]:
|
||||
"""Process an Enum and return TypeScript type definition"""
|
||||
enum_name = enum_class.__name__
|
||||
values = [f'"{v.value}"' for v in enum_class]
|
||||
if len(values) == 0:
|
||||
raise ValueError(f"Enum class '{enum_name}' has no values.")
|
||||
return {
|
||||
'name': enum_name,
|
||||
'values': " | ".join(values)
|
||||
}
|
||||
|
||||
def generate_typescript_interfaces(source_file: str):
|
||||
"""Generate TypeScript interfaces from models"""
|
||||
|
||||
print(f"📖 Scanning {source_file} for Pydantic models and enums...")
|
||||
|
||||
# Import the models module dynamically
|
||||
try:
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location("models", source_file)
|
||||
if spec is None or spec.loader is None:
|
||||
raise ImportError(f"Could not load module from {source_file}")
|
||||
|
||||
models_module = importlib.util.module_from_spec(spec)
|
||||
sys.modules["models"] = models_module
|
||||
spec.loader.exec_module(models_module)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error importing {source_file}: {e}")
|
||||
return None
|
||||
|
||||
interfaces = []
|
||||
enums = []
|
||||
|
||||
# Scan the models module
|
||||
for name in dir(models_module):
|
||||
obj = getattr(models_module, name)
|
||||
|
||||
# Skip private attributes
|
||||
if name.startswith('_'):
|
||||
continue
|
||||
|
||||
try:
|
||||
# Check if it's a Pydantic model
|
||||
if (isinstance(obj, type) and
|
||||
issubclass(obj, BaseModel) and
|
||||
obj != BaseModel):
|
||||
|
||||
interface = process_pydantic_model(obj)
|
||||
interfaces.append(interface)
|
||||
print(f" ✅ Found Pydantic model: {name}")
|
||||
|
||||
# Check if it's an Enum
|
||||
elif (isinstance(obj, type) and
|
||||
issubclass(obj, Enum)):
|
||||
|
||||
enum_def = process_enum(obj)
|
||||
enums.append(enum_def)
|
||||
print(f" ✅ Found enum: {name}")
|
||||
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Warning: Error processing {name}: {e}")
|
||||
continue
|
||||
|
||||
print(f"\n📊 Found {len(interfaces)} interfaces and {len(enums)} enums")
|
||||
|
||||
# Generate TypeScript content
|
||||
ts_content = f"""// Generated TypeScript types from Pydantic models
|
||||
// Source: {source_file}
|
||||
// Generated on: {datetime.now().isoformat()}
|
||||
// DO NOT EDIT MANUALLY - This file is auto-generated
|
||||
|
||||
"""
|
||||
|
||||
# Add enums
|
||||
if enums:
|
||||
ts_content += "// ============================\n"
|
||||
ts_content += "// Enums\n"
|
||||
ts_content += "// ============================\n\n"
|
||||
|
||||
for enum_def in enums:
|
||||
ts_content += f"export type {enum_def['name']} = {enum_def['values']};\n\n"
|
||||
|
||||
# Add interfaces
|
||||
if interfaces:
|
||||
ts_content += "// ============================\n"
|
||||
ts_content += "// Interfaces\n"
|
||||
ts_content += "// ============================\n\n"
|
||||
|
||||
for interface in interfaces:
|
||||
ts_content += f"export interface {interface['name']} {{\n"
|
||||
|
||||
for prop in interface['properties']:
|
||||
optional_marker = "?" if prop['optional'] else ""
|
||||
ts_content += f" {prop['name']}{optional_marker}: {prop['type']};\n"
|
||||
|
||||
ts_content += "}\n\n"
|
||||
|
||||
# Add user union type if we have user types
|
||||
user_interfaces = [i for i in interfaces if i['name'] in ['Candidate', 'Employer']]
|
||||
if len(user_interfaces) >= 2:
|
||||
ts_content += "// ============================\n"
|
||||
ts_content += "// Union Types\n"
|
||||
ts_content += "// ============================\n\n"
|
||||
user_type_names = [i['name'] for i in user_interfaces]
|
||||
ts_content += f"export type User = {' | '.join(user_type_names)};\n\n"
|
||||
|
||||
# Add export statement
|
||||
ts_content += "// Export all types\n"
|
||||
ts_content += "export type { };\n"
|
||||
|
||||
return ts_content
|
||||
|
||||
def compile_typescript(ts_file: str) -> bool:
|
||||
"""Compile TypeScript file to check for syntax errors"""
|
||||
print(f"🔧 Compiling TypeScript file to check syntax...")
|
||||
|
||||
# Check if TypeScript is available
|
||||
if not check_typescript_available():
|
||||
print("⚠️ TypeScript compiler not available - skipping compilation check")
|
||||
print(" To install: npm install -g typescript")
|
||||
return True # Don't fail if TS isn't available
|
||||
|
||||
# Run TypeScript compiler in check mode
|
||||
return run_command(
|
||||
f"npx tsc --noEmit --skipLibCheck {ts_file}",
|
||||
"TypeScript syntax validation"
|
||||
)
|
||||
|
||||
def main():
|
||||
"""Main function with command line argument parsing"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Generate TypeScript types from Pydantic models',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
python generate_types.py # Use defaults
|
||||
python generate_types.py --source models.py --output types.ts # Specify files
|
||||
python generate_types.py --skip-test # Skip model validation
|
||||
python generate_types.py --skip-compile # Skip TS compilation
|
||||
python generate_types.py --source models.py --output types.ts --skip-test --skip-compile
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--source', '-s',
|
||||
default='models.py',
|
||||
help='Source Python file with Pydantic models (default: models.py)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--output', '-o',
|
||||
default='types.ts',
|
||||
help='Output TypeScript file (default: types.ts)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--skip-test',
|
||||
action='store_true',
|
||||
help='Skip running focused_test.py before generation'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--skip-compile',
|
||||
action='store_true',
|
||||
help='Skip TypeScript compilation check after generation'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--version', '-v',
|
||||
action='version',
|
||||
version='TypeScript Generator 2.0'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print("🚀 Enhanced TypeScript Type Generator")
|
||||
print("=" * 50)
|
||||
print(f"📁 Source file: {args.source}")
|
||||
print(f"📁 Output file: {args.output}")
|
||||
print()
|
||||
|
||||
try:
|
||||
# Step 1: Validate source file exists
|
||||
if not os.path.exists(args.source):
|
||||
print(f"❌ Source file '{args.source}' not found")
|
||||
sys.exit(1)
|
||||
|
||||
# Step 2: Run focused test (unless skipped)
|
||||
if not args.skip_test:
|
||||
if not run_focused_test():
|
||||
print("❌ Model validation failed - aborting type generation")
|
||||
sys.exit(1)
|
||||
print()
|
||||
else:
|
||||
print("⏭️ Skipping model validation test")
|
||||
print()
|
||||
|
||||
# Step 3: Generate TypeScript content
|
||||
print("🔄 Generating TypeScript types...")
|
||||
ts_content = generate_typescript_interfaces(args.source)
|
||||
|
||||
if ts_content is None:
|
||||
print("❌ Failed to generate TypeScript content")
|
||||
sys.exit(1)
|
||||
|
||||
# Step 4: Write to output file
|
||||
with open(args.output, 'w') as f:
|
||||
f.write(ts_content)
|
||||
|
||||
file_size = len(ts_content)
|
||||
print(f"✅ TypeScript types generated: {args.output} ({file_size} characters)")
|
||||
|
||||
# Step 5: Compile TypeScript (unless skipped)
|
||||
if not args.skip_compile:
|
||||
print()
|
||||
if not compile_typescript(args.output):
|
||||
print("❌ TypeScript compilation failed - check the generated file")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("⏭️ Skipping TypeScript compilation check")
|
||||
|
||||
# Step 6: Success summary
|
||||
print(f"\n🎉 Type generation completed successfully!")
|
||||
print("=" * 50)
|
||||
print(f"✅ Generated {args.output} from {args.source}")
|
||||
print(f"✅ File size: {file_size} characters")
|
||||
if not args.skip_test:
|
||||
print("✅ Model validation passed")
|
||||
if not args.skip_compile:
|
||||
print("✅ TypeScript syntax validated")
|
||||
print(f"\n💡 Usage in your TypeScript project:")
|
||||
print(f" import {{ Candidate, Employer, Job }} from './{Path(args.output).stem}';")
|
||||
|
||||
return True
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print(f"\n⏹️ Type generation cancelled by user")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error generating types: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = main()
|
||||
sys.exit(0 if success else 1)
|
Loading…
x
Reference in New Issue
Block a user