refactor(web): migrate profileService to orval-generated user client (v1.0.8 B3)
First real service migration post-scaffolding. Replaces raw apiClient
calls in @/features/profile/services/profileService.ts with the
orval-generated functions from services/generated/user/user.ts while
keeping every public function signature intact — no call sites touched.
Functions migrated (8):
- getProfile → getUsersId
- getProfileByUsername → getUsersByUsernameUsername
- updateProfile → putUsersId
- calculateProfileCompletion → getUsersIdCompletion
- followUser → postUsersIdFollow
- unfollowUser → deleteUsersIdFollow
- getSuggestions → getUsersSuggestions
- getUserReposts → getUsersIdReposts
Functions still on raw apiClient (endpoints lack swaggo annotations,
deferred v1.0.9):
- getFollowers → GET /users/{id}/followers
- getFollowing → GET /users/{id}/following
A small `unwrapProfile` helper normalises the two envelope shapes the
backend returns for profile endpoints ({profile: ...} vs the raw
object) so the public API stays identical.
Test file rewritten to mock the generated module (`services/generated/
user/user`) for migrated functions, with the apiClient mock retained
only for the two followers/following paths. 12/12 profileService
tests + 36/36 feature/profile suite green. npm run typecheck ✅.
Bisectable: revert this commit → tests return to apiClient-mocking
pattern, profileService.ts returns to raw apiClient. No data-shape
drift, no interceptor changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3ca9a2afec
commit
9325cd0e66
2 changed files with 124 additions and 71 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getProfile,
|
getProfile,
|
||||||
getProfileByUsername,
|
getProfileByUsername,
|
||||||
|
|
@ -12,9 +13,33 @@ import {
|
||||||
type UserProfile,
|
type UserProfile,
|
||||||
type UpdateProfileRequest,
|
type UpdateProfileRequest,
|
||||||
} from './profileService';
|
} from './profileService';
|
||||||
|
import {
|
||||||
|
getUsersId,
|
||||||
|
getUsersByUsernameUsername,
|
||||||
|
putUsersId,
|
||||||
|
getUsersIdCompletion,
|
||||||
|
postUsersIdFollow,
|
||||||
|
deleteUsersIdFollow,
|
||||||
|
} from '@/services/generated/user/user';
|
||||||
import { apiClient } from '@/services/api/client';
|
import { apiClient } from '@/services/api/client';
|
||||||
|
|
||||||
// Mock apiClient
|
// v1.0.8 B3 — profileService migrated to orval-generated calls. Mock the
|
||||||
|
// generated module so tests validate our wrapper logic (envelope
|
||||||
|
// unwrapping, default values) without hitting the transport layer.
|
||||||
|
// `followers` / `following` endpoints lack swaggo annotations so they
|
||||||
|
// still call apiClient directly — keep the apiClient mock for them.
|
||||||
|
|
||||||
|
vi.mock('@/services/generated/user/user', () => ({
|
||||||
|
getUsersId: vi.fn(),
|
||||||
|
getUsersByUsernameUsername: vi.fn(),
|
||||||
|
putUsersId: vi.fn(),
|
||||||
|
getUsersIdCompletion: vi.fn(),
|
||||||
|
postUsersIdFollow: vi.fn(),
|
||||||
|
deleteUsersIdFollow: vi.fn(),
|
||||||
|
getUsersSuggestions: vi.fn(),
|
||||||
|
getUsersIdReposts: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('@/services/api/client', () => ({
|
vi.mock('@/services/api/client', () => ({
|
||||||
apiClient: {
|
apiClient: {
|
||||||
get: vi.fn(),
|
get: vi.fn(),
|
||||||
|
|
@ -24,7 +49,16 @@ vi.mock('@/services/api/client', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const mockedApiClient = apiClient as {
|
const mockedGen = {
|
||||||
|
getUsersId: getUsersId as unknown as ReturnType<typeof vi.fn>,
|
||||||
|
getUsersByUsernameUsername: getUsersByUsernameUsername as unknown as ReturnType<typeof vi.fn>,
|
||||||
|
putUsersId: putUsersId as unknown as ReturnType<typeof vi.fn>,
|
||||||
|
getUsersIdCompletion: getUsersIdCompletion as unknown as ReturnType<typeof vi.fn>,
|
||||||
|
postUsersIdFollow: postUsersIdFollow as unknown as ReturnType<typeof vi.fn>,
|
||||||
|
deleteUsersIdFollow: deleteUsersIdFollow as unknown as ReturnType<typeof vi.fn>,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockedApiClient = apiClient as unknown as {
|
||||||
get: ReturnType<typeof vi.fn>;
|
get: ReturnType<typeof vi.fn>;
|
||||||
put: ReturnType<typeof vi.fn>;
|
put: ReturnType<typeof vi.fn>;
|
||||||
post: ReturnType<typeof vi.fn>;
|
post: ReturnType<typeof vi.fn>;
|
||||||
|
|
@ -53,14 +87,14 @@ describe('profileService', () => {
|
||||||
following_count: 5,
|
following_count: 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
mockedApiClient.get.mockResolvedValue({
|
// Orval response envelope is typically { data, status }; our
|
||||||
data: { profile: mockProfile },
|
// helper unwraps `.data` then the optional `profile` key.
|
||||||
});
|
mockedGen.getUsersId.mockResolvedValue({ profile: mockProfile });
|
||||||
|
|
||||||
const result = await getProfile('user-1');
|
const result = await getProfile('user-1');
|
||||||
|
|
||||||
expect(result).toEqual(mockProfile);
|
expect(result).toEqual(mockProfile);
|
||||||
expect(mockedApiClient.get).toHaveBeenCalledWith('/users/user-1');
|
expect(mockedGen.getUsersId).toHaveBeenCalledWith('user-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error on fetch failure', async () => {
|
it('should throw error on fetch failure', async () => {
|
||||||
|
|
@ -68,9 +102,9 @@ describe('profileService', () => {
|
||||||
mockError.response = {
|
mockError.response = {
|
||||||
status: 404,
|
status: 404,
|
||||||
data: { error: 'User not found' },
|
data: { error: 'User not found' },
|
||||||
} as any;
|
} as unknown as AxiosError['response'];
|
||||||
|
|
||||||
mockedApiClient.get.mockRejectedValue(mockError);
|
mockedGen.getUsersId.mockRejectedValue(mockError);
|
||||||
|
|
||||||
await expect(getProfile('invalid-user')).rejects.toThrow();
|
await expect(getProfile('invalid-user')).rejects.toThrow();
|
||||||
});
|
});
|
||||||
|
|
@ -91,16 +125,12 @@ describe('profileService', () => {
|
||||||
created_at: '2024-01-01T00:00:00Z',
|
created_at: '2024-01-01T00:00:00Z',
|
||||||
};
|
};
|
||||||
|
|
||||||
mockedApiClient.get.mockResolvedValue({
|
mockedGen.getUsersByUsernameUsername.mockResolvedValue({ profile: mockProfile });
|
||||||
data: { profile: mockProfile },
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await getProfileByUsername('testuser');
|
const result = await getProfileByUsername('testuser');
|
||||||
|
|
||||||
expect(result).toEqual(mockProfile);
|
expect(result).toEqual(mockProfile);
|
||||||
expect(mockedApiClient.get).toHaveBeenCalledWith(
|
expect(mockedGen.getUsersByUsernameUsername).toHaveBeenCalledWith('testuser');
|
||||||
'/users/by-username/testuser',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle response without profile wrapper', async () => {
|
it('should handle response without profile wrapper', async () => {
|
||||||
|
|
@ -117,9 +147,9 @@ describe('profileService', () => {
|
||||||
created_at: '2024-01-01T00:00:00Z',
|
created_at: '2024-01-01T00:00:00Z',
|
||||||
};
|
};
|
||||||
|
|
||||||
mockedApiClient.get.mockResolvedValue({
|
// Some endpoints skip the envelope and return the profile directly;
|
||||||
data: mockProfile,
|
// the service's unwrapProfile helper handles both.
|
||||||
});
|
mockedGen.getUsersByUsernameUsername.mockResolvedValue(mockProfile);
|
||||||
|
|
||||||
const result = await getProfileByUsername('testuser');
|
const result = await getProfileByUsername('testuser');
|
||||||
|
|
||||||
|
|
@ -148,17 +178,12 @@ describe('profileService', () => {
|
||||||
created_at: '2024-01-01T00:00:00Z',
|
created_at: '2024-01-01T00:00:00Z',
|
||||||
};
|
};
|
||||||
|
|
||||||
mockedApiClient.put.mockResolvedValue({
|
mockedGen.putUsersId.mockResolvedValue({ profile: mockUpdatedProfile });
|
||||||
data: { profile: mockUpdatedProfile },
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await updateProfile('user-1', updateData);
|
const result = await updateProfile('user-1', updateData);
|
||||||
|
|
||||||
expect(result).toEqual(mockUpdatedProfile);
|
expect(result).toEqual(mockUpdatedProfile);
|
||||||
expect(mockedApiClient.put).toHaveBeenCalledWith(
|
expect(mockedGen.putUsersId).toHaveBeenCalledWith('user-1', updateData);
|
||||||
'/users/user-1',
|
|
||||||
updateData,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error on update failure', async () => {
|
it('should throw error on update failure', async () => {
|
||||||
|
|
@ -166,9 +191,9 @@ describe('profileService', () => {
|
||||||
mockError.response = {
|
mockError.response = {
|
||||||
status: 400,
|
status: 400,
|
||||||
data: { error: 'Invalid data' },
|
data: { error: 'Invalid data' },
|
||||||
} as any;
|
} as unknown as AxiosError['response'];
|
||||||
|
|
||||||
mockedApiClient.put.mockRejectedValue(mockError);
|
mockedGen.putUsersId.mockRejectedValue(mockError);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
updateProfile('user-1', { username: 'invalid' }),
|
updateProfile('user-1', { username: 'invalid' }),
|
||||||
|
|
@ -183,16 +208,12 @@ describe('profileService', () => {
|
||||||
missing: ['bio', 'location'],
|
missing: ['bio', 'location'],
|
||||||
};
|
};
|
||||||
|
|
||||||
mockedApiClient.get.mockResolvedValue({
|
mockedGen.getUsersIdCompletion.mockResolvedValue(mockCompletion);
|
||||||
data: mockCompletion,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await calculateProfileCompletion('user-1');
|
const result = await calculateProfileCompletion('user-1');
|
||||||
|
|
||||||
expect(result).toEqual(mockCompletion);
|
expect(result).toEqual(mockCompletion);
|
||||||
expect(mockedApiClient.get).toHaveBeenCalledWith(
|
expect(mockedGen.getUsersIdCompletion).toHaveBeenCalledWith('user-1');
|
||||||
'/users/user-1/completion',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -203,14 +224,12 @@ describe('profileService', () => {
|
||||||
is_following: true,
|
is_following: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
mockedApiClient.post.mockResolvedValue({
|
mockedGen.postUsersIdFollow.mockResolvedValue(mockResponse);
|
||||||
data: mockResponse,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await followUser('user-2');
|
const result = await followUser('user-2');
|
||||||
|
|
||||||
expect(result).toEqual(mockResponse);
|
expect(result).toEqual(mockResponse);
|
||||||
expect(mockedApiClient.post).toHaveBeenCalledWith('/users/user-2/follow');
|
expect(mockedGen.postUsersIdFollow).toHaveBeenCalledWith('user-2');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error on follow failure', async () => {
|
it('should throw error on follow failure', async () => {
|
||||||
|
|
@ -218,9 +237,9 @@ describe('profileService', () => {
|
||||||
mockError.response = {
|
mockError.response = {
|
||||||
status: 400,
|
status: 400,
|
||||||
data: { error: 'Cannot follow yourself' },
|
data: { error: 'Cannot follow yourself' },
|
||||||
} as any;
|
} as unknown as AxiosError['response'];
|
||||||
|
|
||||||
mockedApiClient.post.mockRejectedValue(mockError);
|
mockedGen.postUsersIdFollow.mockRejectedValue(mockError);
|
||||||
|
|
||||||
await expect(followUser('user-1')).rejects.toThrow();
|
await expect(followUser('user-1')).rejects.toThrow();
|
||||||
});
|
});
|
||||||
|
|
@ -233,16 +252,12 @@ describe('profileService', () => {
|
||||||
is_following: false,
|
is_following: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
mockedApiClient.delete.mockResolvedValue({
|
mockedGen.deleteUsersIdFollow.mockResolvedValue(mockResponse);
|
||||||
data: mockResponse,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await unfollowUser('user-2');
|
const result = await unfollowUser('user-2');
|
||||||
|
|
||||||
expect(result).toEqual(mockResponse);
|
expect(result).toEqual(mockResponse);
|
||||||
expect(mockedApiClient.delete).toHaveBeenCalledWith(
|
expect(mockedGen.deleteUsersIdFollow).toHaveBeenCalledWith('user-2');
|
||||||
'/users/user-2/follow',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -260,9 +275,7 @@ describe('profileService', () => {
|
||||||
total: 1,
|
total: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
mockedApiClient.get.mockResolvedValue({
|
mockedApiClient.get.mockResolvedValue({ data: mockResponse });
|
||||||
data: mockResponse,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await getFollowers('user-1', 1, 20);
|
const result = await getFollowers('user-1', 1, 20);
|
||||||
|
|
||||||
|
|
@ -290,9 +303,7 @@ describe('profileService', () => {
|
||||||
total: 1,
|
total: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
mockedApiClient.get.mockResolvedValue({
|
mockedApiClient.get.mockResolvedValue({ data: mockResponse });
|
||||||
data: mockResponse,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await getFollowing('user-1', 1, 20);
|
const result = await getFollowing('user-1', 1, 20);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,24 @@
|
||||||
|
/**
|
||||||
|
* Profile service — thin wrapper over orval-generated user client.
|
||||||
|
* v1.0.8 B3 migration: replaced raw apiClient calls with generated
|
||||||
|
* functions from services/generated/user/user.ts. Same public API
|
||||||
|
* (function signatures + response shapes) so callers stay untouched.
|
||||||
|
*
|
||||||
|
* The orval calls funnel through vezaMutator → apiClient, preserving
|
||||||
|
* the existing interceptor chain (auth / CSRF / retry / offline-queue).
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
getUsersId,
|
||||||
|
putUsersId,
|
||||||
|
getUsersByUsernameUsername,
|
||||||
|
getUsersIdCompletion,
|
||||||
|
postUsersIdFollow,
|
||||||
|
deleteUsersIdFollow,
|
||||||
|
getUsersSuggestions,
|
||||||
|
getUsersIdReposts,
|
||||||
|
} from '@/services/generated/user/user';
|
||||||
import { apiClient } from '@/services/api/client';
|
import { apiClient } from '@/services/api/client';
|
||||||
|
|
||||||
import type { Track } from '@/features/tracks/types/track';
|
import type { Track } from '@/features/tracks/types/track';
|
||||||
|
|
||||||
export interface UserProfile {
|
export interface UserProfile {
|
||||||
|
|
@ -20,18 +40,31 @@ export interface UserProfile {
|
||||||
is_public?: boolean;
|
is_public?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Orval responses wrap the payload in `.data`. Our API client response
|
||||||
|
// interceptor flattens the success envelope, so the actual shape we
|
||||||
|
// receive is `{ profile: {...} }` (or sometimes the profile directly
|
||||||
|
// depending on the route). Helpers below normalise that ambiguity.
|
||||||
|
|
||||||
|
type ProfileEnvelope = UserProfile | { profile?: UserProfile };
|
||||||
|
|
||||||
|
const unwrapProfile = (raw: unknown): UserProfile => {
|
||||||
|
const env = raw as ProfileEnvelope;
|
||||||
|
if (env && typeof env === 'object' && 'profile' in env && env.profile) {
|
||||||
|
return env.profile as UserProfile;
|
||||||
|
}
|
||||||
|
return env as UserProfile;
|
||||||
|
};
|
||||||
|
|
||||||
export async function getProfile(userId: string): Promise<UserProfile> {
|
export async function getProfile(userId: string): Promise<UserProfile> {
|
||||||
const response = await apiClient.get(`/users/${userId}`);
|
const response = await getUsersId(userId);
|
||||||
return (response.data as { profile?: UserProfile })?.profile ?? (response.data as UserProfile);
|
return unwrapProfile((response as unknown as { data?: unknown })?.data ?? response);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getProfileByUsername(
|
export async function getProfileByUsername(
|
||||||
username: string,
|
username: string,
|
||||||
): Promise<UserProfile> {
|
): Promise<UserProfile> {
|
||||||
const encoded = encodeURIComponent(username);
|
const response = await getUsersByUsernameUsername(username);
|
||||||
const response = await apiClient.get(`/users/by-username/${encoded}`);
|
return unwrapProfile((response as unknown as { data?: unknown })?.data ?? response);
|
||||||
// Backend returns { profile: {...} }; client unwraps success/data so response.data is the payload
|
|
||||||
return response.data?.profile ?? response.data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateProfileRequest {
|
export interface UpdateProfileRequest {
|
||||||
|
|
@ -51,8 +84,11 @@ export async function updateProfile(
|
||||||
userId: string,
|
userId: string,
|
||||||
data: UpdateProfileRequest,
|
data: UpdateProfileRequest,
|
||||||
): Promise<UserProfile> {
|
): Promise<UserProfile> {
|
||||||
const response = await apiClient.put(`/users/${userId}`, data);
|
// Orval generated body type matches the JSON shape; cast to
|
||||||
return response.data.profile || response.data;
|
// Parameters[1] of putUsersId to keep the signature strict without
|
||||||
|
// repeating the types. Orval response carries data envelope.
|
||||||
|
const response = await putUsersId(userId, data as Parameters<typeof putUsersId>[1]);
|
||||||
|
return unwrapProfile((response as unknown as { data?: unknown })?.data ?? response);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProfileCompletion {
|
export interface ProfileCompletion {
|
||||||
|
|
@ -63,8 +99,9 @@ export interface ProfileCompletion {
|
||||||
export async function calculateProfileCompletion(
|
export async function calculateProfileCompletion(
|
||||||
userId: string,
|
userId: string,
|
||||||
): Promise<ProfileCompletion> {
|
): Promise<ProfileCompletion> {
|
||||||
const response = await apiClient.get(`/users/${userId}/completion`);
|
const response = await getUsersIdCompletion(userId);
|
||||||
return response.data;
|
const payload = (response as unknown as { data?: unknown })?.data ?? response;
|
||||||
|
return payload as ProfileCompletion;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FE-PAGE-010: Complete User Profile page implementation - Follow/Unfollow
|
// FE-PAGE-010: Complete User Profile page implementation - Follow/Unfollow
|
||||||
|
|
@ -75,15 +112,17 @@ export interface FollowUserResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function followUser(userId: string): Promise<FollowUserResponse> {
|
export async function followUser(userId: string): Promise<FollowUserResponse> {
|
||||||
const response = await apiClient.post(`/users/${userId}/follow`);
|
const response = await postUsersIdFollow(userId);
|
||||||
return response.data;
|
const payload = (response as unknown as { data?: unknown })?.data ?? response;
|
||||||
|
return payload as FollowUserResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function unfollowUser(
|
export async function unfollowUser(
|
||||||
userId: string,
|
userId: string,
|
||||||
): Promise<FollowUserResponse> {
|
): Promise<FollowUserResponse> {
|
||||||
const response = await apiClient.delete(`/users/${userId}/follow`);
|
const response = await deleteUsersIdFollow(userId);
|
||||||
return response.data;
|
const payload = (response as unknown as { data?: unknown })?.data ?? response;
|
||||||
|
return payload as FollowUserResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserFollowersResponse {
|
export interface UserFollowersResponse {
|
||||||
|
|
@ -96,6 +135,9 @@ export interface UserFollowersResponse {
|
||||||
total: number;
|
total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// /users/:id/followers and /users/:id/following endpoints still lack
|
||||||
|
// swaggo annotations on the backend (deferred v1.0.9). Kept as raw
|
||||||
|
// apiClient calls for now.
|
||||||
export async function getFollowers(
|
export async function getFollowers(
|
||||||
userId: string,
|
userId: string,
|
||||||
page: number = 1,
|
page: number = 1,
|
||||||
|
|
@ -141,8 +183,9 @@ export interface SuggestionsResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSuggestions(limit = 10): Promise<SuggestionsResponse> {
|
export async function getSuggestions(limit = 10): Promise<SuggestionsResponse> {
|
||||||
const response = await apiClient.get('/users/suggestions', { params: { limit } });
|
const response = await getUsersSuggestions({ limit });
|
||||||
const data = response.data as SuggestionsResponse;
|
const payload = (response as unknown as { data?: unknown })?.data ?? response;
|
||||||
|
const data = payload as Partial<SuggestionsResponse>;
|
||||||
return { suggestions: data?.suggestions ?? [] };
|
return { suggestions: data?.suggestions ?? [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -159,12 +202,11 @@ export async function getUserReposts(
|
||||||
limit = 20,
|
limit = 20,
|
||||||
offset = 0,
|
offset = 0,
|
||||||
): Promise<UserRepostsResponse> {
|
): Promise<UserRepostsResponse> {
|
||||||
const response = await apiClient.get(`/users/${userId}/reposts`, {
|
const response = await getUsersIdReposts(userId, { limit, offset });
|
||||||
params: { limit, offset },
|
const payload = (response as unknown as { data?: unknown })?.data ?? response;
|
||||||
});
|
const data = payload as Partial<UserRepostsResponse>;
|
||||||
const data = response.data as UserRepostsResponse;
|
|
||||||
return {
|
return {
|
||||||
tracks: data?.tracks ?? [],
|
tracks: (data?.tracks ?? []) as Track[],
|
||||||
total: data?.total ?? 0,
|
total: data?.total ?? 0,
|
||||||
limit: data?.limit ?? limit,
|
limit: data?.limit ?? limit,
|
||||||
offset: data?.offset ?? offset,
|
offset: data?.offset ?? offset,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue