diff --git a/app/src/api/admin.ts b/app/src/api/admin.ts index f7de7b7..672145a 100644 --- a/app/src/api/admin.ts +++ b/app/src/api/admin.ts @@ -19,14 +19,8 @@ const ADMIN_BASE_URL = `${API_BASE_URL}/admin`; * */ export const getUsers = async (): Promise => { const response = await apiCall(`${ADMIN_BASE_URL}/users`); - - if (!response.ok) { - const data = await response.json(); - const errorData = data as { message: string }; - throw new Error(errorData.message || 'Failed to fetch users'); - } - const data = await response.json(); + if (!Array.isArray(data)) { throw new Error('Invalid users response received from API'); } @@ -52,12 +46,6 @@ export const createUser = async ( body: JSON.stringify(userData), }); - if (!response.ok) { - const data = await response.json(); - const errorData = data as { message: string }; - throw new Error(errorData.message || 'Failed to create user'); - } - const data = await response.json(); if (!isUser(data)) { throw new Error('Invalid user object received from API'); @@ -97,11 +85,6 @@ export const updateUser = async ( body: JSON.stringify(userData), }); - if (!response.ok) { - const data = await response.json(); - const errorData = data as { message: string }; - throw new Error(errorData.message || 'Failed to update user'); - } const data = await response.json(); if (!isUser(data)) { throw new Error('Invalid user object received from API'); @@ -118,11 +101,6 @@ export const updateUser = async ( * */ export const getWorkspaces = async (): Promise => { const response = await apiCall(`${ADMIN_BASE_URL}/workspaces`); - if (!response.ok) { - const data = await response.json(); - const errorData = data as { message: string }; - throw new Error(errorData.message || 'Failed to fetch workspaces'); - } const data = await response.json(); if (!Array.isArray(data)) { throw new Error('Invalid workspaces response received from API'); @@ -144,11 +122,6 @@ export const getWorkspaces = async (): Promise => { * */ export const getSystemStats = async (): Promise => { const response = await apiCall(`${ADMIN_BASE_URL}/stats`); - if (!response.ok) { - const data = await response.json(); - const errorData = data as { message: string }; - throw new Error(errorData.message || 'Failed to fetch system stats'); - } const data = await response.json(); if (!isSystemStats(data)) { throw new Error('Invalid system stats response received from API'); diff --git a/app/src/api/auth.ts b/app/src/api/auth.ts index 6eb0a05..e76aaf3 100644 --- a/app/src/api/auth.ts +++ b/app/src/api/auth.ts @@ -4,13 +4,17 @@ import { LoginRequest, LoginResponse, isLoginResponse, - ErrorResponse, isUser, } from '../types/authApi'; import { apiCall } from './api'; /** * Logs in a user with email and password + * @param {string} email - The user's email + * @param {string} password - The user's password + * @returns {Promise} A promise that resolves to the login response + * @throws {Error} If the API call fails or returns an invalid response + * @throws {Error} If the login fails */ export const login = async ( email: string, @@ -22,12 +26,6 @@ export const login = async ( body: JSON.stringify(loginData), }); - if (!response.ok) { - const data = await response.json(); - const errorData = data as ErrorResponse; - throw new Error(errorData.message || 'Login failed'); - } - const data = await response.json(); if (!isLoginResponse(data)) { throw new Error('Invalid login response received from API'); @@ -38,16 +36,17 @@ export const login = async ( /** * Logs out the current user + * @returns {Promise} A promise that resolves when the logout is successful + * @throws {Error} If the API call fails or returns an invalid response + * @throws {Error} If the logout fails */ export const logout = async (): Promise => { const response = await apiCall(`${API_BASE_URL}/auth/logout`, { method: 'POST', }); - if (!response.ok) { - const data = await response.json(); - const errorData = data as ErrorResponse; - throw new Error(errorData.message || 'Logout failed'); + if (response.status !== 204) { + throw new Error('Failed to log out'); } }; @@ -57,16 +56,10 @@ export const logout = async (): Promise => { */ export const refreshToken = async (): Promise => { try { - const response = await apiCall(`${API_BASE_URL}/auth/refresh`, { + await apiCall(`${API_BASE_URL}/auth/refresh`, { method: 'POST', }); - if (!response.ok) { - const data = await response.json(); - const errorData = data as ErrorResponse; - throw new Error(errorData.message || 'Token refresh failed'); - } - return true; } catch (error) { return false; @@ -75,16 +68,14 @@ export const refreshToken = async (): Promise => { /** * Gets the currently authenticated user + * @returns {Promise} A promise that resolves to the current user + * @throws {Error} If the API call fails or returns an invalid response + * @throws {Error} If the user data is invalid */ export const getCurrentUser = async (): Promise => { const response = await apiCall(`${API_BASE_URL}/auth/me`); const data = await response.json(); - if (!response.ok) { - const errorData = data as ErrorResponse; - throw new Error(errorData.message || 'Failed to get current user'); - } - if (!isUser(data)) { throw new Error('Invalid user data received from API'); } diff --git a/app/src/api/file.ts b/app/src/api/file.ts new file mode 100644 index 0000000..53602a8 --- /dev/null +++ b/app/src/api/file.ts @@ -0,0 +1,170 @@ +import { API_BASE_URL } from '@/types/authApi'; +import { apiCall } from './api'; +import { + FileNode, + isFileNode, + isLastOpenedFileResponse, + isLookupResponse, + isSaveFileResponse, + LastOpenedFileResponse, + LookupResponse, + SaveFileResponse, +} from '@/types/fileApi'; + +/** + * listFiles fetches the list of files in a workspace + * @param workspaceName - The name of the workspace + * @returns {Promise} A promise that resolves to an array of FileNode objects + * @throws {Error} If the API call fails or returns an invalid response + */ +export const listFiles = async (workspaceName: string): Promise => { + const response = await apiCall( + `${API_BASE_URL}/workspaces/${encodeURIComponent(workspaceName)}/files` + ); + const data = await response.json(); + if (!Array.isArray(data)) { + throw new Error('Invalid files response received from API'); + } + return data.map((file) => { + if (!isFileNode(file)) { + throw new Error('Invalid file object received from API'); + } + return file as FileNode; + }); +}; + +/** + * lookupFileByName fetches the file paths that match the given filename in a workspace + * @param workspaceName - The name of the workspace + * @param filename - The name of the file to look up + * @returns {Promise} A promise that resolves to an array of file paths + * @throws {Error} If the API call fails or returns an invalid response + */ +export const lookupFileByName = async ( + workspaceName: string, + filename: string +): Promise => { + const response = await apiCall( + `${API_BASE_URL}/workspaces/${encodeURIComponent( + workspaceName + )}/files/lookup?filename=${encodeURIComponent(filename)}` + ); + const data = await response.json(); + if (!isLookupResponse(data)) { + throw new Error('Invalid lookup response received from API'); + } + const lookupResponse = data as LookupResponse; + return lookupResponse.paths; +}; + +/** + * getFileContent fetches the content of a file in a workspace + * @param workspaceName - The name of the workspace + * @param filePath - The path of the file to fetch + * @returns {Promise} A promise that resolves to the file content + * @throws {Error} If the API call fails or returns an invalid response + */ +export const getFileContent = async ( + workspaceName: string, + filePath: string +): Promise => { + const response = await apiCall( + `${API_BASE_URL}/workspaces/${encodeURIComponent( + workspaceName + )}/files/${encodeURIComponent(filePath)}` + ); + return response.text(); +}; + +/** + * saveFile saves the content to a file in a workspace + * @param workspaceName - The name of the workspace + * @param filePath - The path of the file to save + * @param content - The content to save in the file + * @returns {Promise} A promise that resolves to the save file response + * @throws {Error} If the API call fails or returns an invalid response + */ +export const saveFile = async ( + workspaceName: string, + filePath: string, + content: string +): Promise => { + const response = await apiCall( + `${API_BASE_URL}/workspaces/${encodeURIComponent( + workspaceName + )}/files/${encodeURIComponent(filePath)}`, + { + method: 'POST', + headers: { + 'Content-Type': 'text/plain', + }, + body: content, + } + ); + const data = await response.json(); + if (!isSaveFileResponse(data)) { + throw new Error('Invalid save file response received from API'); + } + return data as SaveFileResponse; +}; + +/** + * deleteFile deletes a file in a workspace + * @param workspaceName - The name of the workspace + * @param filePath - The path of the file to delete + * @throws {Error} If the API call fails or returns an invalid response + */ +export const deleteFile = async (workspaceName: string, filePath: string) => { + await apiCall( + `${API_BASE_URL}/workspaces/${encodeURIComponent( + workspaceName + )}/files/${encodeURIComponent(filePath)}`, + { + method: 'DELETE', + } + ); +}; + +/** + * getLastOpenedFile fetches the last opened file in a workspace + * @param workspaceName - The name of the workspace + * @returns {Promise} A promise that resolves to the last opened file path + * @throws {Error} If the API call fails or returns an invalid response + */ +export const getLastOpenedFile = async ( + workspaceName: string +): Promise => { + const response = await apiCall( + `${API_BASE_URL}/workspaces/${encodeURIComponent(workspaceName)}/files/last` + ); + const data = await response.json(); + if (!isLastOpenedFileResponse(data)) { + throw new Error('Invalid last opened file response received from API'); + } + const lastOpenedFileResponse = data as LastOpenedFileResponse; + return lastOpenedFileResponse.lastOpenedFilePath; +}; + +/** + * updateLastOpenedFile updates the last opened file in a workspace + * @param workspaceName - The name of the workspace + * @param filePath - The path of the file to set as last opened + * @throws {Error} If the API call fails or returns an invalid response + */ +export const updateLastOpenedFile = async ( + workspaceName: string, + filePath: string +) => { + await apiCall( + `${API_BASE_URL}/workspaces/${encodeURIComponent( + workspaceName + )}/files/last`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ filePath }), + } + ); +}; diff --git a/app/src/api/notes.js b/app/src/api/notes.js index a635da9..cb8d8d4 100644 --- a/app/src/api/notes.js +++ b/app/src/api/notes.js @@ -22,43 +22,6 @@ export const fetchLastWorkspaceName = async () => { return response.json(); }; -export const fetchFileList = async (workspaceName) => { - const response = await apiCall( - `${API_BASE_URL}/workspaces/${workspaceName}/files` - ); - return response.json(); -}; - -export const fetchFileContent = async (workspaceName, filePath) => { - const response = await apiCall( - `${API_BASE_URL}/workspaces/${workspaceName}/files/${filePath}` - ); - return response.text(); -}; - -export const saveFileContent = async (workspaceName, filePath, content) => { - const response = await apiCall( - `${API_BASE_URL}/workspaces/${workspaceName}/files/${filePath}`, - { - method: 'POST', - headers: { - 'Content-Type': 'text/plain', - }, - body: content, - } - ); - return response.json(); -}; - -export const deleteFile = async (workspaceName, filePath) => { - await apiCall( - `${API_BASE_URL}/workspaces/${workspaceName}/files/${filePath}`, - { - method: 'DELETE', - } - ); -}; - export const getWorkspace = async (workspaceName) => { const response = await apiCall(`${API_BASE_URL}/workspaces/${workspaceName}`); return response.json(); @@ -107,33 +70,6 @@ export const getFileUrl = (workspaceName, filePath) => { return `${API_BASE_URL}/workspaces/${workspaceName}/files/${filePath}`; }; -export const lookupFileByName = async (workspaceName, filename) => { - const response = await apiCall( - `${API_BASE_URL}/workspaces/${workspaceName}/files/lookup?filename=${encodeURIComponent( - filename - )}` - ); - const data = await response.json(); - return data.paths; -}; - -export const updateLastOpenedFile = async (workspaceName, filePath) => { - await apiCall(`${API_BASE_URL}/workspaces/${workspaceName}/files/last`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ filePath }), - }); -}; - -export const getLastOpenedFile = async (workspaceName) => { - const response = await apiCall( - `${API_BASE_URL}/workspaces/${workspaceName}/files/last` - ); - return response.json(); -}; - export const listWorkspaces = async () => { const response = await apiCall(`${API_BASE_URL}/workspaces`); return response.json(); diff --git a/app/src/types/fileApi.ts b/app/src/types/fileApi.ts new file mode 100644 index 0000000..ffe0c49 --- /dev/null +++ b/app/src/types/fileApi.ts @@ -0,0 +1,73 @@ +export interface LookupResponse { + paths: string[]; +} + +export function isLookupResponse(obj: unknown): obj is LookupResponse { + return ( + typeof obj === 'object' && + obj !== null && + 'paths' in obj && + Array.isArray((obj as LookupResponse).paths) && + (obj as LookupResponse).paths.every((path) => typeof path === 'string') + ); +} + +export interface SaveFileResponse { + filePath: string; + size: number; + updatedAt: string; // ISO 8601 string representation of the date +} + +export function isSaveFileResponse(obj: unknown): obj is SaveFileResponse { + return ( + typeof obj === 'object' && + obj !== null && + 'filePath' in obj && + typeof (obj as SaveFileResponse).filePath === 'string' && + 'size' in obj && + typeof (obj as SaveFileResponse).size === 'number' && + 'updatedAt' in obj && + typeof (obj as SaveFileResponse).updatedAt === 'string' + ); +} + +export interface LastOpenedFileResponse { + lastOpenedFilePath: string; +} + +export function isLastOpenedFileResponse( + obj: unknown +): obj is LastOpenedFileResponse { + return ( + typeof obj === 'object' && + obj !== null && + 'lastOpenedFilePath' in obj && + typeof (obj as LastOpenedFileResponse).lastOpenedFilePath === 'string' + ); +} + +export interface UpdateLastOpenedFileRequest { + filePath: string; +} + +export interface FileNode { + id: string; + name: string; + path: string; + children: FileNode[]; +} + +export function isFileNode(obj: unknown): obj is FileNode { + return ( + typeof obj === 'object' && + obj !== null && + 'id' in obj && + typeof (obj as FileNode).id === 'string' && + 'name' in obj && + typeof (obj as FileNode).name === 'string' && + 'path' in obj && + typeof (obj as FileNode).path === 'string' && + 'children' in obj && + Array.isArray((obj as FileNode).children) + ); +}