fix(settings): extract shared response mappers to prevent server/client shape drift

Addresses PR review feedback — prefetch.ts duplicated response mapping logic from client hooks. Extracted mapGeneralSettingsResponse and mapUserProfileResponse as shared functions used by both client fetch and server prefetch.
This commit is contained in:
waleed
2026-03-09 20:21:43 -07:00
parent ab61f5188c
commit 63927e5afc
3 changed files with 39 additions and 42 deletions

View File

@@ -1,8 +1,8 @@
import type { QueryClient } from '@tanstack/react-query'
import { headers } from 'next/headers'
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
import { generalSettingsKeys } from '@/hooks/queries/general-settings'
import { userProfileKeys } from '@/hooks/queries/user-profile'
import { generalSettingsKeys, mapGeneralSettingsResponse } from '@/hooks/queries/general-settings'
import { mapUserProfileResponse, userProfileKeys } from '@/hooks/queries/user-profile'
/**
* Forwards incoming request cookies so server-side API fetches authenticate correctly.
@@ -29,17 +29,7 @@ export function prefetchGeneralSettings(queryClient: QueryClient) {
})
if (!response.ok) throw new Error(`Settings prefetch failed: ${response.status}`)
const { data } = await response.json()
return {
autoConnect: data.autoConnect ?? true,
showTrainingControls: data.showTrainingControls ?? false,
superUserModeEnabled: data.superUserModeEnabled ?? true,
theme: data.theme || 'system',
telemetryEnabled: data.telemetryEnabled ?? true,
billingUsageNotificationsEnabled: data.billingUsageNotificationsEnabled ?? true,
errorNotificationsEnabled: data.errorNotificationsEnabled ?? true,
snapToGridSize: data.snapToGridSize ?? 0,
showActionBar: data.showActionBar ?? true,
}
return mapGeneralSettingsResponse(data)
},
staleTime: 60 * 60 * 1000,
})
@@ -61,14 +51,7 @@ export function prefetchUserProfile(queryClient: QueryClient) {
})
if (!response.ok) throw new Error(`Profile prefetch failed: ${response.status}`)
const { user } = await response.json()
return {
id: user.id,
name: user.name || '',
email: user.email || '',
image: user.image || null,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
}
return mapUserProfileResponse(user)
},
staleTime: 5 * 60 * 1000,
})

View File

@@ -28,6 +28,24 @@ export interface GeneralSettings {
showActionBar: boolean
}
/**
* Map raw API response data to GeneralSettings with defaults.
* Shared by both client fetch and server prefetch to prevent shape drift.
*/
export function mapGeneralSettingsResponse(data: Record<string, unknown>): GeneralSettings {
return {
autoConnect: (data.autoConnect as boolean) ?? true,
showTrainingControls: (data.showTrainingControls as boolean) ?? false,
superUserModeEnabled: (data.superUserModeEnabled as boolean) ?? true,
theme: (data.theme as GeneralSettings['theme']) || 'system',
telemetryEnabled: (data.telemetryEnabled as boolean) ?? true,
billingUsageNotificationsEnabled: (data.billingUsageNotificationsEnabled as boolean) ?? true,
errorNotificationsEnabled: (data.errorNotificationsEnabled as boolean) ?? true,
snapToGridSize: (data.snapToGridSize as number) ?? 0,
showActionBar: (data.showActionBar as boolean) ?? true,
}
}
/**
* Fetch general settings from API
*/
@@ -39,18 +57,7 @@ async function fetchGeneralSettings(signal?: AbortSignal): Promise<GeneralSettin
}
const { data } = await response.json()
return {
autoConnect: data.autoConnect ?? true,
showTrainingControls: data.showTrainingControls ?? false,
superUserModeEnabled: data.superUserModeEnabled ?? true,
theme: data.theme || 'system',
telemetryEnabled: data.telemetryEnabled ?? true,
billingUsageNotificationsEnabled: data.billingUsageNotificationsEnabled ?? true,
errorNotificationsEnabled: data.errorNotificationsEnabled ?? true,
snapToGridSize: data.snapToGridSize ?? 0,
showActionBar: data.showActionBar ?? true,
}
return mapGeneralSettingsResponse(data)
}
/**

View File

@@ -24,6 +24,21 @@ export interface UserProfile {
updatedAt: string
}
/**
* Map raw API response user object to UserProfile.
* Shared by both client fetch and server prefetch to prevent shape drift.
*/
export function mapUserProfileResponse(user: Record<string, unknown>): UserProfile {
return {
id: user.id as string,
name: (user.name as string) || '',
email: (user.email as string) || '',
image: (user.image as string) || null,
createdAt: user.createdAt as string,
updatedAt: user.updatedAt as string,
}
}
/**
* Fetch user profile from API
*/
@@ -35,15 +50,7 @@ async function fetchUserProfile(signal?: AbortSignal): Promise<UserProfile> {
}
const { user } = await response.json()
return {
id: user.id,
name: user.name || '',
email: user.email || '',
image: user.image || null,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
}
return mapUserProfileResponse(user)
}
/**