Date conversion is working from localstorage and via API
This commit is contained in:
parent
adb407b19a
commit
4330bd4b7c
@ -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();
|
||||
|
@ -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 }}>
|
||||
|
@ -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 }
|
@ -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':
|
||||
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user