Date conversion is working from localstorage and via API

This commit is contained in:
James Ketr 2025-05-30 02:41:52 -07:00
parent adb407b19a
commit 4330bd4b7c
5 changed files with 569 additions and 184 deletions

View File

@ -1,10 +1,19 @@
// Persistent Authentication Hook with localStorage Integration
// Automatically restoring login state on page refresh
// Persistent Authentication Hook with localStorage Integration and Date Conversion
// Automatically restoring login state on page refresh with proper date handling
import React, { createContext, useContext,useState, useCallback, useEffect, useRef } from 'react';
import * as Types from 'types/types';
import { useUser } from 'hooks/useUser';
import { CreateCandidateRequest, CreateEmployerRequest, CreateViewerRequest, LoginRequest } from 'services/api-client';
import { formatApiRequest } from 'types/conversion';
// Import date conversion functions
import {
convertCandidateFromApi,
convertEmployerFromApi,
convertViewerFromApi,
convertFromApi,
} from 'types/types';
export interface AuthState {
user: Types.User | null;
@ -61,10 +70,59 @@ function clearStoredAuth(): void {
localStorage.removeItem(TOKEN_STORAGE.TOKEN_EXPIRY);
}
/**
* Convert user data to storage format (dates to ISO strings)
*/
function prepareUserDataForStorage(user: Types.User): string {
try {
// Convert dates to ISO strings for storage
const userForStorage = formatApiRequest(user);
return JSON.stringify(userForStorage);
} catch (error) {
console.error('Failed to prepare user data for storage:', error);
return JSON.stringify(user); // Fallback to direct serialization
}
}
/**
* Convert stored user data back to proper format (ISO strings to dates)
*/
function parseStoredUserData(userDataStr: string): Types.User | null {
try {
const rawUserData = JSON.parse(userDataStr);
// Determine user type and apply appropriate conversion
const userType = rawUserData.userType ||
(rawUserData.companyName ? 'employer' :
rawUserData.firstName ? 'candidate' : 'viewer');
switch (userType) {
case 'candidate':
return convertCandidateFromApi(rawUserData) as Types.Candidate;
case 'employer':
return convertEmployerFromApi(rawUserData) as Types.Employer;
case 'viewer':
return convertViewerFromApi(rawUserData) as Types.Viewer;
default:
// Fallback: try to determine by fields present
if (rawUserData.companyName) {
return convertEmployerFromApi(rawUserData) as Types.Employer;
} else if (rawUserData.skills || rawUserData.experience) {
return convertCandidateFromApi(rawUserData) as Types.Candidate;
} else {
return convertViewerFromApi(rawUserData) as Types.Viewer;
}
}
} catch (error) {
console.error('Failed to parse stored user data:', error);
return null;
}
}
function storeAuthData(authResponse: Types.AuthResponse): void {
localStorage.setItem(TOKEN_STORAGE.ACCESS_TOKEN, authResponse.accessToken);
localStorage.setItem(TOKEN_STORAGE.REFRESH_TOKEN, authResponse.refreshToken);
localStorage.setItem(TOKEN_STORAGE.USER_DATA, JSON.stringify(authResponse.user));
localStorage.setItem(TOKEN_STORAGE.USER_DATA, prepareUserDataForStorage(authResponse.user));
localStorage.setItem(TOKEN_STORAGE.TOKEN_EXPIRY, authResponse.expiresAt.toString());
}
@ -84,18 +142,31 @@ function getStoredAuthData(): {
try {
if (userDataStr) {
userData = JSON.parse(userDataStr);
userData = parseStoredUserData(userDataStr);
}
if (expiryStr) {
expiresAt = parseInt(expiryStr, 10);
}
} catch (error) {
console.error('Failed to parse stored auth data:', error);
// Clear corrupted data
clearStoredAuth();
}
return { accessToken, refreshToken, userData, expiresAt };
}
/**
* Update stored user data (useful when user profile is updated)
*/
function updateStoredUserData(user: Types.User): void {
try {
localStorage.setItem(TOKEN_STORAGE.USER_DATA, prepareUserDataForStorage(user));
} catch (error) {
console.error('Failed to update stored user data:', error);
}
}
export function useSecureAuth() {
const [authState, setAuthState] = useState<AuthState>({
user: null,
@ -108,10 +179,20 @@ export function useSecureAuth() {
const {apiClient} = useUser();
const initializationCompleted = useRef(false);
// Token refresh function
// Token refresh function with date conversion
const refreshAccessToken = useCallback(async (refreshToken: string): Promise<Types.AuthResponse | null> => {
try {
const response = await apiClient.refreshToken(refreshToken);
// Ensure user data has proper date conversion
if (response.user) {
const userType = response.user.userType ||
(response.user.companyName ? 'employer' :
response.user.firstName ? 'candidate' : 'viewer');
response.user = convertFromApi<Types.User>(response.user, userType);
}
return response;
} catch (error) {
console.error('Token refresh failed:', error);
@ -147,11 +228,11 @@ export function useSecureAuth() {
const refreshResult = await refreshAccessToken(stored.refreshToken);
if (refreshResult) {
// Successfully refreshed
// Successfully refreshed - store with proper date conversion
storeAuthData(refreshResult);
apiClient.setAuthToken(refreshResult.accessToken);
console.log("User =>", refreshResult.user);
console.log("User (refreshed) =>", refreshResult.user);
setAuthState({
user: refreshResult.user,
@ -161,7 +242,7 @@ export function useSecureAuth() {
error: null
});
console.log('Token refreshed successfully');
console.log('Token refreshed successfully with date conversion');
} else {
// Refresh failed, clear stored data
console.log('Token refresh failed, clearing stored auth');
@ -177,19 +258,19 @@ export function useSecureAuth() {
});
}
} else {
// Access token is still valid
// Access token is still valid - user data already has date conversion applied
apiClient.setAuthToken(stored.accessToken);
console.log("User =>", stored.userData);
console.log("User (from storage) =>", stored.userData);
setAuthState({
user: stored.userData,
user: stored.userData, // Already converted by parseStoredUserData
isAuthenticated: true,
isLoading: false,
isInitializing: false,
error: null
});
console.log('Restored authentication from stored tokens');
console.log('Restored authentication from stored tokens with date conversion');
}
} catch (error) {
console.error('Error initializing auth:', error);
@ -250,7 +331,16 @@ export function useSecureAuth() {
try {
const authResponse = await apiClient.login(loginData);
// Store tokens and user data
// Ensure user data has proper date conversion before storing
if (authResponse.user) {
const userType = authResponse.user.userType ||
(authResponse.user.companyName ? 'employer' :
authResponse.user.firstName ? 'candidate' : 'viewer');
authResponse.user = convertFromApi<Types.User>(authResponse.user, userType);
}
// Store tokens and user data with date conversion
storeAuthData(authResponse);
// Update API client with new token
@ -264,7 +354,7 @@ export function useSecureAuth() {
error: null
});
console.log('Login successful');
console.log('Login successful with date conversion applied');
return true;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Login failed';
@ -295,6 +385,20 @@ export function useSecureAuth() {
console.log('User logged out');
}, [apiClient]);
// Update user data in both state and localStorage (with date conversion)
const updateUserData = useCallback((updatedUser: Types.User) => {
// Update localStorage with proper date formatting
updateStoredUserData(updatedUser);
// Update state
setAuthState(prev => ({
...prev,
user: updatedUser
}));
console.log('User data updated with date conversion');
}, []);
const createViewerAccount = useCallback(async (viewerData: CreateViewerRequest): Promise<boolean> => {
setAuthState(prev => ({ ...prev, isLoading: true, error: null }));
@ -316,7 +420,9 @@ export function useSecureAuth() {
throw new Error(usernameValidation.issues.join(', '));
}
// Create viewer - API client automatically applies date conversion
const viewer = await apiClient.createViewer(viewerData);
console.log('Viewer created with date conversion:', viewer);
// Auto-login after successful registration
const loginSuccess = await login({
@ -357,7 +463,9 @@ export function useSecureAuth() {
throw new Error(usernameValidation.issues.join(', '));
}
// Create candidate - API client automatically applies date conversion
const candidate = await apiClient.createCandidate(candidateData);
console.log('Candidate created with date conversion:', candidate);
// Auto-login after successful registration
const loginSuccess = await login({
@ -398,7 +506,9 @@ export function useSecureAuth() {
throw new Error(usernameValidation.issues.join(', '));
}
// Create employer - API client automatically applies date conversion
const employer = await apiClient.createEmployer(employerData);
console.log('Employer created with date conversion:', employer);
// Auto-login after successful registration
const loginSuccess = await login({
@ -474,7 +584,8 @@ export function useSecureAuth() {
createCandidateAccount,
createEmployerAccount,
requestPasswordReset,
refreshAuth
refreshAuth,
updateUserData // New function to update user data with proper storage
};
}
@ -509,7 +620,7 @@ export function useAuth() {
interface ProtectedRouteProps {
children: React.ReactNode;
fallback?: React.ReactNode;
requiredUserType?: 'candidate' | 'employer';
requiredUserType?: 'candidate' | 'employer' | 'viewer';
}
export function ProtectedRoute({
@ -538,7 +649,7 @@ export function ProtectedRoute({
}
// ============================
// Usage Examples
// Usage Examples with Date Conversion
// ============================
/*
@ -580,7 +691,7 @@ function App() {
);
}
// Component using auth
// Component using auth with proper date handling
function Header() {
const { user, isAuthenticated, logout, isInitializing } = useAuth();
@ -592,7 +703,15 @@ function Header() {
<header>
{isAuthenticated ? (
<div>
Welcome, {user?.firstName || user?.companyName}!
<div>
Welcome, {user?.firstName || user?.companyName}!
{user?.createdAt && (
<small>Member since {user.createdAt.toLocaleDateString()}</small>
)}
{user?.lastLogin && (
<small>Last login: {user.lastLogin.toLocaleString()}</small>
)}
</div>
<button onClick={logout}>Logout</button>
</div>
) : (
@ -605,6 +724,48 @@ function Header() {
);
}
// Profile component with date operations
function UserProfile() {
const { user, updateUserData } = useAuth();
if (!user) return null;
// All date operations work properly because dates are Date objects
const accountAge = user.createdAt ?
Math.floor((Date.now() - user.createdAt.getTime()) / (1000 * 60 * 60 * 24)) : 0;
const handleProfileUpdate = async (updates: Partial<Types.User>) => {
// When updating user data, it will be properly stored with date conversion
const updatedUser = { ...user, ...updates, updatedAt: new Date() };
updateUserData(updatedUser);
};
return (
<div>
<h2>Profile</h2>
<p>Account created: {user.createdAt?.toLocaleDateString()}</p>
<p>Account age: {accountAge} days</p>
{user.lastLogin && (
<p>Last login: {user.lastLogin.toLocaleString()}</p>
)}
{'availabilityDate' in user && user.availabilityDate && (
<p>Available from: {user.availabilityDate.toLocaleDateString()}</p>
)}
{'experience' in user && user.experience?.map((exp, index) => (
<div key={index}>
<h4>{exp.position} at {exp.companyName}</h4>
<p>
{exp.startDate.toLocaleDateString()} -
{exp.endDate ? exp.endDate.toLocaleDateString() : 'Present'}
</p>
</div>
))}
</div>
);
}
// Auto-redirect based on auth state
function LoginPage() {
const { isAuthenticated, isInitializing } = useAuth();

View File

@ -290,7 +290,6 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
}
};
console.log(user);
// If user is logged in, show their profile
if (user) {
return (
@ -321,7 +320,7 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<Typography variant="body1" sx={{ mb: 1 }}>
<strong>Status:</strong> {user.status}
{/* <strong>Status:</strong> {user.status} */}
</Typography>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>

View File

@ -1,8 +1,9 @@
/**
* Enhanced API Client with Streaming Support
* Enhanced API Client with Streaming Support and Date Conversion
*
* This demonstrates how to use the generated types with the conversion utilities
* for seamless frontend-backend communication, including streaming responses.
* for seamless frontend-backend communication, including streaming responses and
* automatic date field conversion.
*/
// Import generated types (from running generate_types.py)
@ -21,6 +22,19 @@ import {
PaginatedRequest
} from 'types/conversion';
// Import generated date conversion functions
import {
convertCandidateFromApi,
convertEmployerFromApi,
convertJobFromApi,
convertJobApplicationFromApi,
convertChatSessionFromApi,
convertChatMessageFromApi,
convertViewerFromApi,
convertFromApi,
convertArrayFromApi
} from 'types/types';
// ============================
// Streaming Types and Interfaces
// ============================
@ -135,6 +149,61 @@ class ApiClient {
};
}
// ============================
// Enhanced Response Handlers with Date Conversion
// ============================
/**
* Handle API response with automatic date conversion for specific model types
*/
private async handleApiResponseWithConversion<T>(
response: Response,
modelType?: string
): Promise<T> {
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
const data = await response.json();
const apiResponse = parseApiResponse<T>(data);
const extractedData = extractApiData(apiResponse);
// Apply model-specific date conversion if modelType is provided
if (modelType) {
return convertFromApi<T>(extractedData, modelType);
}
return extractedData;
}
/**
* Handle paginated API response with automatic date conversion
*/
private async handlePaginatedApiResponseWithConversion<T>(
response: Response,
modelType?: string
): Promise<PaginatedResponse<T>> {
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
const data = await response.json();
const apiResponse = parsePaginatedResponse<T>(data);
const extractedData = extractApiData(apiResponse);
// Apply model-specific date conversion to array items if modelType is provided
if (modelType && extractedData.data) {
return {
...extractedData,
data: convertArrayFromApi<T>(extractedData.data, modelType)
};
}
return extractedData;
}
// ============================
// Authentication Methods
// ============================
@ -145,6 +214,7 @@ class ApiClient {
body: JSON.stringify(formatApiRequest(request))
});
// AuthResponse doesn't typically have date fields, use standard handler
return handleApiResponse<Types.AuthResponse>(response);
}
@ -169,7 +239,7 @@ class ApiClient {
}
// ============================
// Viewer Methods
// Viewer Methods with Date Conversion
// ============================
async createViewer(request: CreateViewerRequest): Promise<Types.Viewer> {
@ -179,11 +249,11 @@ class ApiClient {
body: JSON.stringify(formatApiRequest(request))
});
return handleApiResponse<Types.Viewer>(response);
return this.handleApiResponseWithConversion<Types.Viewer>(response, 'Viewer');
}
// ============================
// Candidate Methods
// Candidate Methods with Date Conversion
// ============================
async createCandidate(request: CreateCandidateRequest): Promise<Types.Candidate> {
@ -193,7 +263,7 @@ class ApiClient {
body: JSON.stringify(formatApiRequest(request))
});
return handleApiResponse<Types.Candidate>(response);
return this.handleApiResponseWithConversion<Types.Candidate>(response, 'Candidate');
}
async getCandidate(username: string): Promise<Types.Candidate> {
@ -201,7 +271,7 @@ class ApiClient {
headers: this.defaultHeaders
});
return handleApiResponse<Types.Candidate>(response);
return this.handleApiResponseWithConversion<Types.Candidate>(response, 'Candidate');
}
async updateCandidate(id: string, updates: Partial<Types.Candidate>): Promise<Types.Candidate> {
@ -211,7 +281,7 @@ class ApiClient {
body: JSON.stringify(formatApiRequest(updates))
});
return handleApiResponse<Types.Candidate>(response);
return this.handleApiResponseWithConversion<Types.Candidate>(response, 'Candidate');
}
async getCandidates(request: Partial<PaginatedRequest> = {}): Promise<PaginatedResponse<Types.Candidate>> {
@ -222,7 +292,7 @@ class ApiClient {
headers: this.defaultHeaders
});
return handlePaginatedApiResponse<Types.Candidate>(response);
return this.handlePaginatedApiResponseWithConversion<Types.Candidate>(response, 'Candidate');
}
async searchCandidates(query: string, filters?: Record<string, any>): Promise<PaginatedResponse<Types.Candidate>> {
@ -238,11 +308,11 @@ class ApiClient {
headers: this.defaultHeaders
});
return handlePaginatedApiResponse<Types.Candidate>(response);
return this.handlePaginatedApiResponseWithConversion<Types.Candidate>(response, 'Candidate');
}
// ============================
// Employer Methods
// Employer Methods with Date Conversion
// ============================
async createEmployer(request: CreateEmployerRequest): Promise<Types.Employer> {
@ -252,7 +322,7 @@ class ApiClient {
body: JSON.stringify(formatApiRequest(request))
});
return handleApiResponse<Types.Employer>(response);
return this.handleApiResponseWithConversion<Types.Employer>(response, 'Employer');
}
async getEmployer(id: string): Promise<Types.Employer> {
@ -260,7 +330,7 @@ class ApiClient {
headers: this.defaultHeaders
});
return handleApiResponse<Types.Employer>(response);
return this.handleApiResponseWithConversion<Types.Employer>(response, 'Employer');
}
async updateEmployer(id: string, updates: Partial<Types.Employer>): Promise<Types.Employer> {
@ -270,11 +340,11 @@ class ApiClient {
body: JSON.stringify(formatApiRequest(updates))
});
return handleApiResponse<Types.Employer>(response);
return this.handleApiResponseWithConversion<Types.Employer>(response, 'Employer');
}
// ============================
// Job Methods
// Job Methods with Date Conversion
// ============================
async createJob(job: Omit<Types.Job, 'id' | 'datePosted' | 'views' | 'applicationCount'>): Promise<Types.Job> {
@ -284,7 +354,7 @@ class ApiClient {
body: JSON.stringify(formatApiRequest(job))
});
return handleApiResponse<Types.Job>(response);
return this.handleApiResponseWithConversion<Types.Job>(response, 'Job');
}
async getJob(id: string): Promise<Types.Job> {
@ -292,7 +362,7 @@ class ApiClient {
headers: this.defaultHeaders
});
return handleApiResponse<Types.Job>(response);
return this.handleApiResponseWithConversion<Types.Job>(response, 'Job');
}
async getJobs(request: Partial<PaginatedRequest> = {}): Promise<PaginatedResponse<Types.Job>> {
@ -303,7 +373,7 @@ class ApiClient {
headers: this.defaultHeaders
});
return handlePaginatedApiResponse<Types.Job>(response);
return this.handlePaginatedApiResponseWithConversion<Types.Job>(response, 'Job');
}
async getJobsByEmployer(employerId: string, request: Partial<PaginatedRequest> = {}): Promise<PaginatedResponse<Types.Job>> {
@ -314,7 +384,7 @@ class ApiClient {
headers: this.defaultHeaders
});
return handlePaginatedApiResponse<Types.Job>(response);
return this.handlePaginatedApiResponseWithConversion<Types.Job>(response, 'Job');
}
async searchJobs(query: string, filters?: Record<string, any>): Promise<PaginatedResponse<Types.Job>> {
@ -330,11 +400,11 @@ class ApiClient {
headers: this.defaultHeaders
});
return handlePaginatedApiResponse<Types.Job>(response);
return this.handlePaginatedApiResponseWithConversion<Types.Job>(response, 'Job');
}
// ============================
// Job Application Methods
// Job Application Methods with Date Conversion
// ============================
async applyToJob(application: Omit<Types.JobApplication, 'id' | 'appliedDate' | 'updatedDate' | 'status'>): Promise<Types.JobApplication> {
@ -344,7 +414,7 @@ class ApiClient {
body: JSON.stringify(formatApiRequest(application))
});
return handleApiResponse<Types.JobApplication>(response);
return this.handleApiResponseWithConversion<Types.JobApplication>(response, 'JobApplication');
}
async getJobApplication(id: string): Promise<Types.JobApplication> {
@ -352,7 +422,7 @@ class ApiClient {
headers: this.defaultHeaders
});
return handleApiResponse<Types.JobApplication>(response);
return this.handleApiResponseWithConversion<Types.JobApplication>(response, 'JobApplication');
}
async getJobApplications(request: Partial<PaginatedRequest> = {}): Promise<PaginatedResponse<Types.JobApplication>> {
@ -363,7 +433,7 @@ class ApiClient {
headers: this.defaultHeaders
});
return handlePaginatedApiResponse<Types.JobApplication>(response);
return this.handlePaginatedApiResponseWithConversion<Types.JobApplication>(response, 'JobApplication');
}
async updateApplicationStatus(id: string, status: Types.ApplicationStatus): Promise<Types.JobApplication> {
@ -373,14 +443,14 @@ class ApiClient {
body: JSON.stringify(formatApiRequest({ status }))
});
return handleApiResponse<Types.JobApplication>(response);
return this.handleApiResponseWithConversion<Types.JobApplication>(response, 'JobApplication');
}
// ============================
// Chat Methods
// Chat Methods with Date Conversion
// ============================
/**
/**
* Create a chat session with optional candidate association
*/
async createChatSessionWithCandidate(
@ -392,7 +462,7 @@ class ApiClient {
body: JSON.stringify(formatApiRequest(request))
});
return handleApiResponse<Types.ChatSession>(response);
return this.handleApiResponseWithConversion<Types.ChatSession>(response, 'ChatSession');
}
/**
@ -409,7 +479,15 @@ class ApiClient {
headers: this.defaultHeaders
});
return handleApiResponse<CandidateSessionsResponse>(response);
// Handle the nested sessions with date conversion
const result = await this.handleApiResponseWithConversion<CandidateSessionsResponse>(response);
// Convert the nested sessions array
if (result.sessions && result.sessions.data) {
result.sessions.data = convertArrayFromApi<Types.ChatSession>(result.sessions.data, 'ChatSession');
}
return result;
}
/**
@ -439,7 +517,7 @@ class ApiClient {
body: JSON.stringify(formatApiRequest({ context }))
});
return handleApiResponse<Types.ChatSession>(response);
return this.handleApiResponseWithConversion<Types.ChatSession>(response, 'ChatSession');
}
async getChatSession(id: string): Promise<Types.ChatSession> {
@ -447,7 +525,7 @@ class ApiClient {
headers: this.defaultHeaders
});
return handleApiResponse<Types.ChatSession>(response);
return this.handleApiResponseWithConversion<Types.ChatSession>(response, 'ChatSession');
}
/**
@ -460,11 +538,11 @@ class ApiClient {
body: JSON.stringify(formatApiRequest({query}))
});
return handleApiResponse<Types.ChatMessage>(response);
return this.handleApiResponseWithConversion<Types.ChatMessage>(response, 'ChatMessage');
}
/**
* Send message with streaming response support
* Send message with streaming response support and date conversion
*/
sendMessageStream(
sessionId: string,
@ -501,7 +579,7 @@ class ApiClient {
const decoder = new TextDecoder();
let buffer = '';
let chatMessage: Types.ChatMessage | null = null;
const chatMessageList : Types.ChatMessage[] = [];
const chatMessageList: Types.ChatMessage[] = [];
try {
while (true) {
@ -526,30 +604,35 @@ class ApiClient {
const data = line.slice(5).trim();
const incoming: Types.ChatMessageBase = JSON.parse(data);
// Convert date fields for incoming messages
const convertedIncoming = convertChatMessageFromApi(incoming);
// Trigger callbacks based on status
if (incoming.status !== chatMessage?.status) {
options.onStatusChange?.(incoming.status);
if (convertedIncoming.status !== chatMessage?.status) {
options.onStatusChange?.(convertedIncoming.status);
}
// Handle different status types
switch (incoming.status) {
switch (convertedIncoming.status) {
case 'streaming':
if (chatMessage === null) {
chatMessage = {...incoming};
chatMessage = {...convertedIncoming};
} else {
// Can't do a simple += as typescript thinks .content might not be there
chatMessage.content = (chatMessage?.content || '') + incoming.content;
chatMessage.content = (chatMessage?.content || '') + convertedIncoming.content;
// Update timestamp to latest
chatMessage.timestamp = convertedIncoming.timestamp;
}
options.onStreaming?.(incoming);
options.onStreaming?.(convertedIncoming);
break;
case 'error':
options.onError?.(incoming);
options.onError?.(convertedIncoming);
break;
default:
chatMessageList.push(incoming);
options.onMessage?.(incoming);
chatMessageList.push(convertedIncoming);
options.onMessage?.(convertedIncoming);
break;
}
}
@ -606,7 +689,7 @@ class ApiClient {
}
/**
* Get persisted chat messages for a session
* Get persisted chat messages for a session with date conversion
*/
async getChatMessages(
sessionId: string,
@ -621,7 +704,7 @@ class ApiClient {
headers: this.defaultHeaders
});
return handlePaginatedApiResponse<Types.ChatMessage>(response);
return this.handlePaginatedApiResponseWithConversion<Types.ChatMessage>(response, 'ChatMessage');
}
// ============================
@ -712,10 +795,10 @@ class ApiClient {
// Error Handling Helper
// ============================
async handleRequest<T>(requestFn: () => Promise<Response>): Promise<T> {
async handleRequest<T>(requestFn: () => Promise<Response>, modelType?: string): Promise<T> {
try {
const response = await requestFn();
return await handleApiResponse<T>(response);
return await this.handleApiResponseWithConversion<T>(response, modelType);
} catch (error) {
console.error('API request failed:', error);
throw error;
@ -740,10 +823,10 @@ class ApiClient {
}
// ============================
// React Hooks for Streaming
// React Hooks for Streaming with Date Conversion
// ============================
/* React Hook Examples for Streaming Chat
/* React Hook Examples for Streaming Chat with proper date handling
import { useState, useEffect, useCallback, useRef } from 'react';
export function useStreamingChat(sessionId: string) {
@ -762,31 +845,39 @@ export function useStreamingChat(sessionId: string) {
const streamingOptions: StreamingOptions = {
onMessage: (message) => {
// Message already has proper Date objects from conversion
setCurrentMessage(message);
},
onPartialMessage: (content, messageId) => {
onStreaming: (chunk) => {
// Chunk also has proper Date objects
setCurrentMessage(prev => prev ?
{ ...prev, content: prev.content + content } :
{
...prev,
content: prev.content + chunk.content,
timestamp: chunk.timestamp // Update to latest timestamp
} :
{
id: messageId || '',
id: chunk.id || '',
sessionId,
status: 'streaming',
sender: 'ai',
content,
timestamp: new Date()
content: chunk.content,
timestamp: chunk.timestamp // Already a Date object
}
);
},
onStatusChange: (status) => {
setCurrentMessage(prev => prev ? { ...prev, status } : null);
},
onComplete: (finalMessage) => {
setMessages(prev => [...prev, finalMessage]);
onComplete: () => {
if (currentMessage) {
setMessages(prev => [...prev, currentMessage]);
}
setCurrentMessage(null);
setIsStreaming(false);
},
onError: (err) => {
setError(err.message);
setError(typeof err === 'string' ? err : err.content);
setIsStreaming(false);
setCurrentMessage(null);
}
@ -799,7 +890,7 @@ export function useStreamingChat(sessionId: string) {
setError(err instanceof Error ? err.message : 'Failed to send message');
setIsStreaming(false);
}
}, [sessionId, apiClient]);
}, [sessionId, apiClient, currentMessage]);
const cancelStreaming = useCallback(() => {
if (streamingRef.current) {
@ -819,7 +910,7 @@ export function useStreamingChat(sessionId: string) {
};
}
// Usage in React component:
// Usage in React component with proper date handling:
function ChatInterface({ sessionId }: { sessionId: string }) {
const {
messages,
@ -839,14 +930,26 @@ function ChatInterface({ sessionId }: { sessionId: string }) {
<div className="messages">
{messages.map(message => (
<div key={message.id}>
<strong>{message.sender}:</strong> {message.content}
<div className="message-header">
<strong>{message.sender}:</strong>
<span className="timestamp">
{message.timestamp.toLocaleTimeString()}
</span>
</div>
<div className="message-content">{message.content}</div>
</div>
))}
{currentMessage && (
<div className="current-message">
<strong>{currentMessage.sender}:</strong> {currentMessage.content}
{isStreaming && <span className="streaming-indicator">...</span>}
<div className="message-header">
<strong>{currentMessage.sender}:</strong>
<span className="timestamp">
{currentMessage.timestamp.toLocaleTimeString()}
</span>
{isStreaming && <span className="streaming-indicator">...</span>}
</div>
<div className="message-content">{currentMessage.content}</div>
</div>
)}
</div>
@ -874,61 +977,96 @@ function ChatInterface({ sessionId }: { sessionId: string }) {
*/
// ============================
// Usage Examples
// Usage Examples with Date Conversion
// ============================
/*
// Initialize API client
const apiClient = new ApiClient();
// Standard message sending (non-streaming)
// All returned objects now have proper Date fields automatically!
// Create a candidate - createdAt, updatedAt, lastLogin are Date objects
try {
const message = await apiClient.sendMessage(sessionId, 'Hello, how are you?');
console.log('Response:', message.content);
const candidate = await apiClient.createCandidate({
email: 'jane@example.com',
username: 'jane_doe',
password: 'SecurePassword123!',
firstName: 'Jane',
lastName: 'Doe'
});
// These are now Date objects, not strings!
console.log('Created at:', candidate.createdAt.toLocaleDateString());
console.log('Profile created on:', candidate.createdAt.toDateString());
if (candidate.lastLogin) {
console.log('Last seen:', candidate.lastLogin.toRelativeTimeString());
}
} catch (error) {
console.error('Failed to send message:', error);
console.error('Failed to create candidate:', error);
}
// Streaming message with callbacks
const streamResponse = apiClient.sendMessageStream(sessionId, 'Tell me a long story', {
onPartialMessage: (content, messageId) => {
console.log('Partial content:', content);
// Update UI with partial content
// Get jobs with proper date conversion
try {
const jobs = await apiClient.getJobs({ limit: 10 });
jobs.data.forEach(job => {
// datePosted, applicationDeadline, featuredUntil are Date objects
console.log(`${job.title} - Posted: ${job.datePosted.toLocaleDateString()}`);
if (job.applicationDeadline) {
const daysRemaining = Math.ceil(
(job.applicationDeadline.getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24)
);
console.log(`Deadline in ${daysRemaining} days`);
}
});
} catch (error) {
console.error('Failed to fetch jobs:', error);
}
// Streaming with proper date conversion
const streamResponse = apiClient.sendMessageStream(sessionId, 'Tell me about job opportunities', {
onStreaming: (chunk) => {
// chunk.timestamp is a Date object
console.log(`Streaming at ${chunk.timestamp.toLocaleTimeString()}:`, chunk.content);
},
onStatusChange: (status) => {
console.log('Status changed:', status);
// Update UI status indicator
onMessage: (message) => {
// message.timestamp is a Date object
console.log(`Final message at ${message.timestamp.toLocaleTimeString()}:`, message.content);
},
onComplete: (finalMessage) => {
console.log('Final message:', finalMessage.content);
// Handle completed message
},
onError: (error) => {
console.error('Streaming error:', error);
// Handle error
onComplete: () => {
console.log('Streaming completed');
}
});
// Can cancel the stream if needed
setTimeout(() => {
streamResponse.cancel();
}, 10000); // Cancel after 10 seconds
// Wait for completion
// Chat sessions with date conversion
try {
const finalMessage = await streamResponse.promise;
console.log('Stream completed:', finalMessage);
const chatSession = await apiClient.createChatSession({
type: 'job_search',
additionalContext: {}
});
// createdAt and lastActivity are Date objects
console.log('Session created:', chatSession.createdAt.toISOString());
console.log('Last activity:', chatSession.lastActivity.toLocaleDateString());
} catch (error) {
console.error('Stream failed:', error);
console.error('Failed to create chat session:', error);
}
// Auto-detection: streaming if callbacks provided, standard otherwise
await apiClient.sendMessageAuto(sessionId, 'Quick question', {
onPartialMessage: (content) => console.log('Streaming:', content)
}); // Will use streaming
await apiClient.sendMessageAuto(sessionId, 'Quick question'); // Will use standard
// Get chat messages with date conversion
try {
const messages = await apiClient.getChatMessages(sessionId);
messages.data.forEach(message => {
// timestamp is a Date object
console.log(`[${message.timestamp.toLocaleString()}] ${message.sender}: ${message.content}`);
});
} catch (error) {
console.error('Failed to fetch messages:', error);
}
*/
export { ApiClient }
export type { StreamingOptions, StreamingResponse };
export type { StreamingOptions, StreamingResponse }

View File

@ -1,6 +1,6 @@
// Generated TypeScript types from Pydantic models
// Source: src/backend/models.py
// Generated on: 2025-05-30T09:14:59.413256
// Generated on: 2025-05-30T09:39:47.716115
// DO NOT EDIT MANUALLY - This file is auto-generated
// ============================
@ -712,370 +712,400 @@ export interface WorkExperience {
/**
* Convert Analytics from API response, parsing date fields
* Date fields: timestamp
*/
export function convertAnalyticsFromApi(data: any): Analytics {
if (!data) return data;
return {
...data,
entityType: new Date(data.entityType),
// Convert timestamp from ISO string to Date
timestamp: new Date(data.timestamp),
};
}
/**
* Convert ApplicationDecision from API response, parsing date fields
* Date fields: date
*/
export function convertApplicationDecisionFromApi(data: any): ApplicationDecision {
if (!data) return data;
return {
...data,
// Convert date from ISO string to Date
date: new Date(data.date),
};
}
/**
* Convert Attachment from API response, parsing date fields
* Date fields: uploadedAt
*/
export function convertAttachmentFromApi(data: any): Attachment {
if (!data) return data;
return {
...data,
// Convert uploadedAt from ISO string to Date
uploadedAt: new Date(data.uploadedAt),
};
}
/**
* Convert AuthResponse from API response, parsing date fields
*/
export function convertAuthResponseFromApi(data: any): AuthResponse {
if (!data) return data;
return {
...data,
user: new Date(data.user),
};
}
/**
* Convert Authentication from API response, parsing date fields
* Date fields: resetPasswordExpiry, lastPasswordChange, lockedUntil
*/
export function convertAuthenticationFromApi(data: any): Authentication {
if (!data) return data;
return {
...data,
// Convert resetPasswordExpiry from ISO string to Date
resetPasswordExpiry: data.resetPasswordExpiry ? new Date(data.resetPasswordExpiry) : undefined,
// Convert lastPasswordChange from ISO string to Date
lastPasswordChange: new Date(data.lastPasswordChange),
// Convert lockedUntil from ISO string to Date
lockedUntil: data.lockedUntil ? new Date(data.lockedUntil) : undefined,
};
}
/**
* Convert BaseUser from API response, parsing date fields
* Date fields: createdAt, updatedAt, lastLogin
*/
export function convertBaseUserFromApi(data: any): BaseUser {
if (!data) return data;
return {
...data,
// Convert createdAt from ISO string to Date
createdAt: new Date(data.createdAt),
// Convert updatedAt from ISO string to Date
updatedAt: new Date(data.updatedAt),
// Convert lastLogin from ISO string to Date
lastLogin: data.lastLogin ? new Date(data.lastLogin) : undefined,
};
}
/**
* Convert BaseUserWithType from API response, parsing date fields
* Date fields: createdAt, updatedAt, lastLogin
*/
export function convertBaseUserWithTypeFromApi(data: any): BaseUserWithType {
if (!data) return data;
return {
...data,
// Convert createdAt from ISO string to Date
createdAt: new Date(data.createdAt),
// Convert updatedAt from ISO string to Date
updatedAt: new Date(data.updatedAt),
// Convert lastLogin from ISO string to Date
lastLogin: data.lastLogin ? new Date(data.lastLogin) : undefined,
};
}
/**
* Convert Candidate from API response, parsing date fields
* Date fields: createdAt, updatedAt, lastLogin, availabilityDate
*/
export function convertCandidateFromApi(data: any): Candidate {
if (!data) return data;
return {
...data,
// Convert createdAt from ISO string to Date
createdAt: new Date(data.createdAt),
// Convert updatedAt from ISO string to Date
updatedAt: new Date(data.updatedAt),
// Convert lastLogin from ISO string to Date
lastLogin: data.lastLogin ? new Date(data.lastLogin) : undefined,
userType: data.userType ? new Date(data.userType) : undefined,
questions: data.questions ? new Date(data.questions) : undefined,
// Convert availabilityDate from ISO string to Date
availabilityDate: data.availabilityDate ? new Date(data.availabilityDate) : undefined,
};
}
/**
* Convert CandidateListResponse from API response, parsing date fields
*/
export function convertCandidateListResponseFromApi(data: any): CandidateListResponse {
if (!data) return data;
return {
...data,
data: data.data ? new Date(data.data) : undefined,
};
}
/**
* Convert CandidateResponse from API response, parsing date fields
*/
export function convertCandidateResponseFromApi(data: any): CandidateResponse {
if (!data) return data;
return {
...data,
data: data.data ? new Date(data.data) : undefined,
};
}
/**
* Convert Certification from API response, parsing date fields
* Date fields: issueDate, expirationDate
*/
export function convertCertificationFromApi(data: any): Certification {
if (!data) return data;
return {
...data,
// Convert issueDate from ISO string to Date
issueDate: new Date(data.issueDate),
// Convert expirationDate from ISO string to Date
expirationDate: data.expirationDate ? new Date(data.expirationDate) : undefined,
};
}
/**
* Convert ChatContext from API response, parsing date fields
*/
export function convertChatContextFromApi(data: any): ChatContext {
if (!data) return data;
return {
...data,
relatedEntityType: data.relatedEntityType ? new Date(data.relatedEntityType) : undefined,
};
}
/**
* Convert ChatMessage from API response, parsing date fields
* Date fields: timestamp
*/
export function convertChatMessageFromApi(data: any): ChatMessage {
if (!data) return data;
return {
...data,
// Convert timestamp from ISO string to Date
timestamp: new Date(data.timestamp),
};
}
/**
* Convert ChatMessageBase from API response, parsing date fields
* Date fields: timestamp
*/
export function convertChatMessageBaseFromApi(data: any): ChatMessageBase {
if (!data) return data;
return {
...data,
// Convert timestamp from ISO string to Date
timestamp: new Date(data.timestamp),
};
}
/**
* Convert ChatMessageUser from API response, parsing date fields
* Date fields: timestamp
*/
export function convertChatMessageUserFromApi(data: any): ChatMessageUser {
if (!data) return data;
return {
...data,
// Convert timestamp from ISO string to Date
timestamp: new Date(data.timestamp),
};
}
/**
* Convert ChatSession from API response, parsing date fields
* Date fields: createdAt, lastActivity
*/
export function convertChatSessionFromApi(data: any): ChatSession {
if (!data) return data;
return {
...data,
// Convert createdAt from ISO string to Date
createdAt: data.createdAt ? new Date(data.createdAt) : undefined,
// Convert lastActivity from ISO string to Date
lastActivity: data.lastActivity ? new Date(data.lastActivity) : undefined,
};
}
/**
* Convert DataSourceConfiguration from API response, parsing date fields
* Date fields: lastRefreshed
*/
export function convertDataSourceConfigurationFromApi(data: any): DataSourceConfiguration {
if (!data) return data;
return {
...data,
// Convert lastRefreshed from ISO string to Date
lastRefreshed: data.lastRefreshed ? new Date(data.lastRefreshed) : undefined,
};
}
/**
* Convert EditHistory from API response, parsing date fields
* Date fields: editedAt
*/
export function convertEditHistoryFromApi(data: any): EditHistory {
if (!data) return data;
return {
...data,
// Convert editedAt from ISO string to Date
editedAt: new Date(data.editedAt),
};
}
/**
* Convert Education from API response, parsing date fields
* Date fields: startDate, endDate
*/
export function convertEducationFromApi(data: any): Education {
if (!data) return data;
return {
...data,
// Convert startDate from ISO string to Date
startDate: new Date(data.startDate),
// Convert endDate from ISO string to Date
endDate: data.endDate ? new Date(data.endDate) : undefined,
};
}
/**
* Convert Employer from API response, parsing date fields
* Date fields: createdAt, updatedAt, lastLogin
*/
export function convertEmployerFromApi(data: any): Employer {
if (!data) return data;
return {
...data,
// Convert createdAt from ISO string to Date
createdAt: new Date(data.createdAt),
// Convert updatedAt from ISO string to Date
updatedAt: new Date(data.updatedAt),
// Convert lastLogin from ISO string to Date
lastLogin: data.lastLogin ? new Date(data.lastLogin) : undefined,
};
}
/**
* Convert Guest from API response, parsing date fields
* Date fields: createdAt, lastActivity
*/
export function convertGuestFromApi(data: any): Guest {
if (!data) return data;
return {
...data,
// Convert createdAt from ISO string to Date
createdAt: new Date(data.createdAt),
// Convert lastActivity from ISO string to Date
lastActivity: new Date(data.lastActivity),
};
}
/**
* Convert InterviewFeedback from API response, parsing date fields
* Date fields: createdAt, updatedAt
*/
export function convertInterviewFeedbackFromApi(data: any): InterviewFeedback {
if (!data) return data;
return {
...data,
// Convert createdAt from ISO string to Date
createdAt: new Date(data.createdAt),
// Convert updatedAt from ISO string to Date
updatedAt: new Date(data.updatedAt),
};
}
/**
* Convert InterviewSchedule from API response, parsing date fields
* Date fields: scheduledDate, endDate
*/
export function convertInterviewScheduleFromApi(data: any): InterviewSchedule {
if (!data) return data;
return {
...data,
// Convert scheduledDate from ISO string to Date
scheduledDate: new Date(data.scheduledDate),
// Convert endDate from ISO string to Date
endDate: new Date(data.endDate),
};
}
/**
* Convert Job from API response, parsing date fields
* Date fields: datePosted, applicationDeadline, featuredUntil
*/
export function convertJobFromApi(data: any): Job {
if (!data) return data;
return {
...data,
// Convert datePosted from ISO string to Date
datePosted: new Date(data.datePosted),
// Convert applicationDeadline from ISO string to Date
applicationDeadline: data.applicationDeadline ? new Date(data.applicationDeadline) : undefined,
// Convert featuredUntil from ISO string to Date
featuredUntil: data.featuredUntil ? new Date(data.featuredUntil) : undefined,
};
}
/**
* Convert JobApplication from API response, parsing date fields
* Date fields: appliedDate, updatedDate
*/
export function convertJobApplicationFromApi(data: any): JobApplication {
if (!data) return data;
return {
...data,
// Convert appliedDate from ISO string to Date
appliedDate: new Date(data.appliedDate),
// Convert updatedDate from ISO string to Date
updatedDate: new Date(data.updatedDate),
candidateContact: data.candidateContact ? new Date(data.candidateContact) : undefined,
};
}
/**
* Convert MessageReaction from API response, parsing date fields
* Date fields: timestamp
*/
export function convertMessageReactionFromApi(data: any): MessageReaction {
if (!data) return data;
return {
...data,
// Convert timestamp from ISO string to Date
timestamp: new Date(data.timestamp),
};
}
/**
* Convert RAGConfiguration from API response, parsing date fields
* Date fields: createdAt, updatedAt
*/
export function convertRAGConfigurationFromApi(data: any): RAGConfiguration {
if (!data) return data;
return {
...data,
// Convert createdAt from ISO string to Date
createdAt: new Date(data.createdAt),
// Convert updatedAt from ISO string to Date
updatedAt: new Date(data.updatedAt),
};
}
/**
* Convert RefreshToken from API response, parsing date fields
* Date fields: expiresAt
*/
export function convertRefreshTokenFromApi(data: any): RefreshToken {
if (!data) return data;
return {
...data,
// Convert expiresAt from ISO string to Date
expiresAt: new Date(data.expiresAt),
};
}
/**
* Convert UserActivity from API response, parsing date fields
* Date fields: timestamp
*/
export function convertUserActivityFromApi(data: any): UserActivity {
if (!data) return data;
return {
...data,
// Convert timestamp from ISO string to Date
timestamp: new Date(data.timestamp),
};
}
/**
* Convert Viewer from API response, parsing date fields
* Date fields: createdAt, updatedAt, lastLogin
*/
export function convertViewerFromApi(data: any): Viewer {
if (!data) return data;
return {
...data,
// Convert createdAt from ISO string to Date
createdAt: new Date(data.createdAt),
// Convert updatedAt from ISO string to Date
updatedAt: new Date(data.updatedAt),
// Convert lastLogin from ISO string to Date
lastLogin: data.lastLogin ? new Date(data.lastLogin) : undefined,
};
}
/**
* Convert WorkExperience from API response, parsing date fields
* Date fields: startDate, endDate
*/
export function convertWorkExperienceFromApi(data: any): WorkExperience {
if (!data) return data;
return {
...data,
// Convert startDate from ISO string to Date
startDate: new Date(data.startDate),
// Convert endDate from ISO string to Date
endDate: data.endDate ? new Date(data.endDate) : undefined,
};
}
@ -1094,8 +1124,6 @@ export function convertFromApi<T>(data: any, modelType: string): T {
return convertApplicationDecisionFromApi(data) as T;
case 'Attachment':
return convertAttachmentFromApi(data) as T;
case 'AuthResponse':
return convertAuthResponseFromApi(data) as T;
case 'Authentication':
return convertAuthenticationFromApi(data) as T;
case 'BaseUser':
@ -1104,14 +1132,8 @@ export function convertFromApi<T>(data: any, modelType: string): T {
return convertBaseUserWithTypeFromApi(data) as T;
case 'Candidate':
return convertCandidateFromApi(data) as T;
case 'CandidateListResponse':
return convertCandidateListResponseFromApi(data) as T;
case 'CandidateResponse':
return convertCandidateResponseFromApi(data) as T;
case 'Certification':
return convertCertificationFromApi(data) as T;
case 'ChatContext':
return convertChatContextFromApi(data) as T;
case 'ChatMessage':
return convertChatMessageFromApi(data) as T;
case 'ChatMessageBase':

View File

@ -101,10 +101,42 @@ def is_date_type(python_type: Any) -> bool:
if python_type == datetime:
return True
# String representation checks for various datetime types
# Check if it's a datetime type from the datetime module
if hasattr(python_type, '__module__') and hasattr(python_type, '__name__'):
module_name = getattr(python_type, '__module__', '')
type_name = getattr(python_type, '__name__', '')
# Check for datetime module types
if module_name == 'datetime' and type_name in ('datetime', 'date', 'time'):
return True
# String representation checks for specific datetime patterns (more restrictive)
type_str = str(python_type)
date_patterns = ['datetime', 'date', 'DateTime', 'Date']
return any(pattern in type_str for pattern in date_patterns)
# Be very specific about datetime patterns to avoid false positives
specific_date_patterns = [
'datetime.datetime',
'datetime.date',
'datetime.time',
'<class \'datetime.datetime\'>',
'<class \'datetime.date\'>',
'<class \'datetime.time\'>',
'typing.DateTime',
'pydantic.datetime',
]
# Check for exact matches or specific patterns
for pattern in specific_date_patterns:
if pattern in type_str:
return True
# Additional check for common datetime type aliases
if hasattr(python_type, '__origin__'):
origin_str = str(python_type.__origin__)
if 'datetime' in origin_str and 'datetime.' in origin_str:
return True
return False
def python_type_to_typescript(python_type: Any, debug: bool = False) -> str:
"""Convert a Python type to TypeScript type string"""
@ -351,7 +383,11 @@ def process_pydantic_model(model_class, debug: bool = False) -> Dict[str, Any]:
print(f" Raw type: {field_type}")
# Check if this is a date field
if is_date_type(field_type):
is_date = is_date_type(field_type)
if debug:
print(f" 📅 Date type check for {ts_name}: {is_date} (type: {field_type})")
if is_date:
is_optional = is_field_optional(field_info, field_type, debug)
date_fields.append({
'name': ts_name,
@ -359,6 +395,8 @@ def process_pydantic_model(model_class, debug: bool = False) -> Dict[str, Any]:
})
if debug:
print(f" 🗓️ Date field detected: {ts_name} (optional: {is_optional})")
elif debug and ('date' in str(field_type).lower() or 'time' in str(field_type).lower()):
print(f" ⚠️ Field {ts_name} contains 'date'/'time' but not detected as date type: {field_type}")
ts_type = python_type_to_typescript(field_type, debug)
@ -396,7 +434,11 @@ def process_pydantic_model(model_class, debug: bool = False) -> Dict[str, Any]:
print(f" Raw type: {field_type}")
# Check if this is a date field
if is_date_type(field_type):
is_date = is_date_type(field_type)
if debug:
print(f" 📅 Date type check for {ts_name}: {is_date} (type: {field_type})")
if is_date:
is_optional = is_field_optional(field_info, field_type)
date_fields.append({
'name': ts_name,
@ -404,6 +446,8 @@ def process_pydantic_model(model_class, debug: bool = False) -> Dict[str, Any]:
})
if debug:
print(f" 🗓️ Date field detected: {ts_name} (optional: {is_optional})")
elif debug and ('date' in str(field_type).lower() or 'time' in str(field_type).lower()):
print(f" ⚠️ Field {ts_name} contains 'date'/'time' but not detected as date type: {field_type}")
ts_type = python_type_to_typescript(field_type, debug)
@ -471,6 +515,7 @@ def generate_conversion_functions(interfaces: List[Dict[str, Any]]) -> str:
func_lines = [
f"/**",
f" * Convert {interface_name} from API response, parsing date fields",
f" * Date fields: {', '.join([f['name'] for f in date_fields])}",
f" */",
f"export function {function_name}(data: any): {interface_name} {{",
f" if (!data) return data;",
@ -479,11 +524,14 @@ def generate_conversion_functions(interfaces: List[Dict[str, Any]]) -> str:
f" ...data,"
]
# Add date field conversions
# Add date field conversions with validation
for date_field in date_fields:
field_name = date_field['name']
is_optional = date_field['optional']
# Add a comment for clarity
func_lines.append(f" // Convert {field_name} from ISO string to Date")
if is_optional:
func_lines.append(f" {field_name}: data.{field_name} ? new Date(data.{field_name}) : undefined,")
else:
@ -502,7 +550,7 @@ def generate_conversion_functions(interfaces: List[Dict[str, Any]]) -> str:
# Generate the conversion functions section
result = [
"// ============================",
"// Date Conversion Functions",
"// Date Conversion Functions",
"// ============================",
"",
"// These functions convert API responses to properly typed objects",
@ -780,10 +828,27 @@ Generated conversion functions can be used like:
file_size = len(ts_content)
print(f"✅ TypeScript types generated: {args.output} ({file_size} characters)")
# Count conversion functions
# Count conversion functions and provide detailed feedback
conversion_count = ts_content.count('export function convert') - ts_content.count('convertFromApi') - ts_content.count('convertArrayFromApi')
if conversion_count > 0:
print(f"🗓️ Generated {conversion_count} date conversion functions")
if args.debug:
# Show which models have date conversion
models_with_dates = []
for line in ts_content.split('\n'):
if line.startswith('export function convert') and 'FromApi' in line and 'convertFromApi' not in line:
model_name = line.split('convert')[1].split('FromApi')[0]
models_with_dates.append(model_name)
if models_with_dates:
print(f" Models with date conversion: {', '.join(models_with_dates)}")
# Provide troubleshooting info if debug mode
if args.debug:
print(f"\n🐛 Debug mode was enabled. If you see incorrect date conversions:")
print(f" 1. Check the debug output above for '📅 Date type check' lines")
print(f" 2. Look for '⚠️' warnings about false positives")
print(f" 3. Verify your Pydantic model field types are correct")
print(f" 4. Re-run with --debug to see detailed type analysis")
# Step 5: Compile TypeScript (unless skipped)
if not args.skip_compile: