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 // Persistent Authentication Hook with localStorage Integration and Date Conversion
// Automatically restoring login state on page refresh // Automatically restoring login state on page refresh with proper date handling
import React, { createContext, useContext,useState, useCallback, useEffect, useRef } from 'react'; import React, { createContext, useContext,useState, useCallback, useEffect, useRef } from 'react';
import * as Types from 'types/types'; import * as Types from 'types/types';
import { useUser } from 'hooks/useUser'; import { useUser } from 'hooks/useUser';
import { CreateCandidateRequest, CreateEmployerRequest, CreateViewerRequest, LoginRequest } from 'services/api-client'; 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 { export interface AuthState {
user: Types.User | null; user: Types.User | null;
@ -61,10 +70,59 @@ function clearStoredAuth(): void {
localStorage.removeItem(TOKEN_STORAGE.TOKEN_EXPIRY); 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 { function storeAuthData(authResponse: Types.AuthResponse): void {
localStorage.setItem(TOKEN_STORAGE.ACCESS_TOKEN, authResponse.accessToken); localStorage.setItem(TOKEN_STORAGE.ACCESS_TOKEN, authResponse.accessToken);
localStorage.setItem(TOKEN_STORAGE.REFRESH_TOKEN, authResponse.refreshToken); 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()); localStorage.setItem(TOKEN_STORAGE.TOKEN_EXPIRY, authResponse.expiresAt.toString());
} }
@ -84,18 +142,31 @@ function getStoredAuthData(): {
try { try {
if (userDataStr) { if (userDataStr) {
userData = JSON.parse(userDataStr); userData = parseStoredUserData(userDataStr);
} }
if (expiryStr) { if (expiryStr) {
expiresAt = parseInt(expiryStr, 10); expiresAt = parseInt(expiryStr, 10);
} }
} catch (error) { } catch (error) {
console.error('Failed to parse stored auth data:', error); console.error('Failed to parse stored auth data:', error);
// Clear corrupted data
clearStoredAuth();
} }
return { accessToken, refreshToken, userData, expiresAt }; 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() { export function useSecureAuth() {
const [authState, setAuthState] = useState<AuthState>({ const [authState, setAuthState] = useState<AuthState>({
user: null, user: null,
@ -108,10 +179,20 @@ export function useSecureAuth() {
const {apiClient} = useUser(); const {apiClient} = useUser();
const initializationCompleted = useRef(false); const initializationCompleted = useRef(false);
// Token refresh function // Token refresh function with date conversion
const refreshAccessToken = useCallback(async (refreshToken: string): Promise<Types.AuthResponse | null> => { const refreshAccessToken = useCallback(async (refreshToken: string): Promise<Types.AuthResponse | null> => {
try { try {
const response = await apiClient.refreshToken(refreshToken); 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; return response;
} catch (error) { } catch (error) {
console.error('Token refresh failed:', error); console.error('Token refresh failed:', error);
@ -147,11 +228,11 @@ export function useSecureAuth() {
const refreshResult = await refreshAccessToken(stored.refreshToken); const refreshResult = await refreshAccessToken(stored.refreshToken);
if (refreshResult) { if (refreshResult) {
// Successfully refreshed // Successfully refreshed - store with proper date conversion
storeAuthData(refreshResult); storeAuthData(refreshResult);
apiClient.setAuthToken(refreshResult.accessToken); apiClient.setAuthToken(refreshResult.accessToken);
console.log("User =>", refreshResult.user); console.log("User (refreshed) =>", refreshResult.user);
setAuthState({ setAuthState({
user: refreshResult.user, user: refreshResult.user,
@ -161,7 +242,7 @@ export function useSecureAuth() {
error: null error: null
}); });
console.log('Token refreshed successfully'); console.log('Token refreshed successfully with date conversion');
} else { } else {
// Refresh failed, clear stored data // Refresh failed, clear stored data
console.log('Token refresh failed, clearing stored auth'); console.log('Token refresh failed, clearing stored auth');
@ -177,19 +258,19 @@ export function useSecureAuth() {
}); });
} }
} else { } else {
// Access token is still valid // Access token is still valid - user data already has date conversion applied
apiClient.setAuthToken(stored.accessToken); apiClient.setAuthToken(stored.accessToken);
console.log("User =>", stored.userData); console.log("User (from storage) =>", stored.userData);
setAuthState({ setAuthState({
user: stored.userData, user: stored.userData, // Already converted by parseStoredUserData
isAuthenticated: true, isAuthenticated: true,
isLoading: false, isLoading: false,
isInitializing: false, isInitializing: false,
error: null error: null
}); });
console.log('Restored authentication from stored tokens'); console.log('Restored authentication from stored tokens with date conversion');
} }
} catch (error) { } catch (error) {
console.error('Error initializing auth:', error); console.error('Error initializing auth:', error);
@ -250,7 +331,16 @@ export function useSecureAuth() {
try { try {
const authResponse = await apiClient.login(loginData); 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); storeAuthData(authResponse);
// Update API client with new token // Update API client with new token
@ -264,7 +354,7 @@ export function useSecureAuth() {
error: null error: null
}); });
console.log('Login successful'); console.log('Login successful with date conversion applied');
return true; return true;
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Login failed'; const errorMessage = error instanceof Error ? error.message : 'Login failed';
@ -295,6 +385,20 @@ export function useSecureAuth() {
console.log('User logged out'); console.log('User logged out');
}, [apiClient]); }, [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> => { const createViewerAccount = useCallback(async (viewerData: CreateViewerRequest): Promise<boolean> => {
setAuthState(prev => ({ ...prev, isLoading: true, error: null })); setAuthState(prev => ({ ...prev, isLoading: true, error: null }));
@ -316,7 +420,9 @@ export function useSecureAuth() {
throw new Error(usernameValidation.issues.join(', ')); throw new Error(usernameValidation.issues.join(', '));
} }
// Create viewer - API client automatically applies date conversion
const viewer = await apiClient.createViewer(viewerData); const viewer = await apiClient.createViewer(viewerData);
console.log('Viewer created with date conversion:', viewer);
// Auto-login after successful registration // Auto-login after successful registration
const loginSuccess = await login({ const loginSuccess = await login({
@ -357,7 +463,9 @@ export function useSecureAuth() {
throw new Error(usernameValidation.issues.join(', ')); throw new Error(usernameValidation.issues.join(', '));
} }
// Create candidate - API client automatically applies date conversion
const candidate = await apiClient.createCandidate(candidateData); const candidate = await apiClient.createCandidate(candidateData);
console.log('Candidate created with date conversion:', candidate);
// Auto-login after successful registration // Auto-login after successful registration
const loginSuccess = await login({ const loginSuccess = await login({
@ -398,7 +506,9 @@ export function useSecureAuth() {
throw new Error(usernameValidation.issues.join(', ')); throw new Error(usernameValidation.issues.join(', '));
} }
// Create employer - API client automatically applies date conversion
const employer = await apiClient.createEmployer(employerData); const employer = await apiClient.createEmployer(employerData);
console.log('Employer created with date conversion:', employer);
// Auto-login after successful registration // Auto-login after successful registration
const loginSuccess = await login({ const loginSuccess = await login({
@ -474,7 +584,8 @@ export function useSecureAuth() {
createCandidateAccount, createCandidateAccount,
createEmployerAccount, createEmployerAccount,
requestPasswordReset, requestPasswordReset,
refreshAuth refreshAuth,
updateUserData // New function to update user data with proper storage
}; };
} }
@ -509,7 +620,7 @@ export function useAuth() {
interface ProtectedRouteProps { interface ProtectedRouteProps {
children: React.ReactNode; children: React.ReactNode;
fallback?: React.ReactNode; fallback?: React.ReactNode;
requiredUserType?: 'candidate' | 'employer'; requiredUserType?: 'candidate' | 'employer' | 'viewer';
} }
export function ProtectedRoute({ 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() { function Header() {
const { user, isAuthenticated, logout, isInitializing } = useAuth(); const { user, isAuthenticated, logout, isInitializing } = useAuth();
@ -591,8 +702,16 @@ function Header() {
return ( return (
<header> <header>
{isAuthenticated ? ( {isAuthenticated ? (
<div>
<div> <div>
Welcome, {user?.firstName || user?.companyName}! 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> <button onClick={logout}>Logout</button>
</div> </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 // Auto-redirect based on auth state
function LoginPage() { function LoginPage() {
const { isAuthenticated, isInitializing } = useAuth(); 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 is logged in, show their profile
if (user) { if (user) {
return ( return (
@ -321,7 +320,7 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
</Grid> </Grid>
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
<Typography variant="body1" sx={{ mb: 1 }}> <Typography variant="body1" sx={{ mb: 1 }}>
<strong>Status:</strong> {user.status} {/* <strong>Status:</strong> {user.status} */}
</Typography> </Typography>
</Grid> </Grid>
<Grid size={{ xs: 12, md: 6 }}> <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 * 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) // Import generated types (from running generate_types.py)
@ -21,6 +22,19 @@ import {
PaginatedRequest PaginatedRequest
} from 'types/conversion'; } 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 // 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 // Authentication Methods
// ============================ // ============================
@ -145,6 +214,7 @@ class ApiClient {
body: JSON.stringify(formatApiRequest(request)) body: JSON.stringify(formatApiRequest(request))
}); });
// AuthResponse doesn't typically have date fields, use standard handler
return handleApiResponse<Types.AuthResponse>(response); 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> { async createViewer(request: CreateViewerRequest): Promise<Types.Viewer> {
@ -179,11 +249,11 @@ class ApiClient {
body: JSON.stringify(formatApiRequest(request)) 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> { async createCandidate(request: CreateCandidateRequest): Promise<Types.Candidate> {
@ -193,7 +263,7 @@ class ApiClient {
body: JSON.stringify(formatApiRequest(request)) body: JSON.stringify(formatApiRequest(request))
}); });
return handleApiResponse<Types.Candidate>(response); return this.handleApiResponseWithConversion<Types.Candidate>(response, 'Candidate');
} }
async getCandidate(username: string): Promise<Types.Candidate> { async getCandidate(username: string): Promise<Types.Candidate> {
@ -201,7 +271,7 @@ class ApiClient {
headers: this.defaultHeaders 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> { async updateCandidate(id: string, updates: Partial<Types.Candidate>): Promise<Types.Candidate> {
@ -211,7 +281,7 @@ class ApiClient {
body: JSON.stringify(formatApiRequest(updates)) 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>> { async getCandidates(request: Partial<PaginatedRequest> = {}): Promise<PaginatedResponse<Types.Candidate>> {
@ -222,7 +292,7 @@ class ApiClient {
headers: this.defaultHeaders 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>> { async searchCandidates(query: string, filters?: Record<string, any>): Promise<PaginatedResponse<Types.Candidate>> {
@ -238,11 +308,11 @@ class ApiClient {
headers: this.defaultHeaders 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> { async createEmployer(request: CreateEmployerRequest): Promise<Types.Employer> {
@ -252,7 +322,7 @@ class ApiClient {
body: JSON.stringify(formatApiRequest(request)) body: JSON.stringify(formatApiRequest(request))
}); });
return handleApiResponse<Types.Employer>(response); return this.handleApiResponseWithConversion<Types.Employer>(response, 'Employer');
} }
async getEmployer(id: string): Promise<Types.Employer> { async getEmployer(id: string): Promise<Types.Employer> {
@ -260,7 +330,7 @@ class ApiClient {
headers: this.defaultHeaders 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> { async updateEmployer(id: string, updates: Partial<Types.Employer>): Promise<Types.Employer> {
@ -270,11 +340,11 @@ class ApiClient {
body: JSON.stringify(formatApiRequest(updates)) 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> { async createJob(job: Omit<Types.Job, 'id' | 'datePosted' | 'views' | 'applicationCount'>): Promise<Types.Job> {
@ -284,7 +354,7 @@ class ApiClient {
body: JSON.stringify(formatApiRequest(job)) body: JSON.stringify(formatApiRequest(job))
}); });
return handleApiResponse<Types.Job>(response); return this.handleApiResponseWithConversion<Types.Job>(response, 'Job');
} }
async getJob(id: string): Promise<Types.Job> { async getJob(id: string): Promise<Types.Job> {
@ -292,7 +362,7 @@ class ApiClient {
headers: this.defaultHeaders headers: this.defaultHeaders
}); });
return handleApiResponse<Types.Job>(response); return this.handleApiResponseWithConversion<Types.Job>(response, 'Job');
} }
async getJobs(request: Partial<PaginatedRequest> = {}): Promise<PaginatedResponse<Types.Job>> { async getJobs(request: Partial<PaginatedRequest> = {}): Promise<PaginatedResponse<Types.Job>> {
@ -303,7 +373,7 @@ class ApiClient {
headers: this.defaultHeaders 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>> { async getJobsByEmployer(employerId: string, request: Partial<PaginatedRequest> = {}): Promise<PaginatedResponse<Types.Job>> {
@ -314,7 +384,7 @@ class ApiClient {
headers: this.defaultHeaders 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>> { async searchJobs(query: string, filters?: Record<string, any>): Promise<PaginatedResponse<Types.Job>> {
@ -330,11 +400,11 @@ class ApiClient {
headers: this.defaultHeaders 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> { async applyToJob(application: Omit<Types.JobApplication, 'id' | 'appliedDate' | 'updatedDate' | 'status'>): Promise<Types.JobApplication> {
@ -344,7 +414,7 @@ class ApiClient {
body: JSON.stringify(formatApiRequest(application)) body: JSON.stringify(formatApiRequest(application))
}); });
return handleApiResponse<Types.JobApplication>(response); return this.handleApiResponseWithConversion<Types.JobApplication>(response, 'JobApplication');
} }
async getJobApplication(id: string): Promise<Types.JobApplication> { async getJobApplication(id: string): Promise<Types.JobApplication> {
@ -352,7 +422,7 @@ class ApiClient {
headers: this.defaultHeaders headers: this.defaultHeaders
}); });
return handleApiResponse<Types.JobApplication>(response); return this.handleApiResponseWithConversion<Types.JobApplication>(response, 'JobApplication');
} }
async getJobApplications(request: Partial<PaginatedRequest> = {}): Promise<PaginatedResponse<Types.JobApplication>> { async getJobApplications(request: Partial<PaginatedRequest> = {}): Promise<PaginatedResponse<Types.JobApplication>> {
@ -363,7 +433,7 @@ class ApiClient {
headers: this.defaultHeaders 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> { async updateApplicationStatus(id: string, status: Types.ApplicationStatus): Promise<Types.JobApplication> {
@ -373,11 +443,11 @@ class ApiClient {
body: JSON.stringify(formatApiRequest({ status })) body: JSON.stringify(formatApiRequest({ status }))
}); });
return handleApiResponse<Types.JobApplication>(response); return this.handleApiResponseWithConversion<Types.JobApplication>(response, 'JobApplication');
} }
// ============================ // ============================
// Chat Methods // Chat Methods with Date Conversion
// ============================ // ============================
/** /**
@ -392,7 +462,7 @@ class ApiClient {
body: JSON.stringify(formatApiRequest(request)) 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 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 })) body: JSON.stringify(formatApiRequest({ context }))
}); });
return handleApiResponse<Types.ChatSession>(response); return this.handleApiResponseWithConversion<Types.ChatSession>(response, 'ChatSession');
} }
async getChatSession(id: string): Promise<Types.ChatSession> { async getChatSession(id: string): Promise<Types.ChatSession> {
@ -447,7 +525,7 @@ class ApiClient {
headers: this.defaultHeaders 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})) 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( sendMessageStream(
sessionId: string, sessionId: string,
@ -526,30 +604,35 @@ class ApiClient {
const data = line.slice(5).trim(); const data = line.slice(5).trim();
const incoming: Types.ChatMessageBase = JSON.parse(data); const incoming: Types.ChatMessageBase = JSON.parse(data);
// Convert date fields for incoming messages
const convertedIncoming = convertChatMessageFromApi(incoming);
// Trigger callbacks based on status // Trigger callbacks based on status
if (incoming.status !== chatMessage?.status) { if (convertedIncoming.status !== chatMessage?.status) {
options.onStatusChange?.(incoming.status); options.onStatusChange?.(convertedIncoming.status);
} }
// Handle different status types // Handle different status types
switch (incoming.status) { switch (convertedIncoming.status) {
case 'streaming': case 'streaming':
if (chatMessage === null) { if (chatMessage === null) {
chatMessage = {...incoming}; chatMessage = {...convertedIncoming};
} else { } else {
// Can't do a simple += as typescript thinks .content might not be there // 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; break;
case 'error': case 'error':
options.onError?.(incoming); options.onError?.(convertedIncoming);
break; break;
default: default:
chatMessageList.push(incoming); chatMessageList.push(convertedIncoming);
options.onMessage?.(incoming); options.onMessage?.(convertedIncoming);
break; 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( async getChatMessages(
sessionId: string, sessionId: string,
@ -621,7 +704,7 @@ class ApiClient {
headers: this.defaultHeaders headers: this.defaultHeaders
}); });
return handlePaginatedApiResponse<Types.ChatMessage>(response); return this.handlePaginatedApiResponseWithConversion<Types.ChatMessage>(response, 'ChatMessage');
} }
// ============================ // ============================
@ -712,10 +795,10 @@ class ApiClient {
// Error Handling Helper // Error Handling Helper
// ============================ // ============================
async handleRequest<T>(requestFn: () => Promise<Response>): Promise<T> { async handleRequest<T>(requestFn: () => Promise<Response>, modelType?: string): Promise<T> {
try { try {
const response = await requestFn(); const response = await requestFn();
return await handleApiResponse<T>(response); return await this.handleApiResponseWithConversion<T>(response, modelType);
} catch (error) { } catch (error) {
console.error('API request failed:', error); console.error('API request failed:', error);
throw 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'; import { useState, useEffect, useCallback, useRef } from 'react';
export function useStreamingChat(sessionId: string) { export function useStreamingChat(sessionId: string) {
@ -762,31 +845,39 @@ export function useStreamingChat(sessionId: string) {
const streamingOptions: StreamingOptions = { const streamingOptions: StreamingOptions = {
onMessage: (message) => { onMessage: (message) => {
// Message already has proper Date objects from conversion
setCurrentMessage(message); setCurrentMessage(message);
}, },
onPartialMessage: (content, messageId) => { onStreaming: (chunk) => {
// Chunk also has proper Date objects
setCurrentMessage(prev => prev ? setCurrentMessage(prev => prev ?
{ ...prev, content: prev.content + content } :
{ {
id: messageId || '', ...prev,
content: prev.content + chunk.content,
timestamp: chunk.timestamp // Update to latest timestamp
} :
{
id: chunk.id || '',
sessionId, sessionId,
status: 'streaming', status: 'streaming',
sender: 'ai', sender: 'ai',
content, content: chunk.content,
timestamp: new Date() timestamp: chunk.timestamp // Already a Date object
} }
); );
}, },
onStatusChange: (status) => { onStatusChange: (status) => {
setCurrentMessage(prev => prev ? { ...prev, status } : null); setCurrentMessage(prev => prev ? { ...prev, status } : null);
}, },
onComplete: (finalMessage) => { onComplete: () => {
setMessages(prev => [...prev, finalMessage]); if (currentMessage) {
setMessages(prev => [...prev, currentMessage]);
}
setCurrentMessage(null); setCurrentMessage(null);
setIsStreaming(false); setIsStreaming(false);
}, },
onError: (err) => { onError: (err) => {
setError(err.message); setError(typeof err === 'string' ? err : err.content);
setIsStreaming(false); setIsStreaming(false);
setCurrentMessage(null); setCurrentMessage(null);
} }
@ -799,7 +890,7 @@ export function useStreamingChat(sessionId: string) {
setError(err instanceof Error ? err.message : 'Failed to send message'); setError(err instanceof Error ? err.message : 'Failed to send message');
setIsStreaming(false); setIsStreaming(false);
} }
}, [sessionId, apiClient]); }, [sessionId, apiClient, currentMessage]);
const cancelStreaming = useCallback(() => { const cancelStreaming = useCallback(() => {
if (streamingRef.current) { 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 }) { function ChatInterface({ sessionId }: { sessionId: string }) {
const { const {
messages, messages,
@ -839,15 +930,27 @@ function ChatInterface({ sessionId }: { sessionId: string }) {
<div className="messages"> <div className="messages">
{messages.map(message => ( {messages.map(message => (
<div key={message.id}> <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> </div>
))} ))}
{currentMessage && ( {currentMessage && (
<div className="current-message"> <div className="current-message">
<strong>{currentMessage.sender}:</strong> {currentMessage.content} <div className="message-header">
<strong>{currentMessage.sender}:</strong>
<span className="timestamp">
{currentMessage.timestamp.toLocaleTimeString()}
</span>
{isStreaming && <span className="streaming-indicator">...</span>} {isStreaming && <span className="streaming-indicator">...</span>}
</div> </div>
<div className="message-content">{currentMessage.content}</div>
</div>
)} )}
</div> </div>
@ -874,61 +977,96 @@ function ChatInterface({ sessionId }: { sessionId: string }) {
*/ */
// ============================ // ============================
// Usage Examples // Usage Examples with Date Conversion
// ============================ // ============================
/* /*
// Initialize API client // Initialize API client
const apiClient = new ApiClient(); 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 { try {
const message = await apiClient.sendMessage(sessionId, 'Hello, how are you?'); const candidate = await apiClient.createCandidate({
console.log('Response:', message.content); 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) { } catch (error) {
console.error('Failed to send message:', error); console.error('Failed to create candidate:', error);
} }
// Streaming message with callbacks // Get jobs with proper date conversion
const streamResponse = apiClient.sendMessageStream(sessionId, 'Tell me a long story', { try {
onPartialMessage: (content, messageId) => { const jobs = await apiClient.getJobs({ limit: 10 });
console.log('Partial content:', content);
// Update UI with partial content 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) => { onMessage: (message) => {
console.log('Status changed:', status); // message.timestamp is a Date object
// Update UI status indicator console.log(`Final message at ${message.timestamp.toLocaleTimeString()}:`, message.content);
}, },
onComplete: (finalMessage) => { onComplete: () => {
console.log('Final message:', finalMessage.content); console.log('Streaming completed');
// Handle completed message
},
onError: (error) => {
console.error('Streaming error:', error);
// Handle error
} }
}); });
// Can cancel the stream if needed // Chat sessions with date conversion
setTimeout(() => {
streamResponse.cancel();
}, 10000); // Cancel after 10 seconds
// Wait for completion
try { try {
const finalMessage = await streamResponse.promise; const chatSession = await apiClient.createChatSession({
console.log('Stream completed:', finalMessage); 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) { } catch (error) {
console.error('Stream failed:', error); console.error('Failed to create chat session:', error);
} }
// Auto-detection: streaming if callbacks provided, standard otherwise // Get chat messages with date conversion
await apiClient.sendMessageAuto(sessionId, 'Quick question', { try {
onPartialMessage: (content) => console.log('Streaming:', content) const messages = await apiClient.getChatMessages(sessionId);
}); // Will use streaming
await apiClient.sendMessageAuto(sessionId, 'Quick question'); // Will use standard 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 { ApiClient }
export type { StreamingOptions, StreamingResponse }; export type { StreamingOptions, StreamingResponse }

View File

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

View File

@ -101,10 +101,42 @@ def is_date_type(python_type: Any) -> bool:
if python_type == datetime: if python_type == datetime:
return True 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) 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: def python_type_to_typescript(python_type: Any, debug: bool = False) -> str:
"""Convert a Python type to TypeScript type string""" """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}") print(f" Raw type: {field_type}")
# Check if this is a date field # 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) is_optional = is_field_optional(field_info, field_type, debug)
date_fields.append({ date_fields.append({
'name': ts_name, 'name': ts_name,
@ -359,6 +395,8 @@ def process_pydantic_model(model_class, debug: bool = False) -> Dict[str, Any]:
}) })
if debug: if debug:
print(f" 🗓️ Date field detected: {ts_name} (optional: {is_optional})") 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) 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}") print(f" Raw type: {field_type}")
# Check if this is a date field # 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) is_optional = is_field_optional(field_info, field_type)
date_fields.append({ date_fields.append({
'name': ts_name, 'name': ts_name,
@ -404,6 +446,8 @@ def process_pydantic_model(model_class, debug: bool = False) -> Dict[str, Any]:
}) })
if debug: if debug:
print(f" 🗓️ Date field detected: {ts_name} (optional: {is_optional})") 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) 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 = [ func_lines = [
f"/**", f"/**",
f" * Convert {interface_name} from API response, parsing date fields", f" * Convert {interface_name} from API response, parsing date fields",
f" * Date fields: {', '.join([f['name'] for f in date_fields])}",
f" */", f" */",
f"export function {function_name}(data: any): {interface_name} {{", f"export function {function_name}(data: any): {interface_name} {{",
f" if (!data) return data;", f" if (!data) return data;",
@ -479,11 +524,14 @@ def generate_conversion_functions(interfaces: List[Dict[str, Any]]) -> str:
f" ...data," f" ...data,"
] ]
# Add date field conversions # Add date field conversions with validation
for date_field in date_fields: for date_field in date_fields:
field_name = date_field['name'] field_name = date_field['name']
is_optional = date_field['optional'] 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: if is_optional:
func_lines.append(f" {field_name}: data.{field_name} ? new Date(data.{field_name}) : undefined,") func_lines.append(f" {field_name}: data.{field_name} ? new Date(data.{field_name}) : undefined,")
else: else:
@ -780,10 +828,27 @@ Generated conversion functions can be used like:
file_size = len(ts_content) file_size = len(ts_content)
print(f"✅ TypeScript types generated: {args.output} ({file_size} characters)") 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') conversion_count = ts_content.count('export function convert') - ts_content.count('convertFromApi') - ts_content.count('convertArrayFromApi')
if conversion_count > 0: if conversion_count > 0:
print(f"🗓️ Generated {conversion_count} date conversion functions") 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) # Step 5: Compile TypeScript (unless skipped)
if not args.skip_compile: if not args.skip_compile: