feat: refactor file operations and add utility functions for file management

This commit is contained in:
Space-Banane
2026-04-05 17:46:38 +02:00
parent 5c18138dd2
commit e6e2203751
8 changed files with 143 additions and 82 deletions
+47
View File
@@ -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();
});
});
+6 -23
View File
@@ -1,6 +1,7 @@
import chalk from "chalk"; import chalk from "chalk";
import fs from "fs/promises"; import fs from "fs/promises";
import { getApiClient } from "../../api.js"; import { getApiClient } from "../../api.js";
import { createOrUpdateFile, handleAxiosError } from "../../utils/fileops.js";
export const fileCreateDefinition = { export const fileCreateDefinition = {
name: "create", name: "create",
@@ -33,31 +34,13 @@ export const fileCreateDefinition = {
} }
const client = await getApiClient(); const client = await getApiClient();
const payload = {
filename: options.filename,
code: content,
};
try { try {
const response = await client.put(`/api/function/${options.functionId}/file`, payload); await createOrUpdateFile(client, options.functionId, options.filename, content as string);
console.log(
if (response.status === 200 || response.status === 201) { `${chalk.green("✓")} File ${chalk.cyan(options.filename)} created/updated successfully in function ${chalk.cyan(options.functionId)}.`,
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}`);
}
} catch (error: any) { } catch (error: any) {
if (error.response) { handleAxiosError(error);
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}`);
}
} }
}, },
}; };
+6 -23
View File
@@ -1,5 +1,6 @@
import chalk from "chalk"; import chalk from "chalk";
import { getApiClient } from "../../api.js"; import { getApiClient } from "../../api.js";
import { deleteFile, handleAxiosError } from "../../utils/fileops.js";
export const fileDeleteDefinition = { export const fileDeleteDefinition = {
name: "delete", name: "delete",
@@ -10,31 +11,13 @@ export const fileDeleteDefinition = {
], ],
action: async (options: any) => { action: async (options: any) => {
const client = await getApiClient(); const client = await getApiClient();
try { try {
const response = await client.delete(`/api/function/${options.functionId}/file/${options.fileId}`); await deleteFile(client, options.functionId, options.fileId, undefined);
console.log(
if (response.status === 200) { `${chalk.green("✓")} File ${chalk.cyan(options.fileId)} deleted from function ${chalk.cyan(options.functionId)}.`,
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}`);
}
} catch (error: any) { } catch (error: any) {
if (error.response) { handleAxiosError(error);
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}`);
}
} }
}, },
}; };
+6 -14
View File
@@ -1,5 +1,6 @@
import chalk from "chalk"; import chalk from "chalk";
import { getApiClient } from "../../api.js"; import { getApiClient } from "../../api.js";
import { listFiles, handleAxiosError } from "../../utils/fileops.js";
export const fileListDefinition = { export const fileListDefinition = {
name: "list", name: "list",
@@ -11,10 +12,9 @@ export const fileListDefinition = {
const client = await getApiClient(); const client = await getApiClient();
try { 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) { if (files) {
const files = response.data.data;
if (!Array.isArray(files) || files.length === 0) { if (!Array.isArray(files) || files.length === 0) {
console.log(`${chalk.yellow("!")} No files found.`); console.log(`${chalk.yellow("!")} No files found.`);
@@ -24,23 +24,15 @@ export const fileListDefinition = {
console.log(chalk.blue("Files:")); console.log(chalk.blue("Files:"));
console.log(chalk.gray(`${"ID".padEnd(25)} ${"Filename"}`)); console.log(chalk.gray(`${"ID".padEnd(25)} ${"Filename"}`));
files.forEach((file: any) => { 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.`); console.log(`\n${chalk.green("✓")} Found ${chalk.bgGreen.black(` ${files.length} `)} files.`);
} else { } else {
console.log(`${chalk.yellow("!")} Unexpected response from server.`); console.log(`${chalk.yellow("!")} Unexpected response from server.`);
console.log(`Status Code: ${chalk.blue(response.status)}`);
} }
} catch (error: any) { } catch (error: any) {
if (error.response) { handleAxiosError(error);
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}`);
}
} }
}, },
}; };
+6 -21
View File
@@ -1,5 +1,6 @@
import chalk from "chalk"; import chalk from "chalk";
import { getApiClient } from "../../api.js"; import { getApiClient } from "../../api.js";
import { renameFile, handleAxiosError } from "../../utils/fileops.js";
export const fileRenameDefinition = { export const fileRenameDefinition = {
name: "rename", name: "rename",
@@ -11,29 +12,13 @@ export const fileRenameDefinition = {
], ],
action: async (options: any) => { action: async (options: any) => {
const client = await getApiClient(); const client = await getApiClient();
try { try {
const response = await client.patch(`/api/function/${options.functionId}/file/${options.fileId}/rename`, { await renameFile(client, options.functionId, options.fileId, options.newFilename);
newFilename: options.newFilename, console.log(
}); `${chalk.green("✓")} File ${chalk.cyan(options.fileId)} renamed to ${chalk.cyan(options.newFilename)} successfully.`,
);
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}`);
}
} catch (error: any) { } catch (error: any) {
if (error.response) { handleAxiosError(error);
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}`);
}
} }
}, },
}; };
+3 -1
View File
@@ -12,10 +12,12 @@ import { createHash } from "crypto";
import type { ApiClient } from "../../types/apiClient.js";
async function deleteNonexistentFiles( async function deleteNonexistentFiles(
currentFiles: any[], currentFiles: any[],
files: any[], files: any[],
client: any, client: ApiClient,
options: any, options: any,
): Promise<{ didDeletion: boolean }> { ): Promise<{ didDeletion: boolean }> {
let didDeletion = false; let didDeletion = false;
+11
View File
@@ -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 };
+58
View File
@@ -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,
};