refactor(client): unwrap .data from TransformHttpResponseInterceptor

The server-side TransformHttpResponseInterceptor wraps every body in
{ data, success, status }. The axios interceptor only unwraps the
transport layer, so calls return that envelope object — read .data
to get the actual payload.

Aligns the remaining Acadenice REST clients (backlinks, clipper,
slash-commands, sync-blocks, templates, graph, rbac) with the
existing convention and drops a few hardcoded /api/v1 prefixes that
duplicated the api baseURL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Corentin JOGUET 2026-05-11 09:54:32 +00:00
parent 843986d5c2
commit 47dee1eb12
7 changed files with 101 additions and 102 deletions

View file

@ -28,7 +28,7 @@ export interface BacklinksResult {
async function fetchBacklinks(pageId: string): Promise<BacklinksResult> { async function fetchBacklinks(pageId: string): Promise<BacklinksResult> {
const res = await api.get<BacklinksResult>( const res = await api.get<BacklinksResult>(
`/acadenice/pages/${pageId}/backlinks`, `/v1/pages/${pageId}/backlinks`,
); );
return res.data; return res.data;
} }

View file

@ -1,6 +1,6 @@
import axios from 'axios'; import api from '@/lib/api-client';
const BASE = '/api/v1/clipper'; const BASE = '/v1/clipper';
export interface ClipperTokenInfo { export interface ClipperTokenInfo {
id: string; id: string;
@ -23,17 +23,17 @@ export interface CreateTokenResponse {
} }
export const clipperClient = { export const clipperClient = {
listTokens(): Promise<ClipperTokenInfo[]> { async listTokens(): Promise<ClipperTokenInfo[]> {
return axios.get<ClipperTokenInfo[]>(`${BASE}/tokens`).then((r) => r.data); const r = await api.get<ClipperTokenInfo[]>(`${BASE}/tokens`);
return r.data;
}, },
createToken(payload: CreateTokenPayload): Promise<CreateTokenResponse> { async createToken(payload: CreateTokenPayload): Promise<CreateTokenResponse> {
return axios const r = await api.post<CreateTokenResponse>(`${BASE}/tokens`, payload);
.post<CreateTokenResponse>(`${BASE}/tokens`, payload) return r.data;
.then((r) => r.data);
}, },
revokeToken(tokenId: string): Promise<void> { async revokeToken(tokenId: string): Promise<void> {
return axios.delete(`${BASE}/tokens/${tokenId}`).then(() => undefined); await api.delete(`${BASE}/tokens/${tokenId}`);
}, },
}; };

View file

@ -69,5 +69,6 @@ export async function fetchGraph(
const qs = new URLSearchParams(query).toString(); const qs = new URLSearchParams(query).toString();
const url = qs ? `/v1/graph?${qs}` : "/v1/graph"; const url = qs ? `/v1/graph?${qs}` : "/v1/graph";
return api.get(url) as unknown as Promise<GraphResponse>; const r = await api.get<GraphResponse>(url);
return r.data;
} }

View file

@ -9,57 +9,44 @@ import {
IMyPermissionsResponse, IMyPermissionsResponse,
} from "@/features/acadenice/rbac/types/rbac.types"; } from "@/features/acadenice/rbac/types/rbac.types";
/** // The global `TransformHttpResponseInterceptor` wraps every body in
* REST client for the Acadenice RBAC API (R2.1 backend). // `{ data, success, status }`. The axios interceptor already unwraps the
* Endpoints under /api/v1 relative to api.baseURL ("/api"). // transport layer, so each call returns that wrap object — we read `.data`
* // to get the actual payload.
* Note : Docmost's axios interceptor returns `response.data` directly, so the
* return value of `api.get(...)` is already the body payload.
*/
export async function getPermissionsCatalog(): Promise<IPermissionDescriptor[]> { export async function getPermissionsCatalog(): Promise<IPermissionDescriptor[]> {
return api.get("/v1/permissions") as unknown as Promise< const r = await api.get<IPermissionDescriptor[]>("/v1/permissions");
IPermissionDescriptor[] return r.data;
>;
} }
/**
* Fetches the effective permissions of the authenticated user in the current
* workspace. Backed by the Redis 60s cache server-side (R2.1).
*/
export async function getMyPermissions(): Promise<IMyPermissionsResponse> { export async function getMyPermissions(): Promise<IMyPermissionsResponse> {
return api.get( const r = await api.get<IMyPermissionsResponse>("/v1/permissions/me");
"/v1/permissions/me", return r.data;
) as unknown as Promise<IMyPermissionsResponse>;
} }
export async function listRoles(): Promise<IRole[]> { export async function listRoles(): Promise<IRole[]> {
return api.get("/v1/roles") as unknown as Promise<IRole[]>; const r = await api.get<IRole[]>("/v1/roles");
return r.data;
} }
export async function getRole(roleId: string): Promise<IRoleWithPermissions> { export async function getRole(roleId: string): Promise<IRoleWithPermissions> {
return api.get( const r = await api.get<IRoleWithPermissions>(`/v1/roles/${roleId}`);
`/v1/roles/${roleId}`, return r.data;
) as unknown as Promise<IRoleWithPermissions>;
} }
export async function createRole( export async function createRole(
payload: ICreateRolePayload, payload: ICreateRolePayload,
): Promise<IRoleWithPermissions> { ): Promise<IRoleWithPermissions> {
return api.post( const r = await api.post<IRoleWithPermissions>("/v1/roles", payload);
"/v1/roles", return r.data;
payload,
) as unknown as Promise<IRoleWithPermissions>;
} }
export async function updateRole( export async function updateRole(
roleId: string, roleId: string,
payload: IUpdateRolePayload, payload: IUpdateRolePayload,
): Promise<IRole> { ): Promise<IRole> {
return api.patch( const r = await api.patch<IRole>(`/v1/roles/${roleId}`, payload);
`/v1/roles/${roleId}`, return r.data;
payload,
) as unknown as Promise<IRole>;
} }
export async function deleteRole(roleId: string): Promise<void> { export async function deleteRole(roleId: string): Promise<void> {
@ -70,26 +57,30 @@ export async function setRolePermissions(
roleId: string, roleId: string,
permissions: string[], permissions: string[],
): Promise<IRoleWithPermissions> { ): Promise<IRoleWithPermissions> {
return api.put(`/v1/roles/${roleId}/permissions`, { const r = await api.put<IRoleWithPermissions>(
permissions, `/v1/roles/${roleId}/permissions`,
}) as unknown as Promise<IRoleWithPermissions>; { permissions },
);
return r.data;
} }
export async function listUserRoles( export async function listUserRoles(
userId: string, userId: string,
): Promise<IUserRoleAssignment[]> { ): Promise<IUserRoleAssignment[]> {
return api.get(`/v1/users/${userId}/roles`) as unknown as Promise< const r = await api.get<IUserRoleAssignment[]>(
IUserRoleAssignment[] `/v1/users/${userId}/roles`,
>; );
return r.data;
} }
export async function assignRolesToUser( export async function assignRolesToUser(
userId: string, userId: string,
roleIds: string[], roleIds: string[],
): Promise<{ ok: true }> { ): Promise<{ ok: true }> {
return api.post(`/v1/users/${userId}/roles`, { const r = await api.post<{ ok: true }>(`/v1/users/${userId}/roles`, {
roleIds, roleIds,
}) as unknown as Promise<{ ok: true }>; });
return r.data;
} }
export async function unassignRoleFromUser( export async function unassignRoleFromUser(

View file

@ -1,7 +1,4 @@
import axios from 'axios'; import api from '@/lib/api-client';
// Re-use the same axios instance that Docmost uses for authenticated requests.
// The `withCredentials` is handled globally by the Docmost axios setup.
export interface SlashCommandDto { export interface SlashCommandDto {
id: string; id: string;
@ -35,34 +32,35 @@ export interface CreateSlashCommandPayload {
export type UpdateSlashCommandPayload = Partial<CreateSlashCommandPayload>; export type UpdateSlashCommandPayload = Partial<CreateSlashCommandPayload>;
const BASE = '/api/v1/slash-commands'; const BASE = '/v1/slash-commands';
export const slashCommandsClient = { export const slashCommandsClient = {
list(): Promise<SlashCommandDto[]> { async list(): Promise<SlashCommandDto[]> {
return axios.get<SlashCommandDto[]>(BASE).then((r) => r.data); const r = await api.get<SlashCommandDto[]>(BASE);
return r.data;
}, },
get(id: string): Promise<SlashCommandDto> { async get(id: string): Promise<SlashCommandDto> {
return axios.get<SlashCommandDto>(`${BASE}/${id}`).then((r) => r.data); const r = await api.get<SlashCommandDto>(`${BASE}/${id}`);
return r.data;
}, },
create(payload: CreateSlashCommandPayload): Promise<SlashCommandDto> { async create(payload: CreateSlashCommandPayload): Promise<SlashCommandDto> {
return axios.post<SlashCommandDto>(BASE, payload).then((r) => r.data); const r = await api.post<SlashCommandDto>(BASE, payload);
return r.data;
}, },
update(id: string, payload: UpdateSlashCommandPayload): Promise<SlashCommandDto> { async update(id: string, payload: UpdateSlashCommandPayload): Promise<SlashCommandDto> {
return axios const r = await api.patch<SlashCommandDto>(`${BASE}/${id}`, payload);
.patch<SlashCommandDto>(`${BASE}/${id}`, payload) return r.data;
.then((r) => r.data);
}, },
delete(id: string): Promise<void> { async delete(id: string): Promise<void> {
return axios.delete(`${BASE}/${id}`).then(() => undefined); await api.delete(`${BASE}/${id}`);
}, },
toggle(id: string, isEnabled: boolean): Promise<SlashCommandDto> { async toggle(id: string, isEnabled: boolean): Promise<SlashCommandDto> {
return axios const r = await api.patch<SlashCommandDto>(`${BASE}/${id}`, { isEnabled });
.patch<SlashCommandDto>(`${BASE}/${id}`, { isEnabled }) return r.data;
.then((r) => r.data);
}, },
}; };

View file

@ -1,4 +1,4 @@
import axios from 'axios'; import api from '@/lib/api-client';
export interface SyncBlockDto { export interface SyncBlockDto {
id: string; id: string;
@ -17,26 +17,30 @@ export interface SyncBlockUsageDto {
workspaceId: string; workspaceId: string;
} }
const BASE = '/api/v1/sync-blocks'; const BASE = '/v1/sync-blocks';
export const syncBlocksClient = { export const syncBlocksClient = {
create(content: Record<string, unknown> = {}): Promise<SyncBlockDto> { async create(content: Record<string, unknown> = {}): Promise<SyncBlockDto> {
return axios.post<SyncBlockDto>(BASE, { content }).then((r) => r.data); const r = await api.post<SyncBlockDto>(BASE, { content });
return r.data;
}, },
get(id: string): Promise<SyncBlockDto> { async get(id: string): Promise<SyncBlockDto> {
return axios.get<SyncBlockDto>(`${BASE}/${id}`).then((r) => r.data); const r = await api.get<SyncBlockDto>(`${BASE}/${id}`);
return r.data;
}, },
update(id: string, content: Record<string, unknown>): Promise<SyncBlockDto> { async update(id: string, content: Record<string, unknown>): Promise<SyncBlockDto> {
return axios.patch<SyncBlockDto>(`${BASE}/${id}`, { content }).then((r) => r.data); const r = await api.patch<SyncBlockDto>(`${BASE}/${id}`, { content });
return r.data;
}, },
delete(id: string): Promise<void> { async delete(id: string): Promise<void> {
return axios.delete(`${BASE}/${id}`).then(() => undefined); await api.delete(`${BASE}/${id}`);
}, },
usages(id: string): Promise<SyncBlockUsageDto[]> { async usages(id: string): Promise<SyncBlockUsageDto[]> {
return axios.get<SyncBlockUsageDto[]>(`${BASE}/${id}/usages`).then((r) => r.data); const r = await api.get<SyncBlockUsageDto[]>(`${BASE}/${id}/usages`);
return r.data;
}, },
}; };

View file

@ -1,4 +1,4 @@
import axios from 'axios'; import api from '@/lib/api-client';
export interface TemplateDto { export interface TemplateDto {
id: string; id: string;
@ -36,41 +36,46 @@ export interface InstantiatePayload {
name?: string; name?: string;
} }
const BASE = '/api/v1/templates'; const BASE = '/v1/templates';
export const templatesClient = { export const templatesClient = {
list(opts: { category?: string; search?: string } = {}): Promise<TemplateDto[]> { async list(opts: { category?: string; search?: string } = {}): Promise<TemplateDto[]> {
return axios const r = await api.get<TemplateDto[]>(BASE, { params: opts });
.get<TemplateDto[]>(BASE, { params: opts }) return r.data;
.then((r) => r.data);
}, },
get(id: string): Promise<TemplateDto> { async get(id: string): Promise<TemplateDto> {
return axios.get<TemplateDto>(`${BASE}/${id}`).then((r) => r.data); const r = await api.get<TemplateDto>(`${BASE}/${id}`);
return r.data;
}, },
create(payload: CreateTemplatePayload): Promise<TemplateDto> { async create(payload: CreateTemplatePayload): Promise<TemplateDto> {
return axios.post<TemplateDto>(BASE, payload).then((r) => r.data); const r = await api.post<TemplateDto>(BASE, payload);
return r.data;
}, },
update(id: string, payload: UpdateTemplatePayload): Promise<TemplateDto> { async update(id: string, payload: UpdateTemplatePayload): Promise<TemplateDto> {
return axios.patch<TemplateDto>(`${BASE}/${id}`, payload).then((r) => r.data); const r = await api.patch<TemplateDto>(`${BASE}/${id}`, payload);
return r.data;
}, },
delete(id: string): Promise<void> { async delete(id: string): Promise<void> {
return axios.delete(`${BASE}/${id}`).then(() => undefined); await api.delete(`${BASE}/${id}`);
}, },
instantiate( async instantiate(
id: string, id: string,
payload: InstantiatePayload, payload: InstantiatePayload,
): Promise<{ pageId: string; slugId: string }> { ): Promise<{ pageId: string; slugId: string }> {
return axios const r = await api.post<{ pageId: string; slugId: string }>(
.post<{ pageId: string; slugId: string }>(`${BASE}/${id}/instantiate`, payload) `${BASE}/${id}/instantiate`,
.then((r) => r.data); payload,
);
return r.data;
}, },
setDefault(id: string): Promise<TemplateDto> { async setDefault(id: string): Promise<TemplateDto> {
return axios.patch<TemplateDto>(`${BASE}/${id}/default`).then((r) => r.data); const r = await api.patch<TemplateDto>(`${BASE}/${id}/default`);
return r.data;
}, },
}; };