From e6e22037517f7f1d59f409c8898d1a2143800f2e Mon Sep 17 00:00:00 2001 From: Space-Banane Date: Sun, 5 Apr 2026 17:46:38 +0200 Subject: [PATCH] feat: refactor file operations and add utility functions for file management --- src/__tests__/fileops.test.ts | 47 ++++++++++++++++++++++++++++ src/commands/file/create.ts | 29 ++++-------------- src/commands/file/delete.ts | 29 ++++-------------- src/commands/file/list.ts | 20 ++++-------- src/commands/file/rename.ts | 27 ++++------------ src/commands/remote/push.ts | 4 ++- src/types/apiClient.ts | 11 +++++++ src/utils/fileops.ts | 58 +++++++++++++++++++++++++++++++++++ 8 files changed, 143 insertions(+), 82 deletions(-) create mode 100644 src/__tests__/fileops.test.ts create mode 100644 src/types/apiClient.ts create mode 100644 src/utils/fileops.ts diff --git a/src/__tests__/fileops.test.ts b/src/__tests__/fileops.test.ts new file mode 100644 index 0000000..c2a8cac --- /dev/null +++ b/src/__tests__/fileops.test.ts @@ -0,0 +1,47 @@ +import { describe, it, expect } from "vitest"; +import { + listFiles, + createOrUpdateFile, + deleteFile, + renameFile, +} from "../utils/fileops.js"; + +function makeClient(overrides: any = {}) { + return { + get: async (u: string) => ({ status: 200, data: { data: overrides.getData || [] } }), + put: async (u: string, p: any) => ({ status: overrides.putStatus || 200, data: { ok: true } }), + delete: async (u: string, opts: any) => ({ status: overrides.deleteStatus || 200, data: {} }), + patch: async (u: string, p: any) => ({ status: overrides.patchStatus || 200, data: {} }), + }; +} + +describe("fileops", () => { + it("lists files", async () => { + const client = makeClient({ getData: [{ id: "1", name: "a.txt" }] }); + const files = await listFiles(client, "fn1"); + expect(files).toHaveLength(1); + const file = files[0]; + expect(file).toHaveProperty("id"); + expect(file.id).toBe("1"); + // Accept either `name` or `filename` depending on API shape + expect(file.name || file.filename).toBe("a.txt"); + }); + + it("creates or updates a file", async () => { + const client = makeClient({ putStatus: 201 }); + const res = await createOrUpdateFile(client, "fn1", "a.txt", "hello"); + expect(res).toBeDefined(); + }); + + it("deletes a file", async () => { + const client = makeClient({ deleteStatus: 200 }); + const ok = await deleteFile(client, "fn1", "file1"); + expect(ok).toBe(true); + }); + + it("renames a file", async () => { + const client = makeClient({ patchStatus: 200 }); + const res = await renameFile(client, "fn1", "file1", "b.txt"); + expect(res).toBeDefined(); + }); +}); diff --git a/src/commands/file/create.ts b/src/commands/file/create.ts index de32c99..e492ff8 100644 --- a/src/commands/file/create.ts +++ b/src/commands/file/create.ts @@ -1,6 +1,7 @@ import chalk from "chalk"; import fs from "fs/promises"; import { getApiClient } from "../../api.js"; +import { createOrUpdateFile, handleAxiosError } from "../../utils/fileops.js"; export const fileCreateDefinition = { name: "create", @@ -33,31 +34,13 @@ export const fileCreateDefinition = { } const client = await getApiClient(); - const payload = { - filename: options.filename, - code: content, - }; - try { - const response = await client.put(`/api/function/${options.functionId}/file`, payload); - - if (response.status === 200 || response.status === 201) { - console.log( - `${chalk.green("✓")} File ${chalk.cyan(options.filename)} created/updated successfully in function ${chalk.cyan(options.functionId)}.`, - ); - } else { - console.log(`${chalk.yellow("!")} Unexpected response from server: ${response.status}`); - } + await createOrUpdateFile(client, options.functionId, options.filename, content as string); + console.log( + `${chalk.green("✓")} File ${chalk.cyan(options.filename)} created/updated successfully in function ${chalk.cyan(options.functionId)}.`, + ); } catch (error: any) { - if (error.response) { - console.error( - `${chalk.red("✗")} Failed to create/update file: ${chalk.yellow(error.response.data?.message || "Unknown error")}`, - ); - } else if (error.request) { - console.error(`${chalk.red("✗")} No response received from server.`); - } else { - console.error(`${chalk.red("✗")} Error: ${error.message}`); - } + handleAxiosError(error); } }, }; diff --git a/src/commands/file/delete.ts b/src/commands/file/delete.ts index b302551..bf34f56 100644 --- a/src/commands/file/delete.ts +++ b/src/commands/file/delete.ts @@ -1,5 +1,6 @@ import chalk from "chalk"; import { getApiClient } from "../../api.js"; +import { deleteFile, handleAxiosError } from "../../utils/fileops.js"; export const fileDeleteDefinition = { name: "delete", @@ -10,31 +11,13 @@ export const fileDeleteDefinition = { ], action: async (options: any) => { const client = await getApiClient(); - try { - const response = await client.delete(`/api/function/${options.functionId}/file/${options.fileId}`); - - if (response.status === 200) { - console.log( - `${chalk.green("✓")} File ${chalk.cyan(options.fileId)} deleted from function ${chalk.cyan(options.functionId)}.`, - ); - } else { - console.log(`${chalk.yellow("!")} Unexpected response from server: ${response.status}`); - } + await deleteFile(client, options.functionId, options.fileId, undefined); + console.log( + `${chalk.green("✓")} File ${chalk.cyan(options.fileId)} deleted from function ${chalk.cyan(options.functionId)}.`, + ); } catch (error: any) { - if (error.response) { - if (error.response.status === 404) { - console.error(`${chalk.red("✗")} File or Function not found.`); - } else { - console.error( - `${chalk.red("✗")} Failed to delete file: ${chalk.yellow(error.response.data?.message || "Unknown error")}`, - ); - } - } else if (error.request) { - console.error(`${chalk.red("✗")} No response received from server.`); - } else { - console.error(`${chalk.red("✗")} Error: ${error.message}`); - } + handleAxiosError(error); } }, }; diff --git a/src/commands/file/list.ts b/src/commands/file/list.ts index 2a9bb77..5aff331 100644 --- a/src/commands/file/list.ts +++ b/src/commands/file/list.ts @@ -1,5 +1,6 @@ import chalk from "chalk"; import { getApiClient } from "../../api.js"; +import { listFiles, handleAxiosError } from "../../utils/fileops.js"; export const fileListDefinition = { name: "list", @@ -11,10 +12,9 @@ export const fileListDefinition = { const client = await getApiClient(); try { - const response = await client.get(`/api/function/${options.functionId}/files`); + const files = await listFiles(client, options.functionId); - if (response.status === 200 && response.data.data) { - const files = response.data.data; + if (files) { if (!Array.isArray(files) || files.length === 0) { console.log(`${chalk.yellow("!")} No files found.`); @@ -24,23 +24,15 @@ export const fileListDefinition = { console.log(chalk.blue("Files:")); console.log(chalk.gray(`${"ID".padEnd(25)} ${"Filename"}`)); files.forEach((file: any) => { - console.log(`${chalk.cyan(file.id.padEnd(25))} ${chalk.white(file.filename)}`); + const id = String(file.id ?? file.name ?? ""); + console.log(`${chalk.cyan(id.padEnd(25))} ${chalk.white(file.filename || file.name)}`); }); console.log(`\n${chalk.green("✓")} Found ${chalk.bgGreen.black(` ${files.length} `)} files.`); } else { console.log(`${chalk.yellow("!")} Unexpected response from server.`); - console.log(`Status Code: ${chalk.blue(response.status)}`); } } catch (error: any) { - if (error.response) { - console.error( - `${chalk.red("✗")} Failed to list files: ${chalk.yellow(error.response.data?.message || "Unknown error")}`, - ); - } else if (error.request) { - console.error(`${chalk.red("✗")} No response received from server.`); - } else { - console.error(`${chalk.red("✗")} Error: ${error.message}`); - } + handleAxiosError(error); } }, }; diff --git a/src/commands/file/rename.ts b/src/commands/file/rename.ts index 1bec309..3964000 100644 --- a/src/commands/file/rename.ts +++ b/src/commands/file/rename.ts @@ -1,5 +1,6 @@ import chalk from "chalk"; import { getApiClient } from "../../api.js"; +import { renameFile, handleAxiosError } from "../../utils/fileops.js"; export const fileRenameDefinition = { name: "rename", @@ -11,29 +12,13 @@ export const fileRenameDefinition = { ], action: async (options: any) => { const client = await getApiClient(); - try { - const response = await client.patch(`/api/function/${options.functionId}/file/${options.fileId}/rename`, { - newFilename: options.newFilename, - }); - - if (response.status === 200) { - console.log( - `${chalk.green("✓")} File ${chalk.cyan(options.fileId)} renamed to ${chalk.cyan(options.newFilename)} successfully.`, - ); - } else { - console.log(`${chalk.yellow("!")} Unexpected response from server: ${response.status}`); - } + await renameFile(client, options.functionId, options.fileId, options.newFilename); + console.log( + `${chalk.green("✓")} File ${chalk.cyan(options.fileId)} renamed to ${chalk.cyan(options.newFilename)} successfully.`, + ); } catch (error: any) { - if (error.response) { - console.error( - `${chalk.red("✗")} Failed to rename file: ${chalk.yellow(error.response.data?.message || "Unknown error")}`, - ); - } else if (error.request) { - console.error(`${chalk.red("✗")} No response received from server.`); - } else { - console.error(`${chalk.red("✗")} Error: ${error.message}`); - } + handleAxiosError(error); } }, }; diff --git a/src/commands/remote/push.ts b/src/commands/remote/push.ts index 20cdc78..5cefd5d 100644 --- a/src/commands/remote/push.ts +++ b/src/commands/remote/push.ts @@ -12,10 +12,12 @@ import { createHash } from "crypto"; +import type { ApiClient } from "../../types/apiClient.js"; + async function deleteNonexistentFiles( currentFiles: any[], files: any[], - client: any, + client: ApiClient, options: any, ): Promise<{ didDeletion: boolean }> { let didDeletion = false; diff --git a/src/types/apiClient.ts b/src/types/apiClient.ts new file mode 100644 index 0000000..0ff34b2 --- /dev/null +++ b/src/types/apiClient.ts @@ -0,0 +1,11 @@ +// Minimal API client interface used by this CLI. Keep it small so tests can +// provide lightweight mocks without implementing the full AxiosInstance. +export interface ApiClient { + get(url: string, config?: any): Promise<{ status: number; data: any }>; + put(url: string, payload?: any, config?: any): Promise<{ status: number; data: any }>; + delete(url: string, config?: any): Promise<{ status: number; data: any }>; + patch(url: string, payload?: any, config?: any): Promise<{ status: number; data: any }>; +} + +export type { ApiClient as ApiClientType }; + diff --git a/src/utils/fileops.ts b/src/utils/fileops.ts new file mode 100644 index 0000000..e5c146d --- /dev/null +++ b/src/utils/fileops.ts @@ -0,0 +1,58 @@ +import chalk from "chalk"; +import type { ApiClient } from "../types/apiClient.js"; + +export async function listFiles(client: ApiClient, functionId: string) { + const response = await client.get(`/api/function/${functionId}/files`); + if (response.status === 200) return response.data.data || []; + throw new Error(`Unexpected status ${response.status}`); +} + +export async function createOrUpdateFile(client: ApiClient, functionId: string, filename: string, code: string) { + const payload = { filename, code }; + const response = await client.put(`/api/function/${functionId}/file`, payload); + if (response.status === 200 || response.status === 201) return response.data; + throw new Error(`Unexpected status ${response.status}`); +} + +export async function deleteFile(client: ApiClient, functionId: string, fileId: string, filename?: string) { + // Some endpoints expect filename on body; include if provided. + const opts: any = {}; + if (filename) opts.data = { filename }; + const response = await client.delete(`/api/function/${functionId}/file/${fileId}`, opts); + if (response.status === 200) return true; + throw new Error(`Unexpected status ${response.status}`); +} + +export async function renameFile(client: ApiClient, functionId: string, fileId: string, newFilename: string) { + const response = await client.patch( + `/api/function/${functionId}/file/${fileId}/rename`, + { newFilename }, + ); + if (response.status === 200) return response.data; + throw new Error(`Unexpected status ${response.status}`); +} + +export function handleAxiosError(error: any) { + if (error.response) { + if (error.response.status === 404) { + console.error(`${chalk.red("✗")} Not found.`); + return { errorType: "notfound" }; + } + console.error(`${chalk.red("✗")} ${error.response.data?.message || error.response.statusText}`); + return { errorType: "response", details: error.response }; + } + if (error.request) { + console.error(`${chalk.red("✗")} No response received from server.`); + return { errorType: "norequest" }; + } + console.error(`${chalk.red("✗")} Error: ${error.message}`); + return { errorType: "other" }; +} + +export default { + listFiles, + createOrUpdateFile, + deleteFile, + renameFile, + handleAxiosError, +};