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> {
const res = await api.get<BacklinksResult>(
`/acadenice/pages/${pageId}/backlinks`,
`/v1/pages/${pageId}/backlinks`,
);
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 {
id: string;
@ -23,17 +23,17 @@ export interface CreateTokenResponse {
}
export const clipperClient = {
listTokens(): Promise<ClipperTokenInfo[]> {
return axios.get<ClipperTokenInfo[]>(`${BASE}/tokens`).then((r) => r.data);
async listTokens(): Promise<ClipperTokenInfo[]> {
const r = await api.get<ClipperTokenInfo[]>(`${BASE}/tokens`);
return r.data;
},
createToken(payload: CreateTokenPayload): Promise<CreateTokenResponse> {
return axios
.post<CreateTokenResponse>(`${BASE}/tokens`, payload)
.then((r) => r.data);
async createToken(payload: CreateTokenPayload): Promise<CreateTokenResponse> {
const r = await api.post<CreateTokenResponse>(`${BASE}/tokens`, payload);
return r.data;
},
revokeToken(tokenId: string): Promise<void> {
return axios.delete(`${BASE}/tokens/${tokenId}`).then(() => undefined);
async revokeToken(tokenId: string): Promise<void> {
await api.delete(`${BASE}/tokens/${tokenId}`);
},
};

View file

@ -69,5 +69,6 @@ export async function fetchGraph(
const qs = new URLSearchParams(query).toString();
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,
} from "@/features/acadenice/rbac/types/rbac.types";
/**
* REST client for the Acadenice RBAC API (R2.1 backend).
* Endpoints under /api/v1 relative to api.baseURL ("/api").
*
* Note : Docmost's axios interceptor returns `response.data` directly, so the
* return value of `api.get(...)` is already the body payload.
*/
// The global `TransformHttpResponseInterceptor` wraps every body in
// `{ data, success, status }`. The axios interceptor already unwraps the
// transport layer, so each call returns that wrap object — we read `.data`
// to get the actual payload.
export async function getPermissionsCatalog(): Promise<IPermissionDescriptor[]> {
return api.get("/v1/permissions") as unknown as Promise<
IPermissionDescriptor[]
>;
const r = await api.get<IPermissionDescriptor[]>("/v1/permissions");
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> {
return api.get(
"/v1/permissions/me",
) as unknown as Promise<IMyPermissionsResponse>;
const r = await api.get<IMyPermissionsResponse>("/v1/permissions/me");
return r.data;
}
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> {
return api.get(
`/v1/roles/${roleId}`,
) as unknown as Promise<IRoleWithPermissions>;
const r = await api.get<IRoleWithPermissions>(`/v1/roles/${roleId}`);
return r.data;
}
export async function createRole(
payload: ICreateRolePayload,
): Promise<IRoleWithPermissions> {
return api.post(
"/v1/roles",
payload,
) as unknown as Promise<IRoleWithPermissions>;
const r = await api.post<IRoleWithPermissions>("/v1/roles", payload);
return r.data;
}
export async function updateRole(
roleId: string,
payload: IUpdateRolePayload,
): Promise<IRole> {
return api.patch(
`/v1/roles/${roleId}`,
payload,
) as unknown as Promise<IRole>;
const r = await api.patch<IRole>(`/v1/roles/${roleId}`, payload);
return r.data;
}
export async function deleteRole(roleId: string): Promise<void> {
@ -70,26 +57,30 @@ export async function setRolePermissions(
roleId: string,
permissions: string[],
): Promise<IRoleWithPermissions> {
return api.put(`/v1/roles/${roleId}/permissions`, {
permissions,
}) as unknown as Promise<IRoleWithPermissions>;
const r = await api.put<IRoleWithPermissions>(
`/v1/roles/${roleId}/permissions`,
{ permissions },
);
return r.data;
}
export async function listUserRoles(
userId: string,
): Promise<IUserRoleAssignment[]> {
return api.get(`/v1/users/${userId}/roles`) as unknown as Promise<
IUserRoleAssignment[]
>;
const r = await api.get<IUserRoleAssignment[]>(
`/v1/users/${userId}/roles`,
);
return r.data;
}
export async function assignRolesToUser(
userId: string,
roleIds: string[],
): Promise<{ ok: true }> {
return api.post(`/v1/users/${userId}/roles`, {
const r = await api.post<{ ok: true }>(`/v1/users/${userId}/roles`, {
roleIds,
}) as unknown as Promise<{ ok: true }>;
});
return r.data;
}
export async function unassignRoleFromUser(

View file

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

View file

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