Skip to content

Commit 34638a5

Browse files
authored
Merge branch 'main' into releases/1.1.0
2 parents 6b42b6c + ee09706 commit 34638a5

File tree

6 files changed

+324
-0
lines changed

6 files changed

+324
-0
lines changed

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ The `files` property provides access to the Strapi Media Library through the Upl
210210
- `find(params?: FileQueryParams): Promise<FileListResponse>` - Retrieves a list of file metadata based on optional query parameters
211211
- `findOne(fileId: number): Promise<FileResponse>` - Retrieves the metadata for a single file by its ID
212212
- `update(fileId: number, fileInfo: FileUpdateData): Promise<FileResponse>` - Updates metadata for an existing file
213+
- `delete(fileId: number): Promise<void>` - Deletes a file by its ID
213214

214215
#### Example: Finding Files
215216

@@ -271,6 +272,23 @@ console.log(updatedFile.name); // Updated file name
271272
console.log(updatedFile.alternativeText); // Updated alt text
272273
```
273274

275+
#### Example: Deleting a File
276+
277+
```typescript
278+
// Initialize the client
279+
const client = strapi({
280+
baseURL: 'http://localhost:1337/api',
281+
auth: 'your-api-token',
282+
});
283+
284+
// Delete a file by ID
285+
const deletedFile = await client.files.delete(1);
286+
287+
console.log('File deleted successfully');
288+
console.log('Deleted file ID:', deletedFile.id);
289+
console.log('Deleted file name:', deletedFile.name);
290+
```
291+
274292
## 🐛 Debug
275293

276294
This section provides guidance on enabling and managing debug logs for the SDK,

demo/node-javascript/src/index.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,52 @@ async function main() {
144144
'Tech category not found. Make sure you have a category with slug "tech" in your Strapi instance.'
145145
);
146146
}
147+
148+
console.log(os.EOL);
149+
console.log('=== File Deletion Operations ===');
150+
console.log(os.EOL);
151+
152+
const getFileThatCanBeDeleted = async () => {
153+
try {
154+
const files = await client.files.find();
155+
if (files && files.length > 0) {
156+
const fileToDelete = files[0];
157+
console.log(`Found file to delete: ${fileToDelete.name} (ID: ${fileToDelete.id})`);
158+
return fileToDelete.id;
159+
}
160+
return null;
161+
} catch (error) {
162+
console.error('Error finding files:', error);
163+
return null;
164+
}
165+
};
166+
167+
const PERFORM_ACTUAL_DELETE = false;
168+
const fileIdToDelete = await getFileThatCanBeDeleted();
169+
170+
if (fileIdToDelete && PERFORM_ACTUAL_DELETE) {
171+
console.log(os.EOL);
172+
console.log(`Attempting to delete file with ID: ${fileIdToDelete}`);
173+
174+
try {
175+
const deletedFile = await client.files.delete(fileIdToDelete);
176+
console.log('Deleted file metadata:', deletedFile);
177+
178+
console.log(os.EOL);
179+
console.log(`Attempting to find deleted file with ID: ${fileIdToDelete}`);
180+
try {
181+
const file = await client.files.findOne(fileIdToDelete);
182+
console.error('Unexpected result: File still exists:', file);
183+
} catch (error) {
184+
console.log('Expected error: File no longer exists');
185+
console.log(`Error message: ${error.message}`);
186+
}
187+
} catch (error) {
188+
console.error('Error during file deletion demonstration:', error);
189+
}
190+
} else {
191+
console.log('No files available for deletion demonstration.');
192+
}
147193
}
148194

149195
main().catch(console.error);

demo/node-typescript/src/index.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ async function runDemo() {
7676
await demonstrateCategoryImageInteractions();
7777
await demonstrateDirectFileOperations();
7878
await demonstrateFileUpdates();
79+
await demonstrateFileDeletion();
7980
}
8081

8182
async function demonstrateBasicCategoryFunctionality() {
@@ -222,6 +223,58 @@ async function demonstrateFileUpdates() {
222223
}
223224
}
224225

226+
const PERFORM_ACTUAL_DELETE = false;
227+
async function demonstrateFileDeletion() {
228+
if (!PERFORM_ACTUAL_DELETE) {
229+
return;
230+
}
231+
232+
console.log(os.EOL);
233+
console.log('=== File Deletion Operations ===');
234+
console.log(os.EOL);
235+
236+
const getFileThatCanBeDeleted = async (): Promise<number | null> => {
237+
try {
238+
const files = await client.files.find();
239+
if (files && files.length > 0) {
240+
const fileToDelete = files[0];
241+
console.log(`Found file to delete: ${fileToDelete.name} (ID: ${fileToDelete.id})`);
242+
return fileToDelete.id;
243+
}
244+
return null;
245+
} catch (error) {
246+
console.error('Error finding files:', error);
247+
return null;
248+
}
249+
};
250+
251+
const fileIdToDelete = await getFileThatCanBeDeleted();
252+
253+
if (fileIdToDelete) {
254+
console.log(os.EOL);
255+
console.log(`Attempting to delete file with ID: ${fileIdToDelete}`);
256+
257+
try {
258+
const deletedFile = await client.files.delete(fileIdToDelete);
259+
console.log('Deleted file metadata:', deletedFile);
260+
261+
console.log(os.EOL);
262+
console.log(`Attempting to find deleted file with ID: ${fileIdToDelete}`);
263+
try {
264+
const file = await client.files.findOne(fileIdToDelete);
265+
console.error('Unexpected result: File still exists:', file);
266+
} catch (error) {
267+
console.log('Expected error: File no longer exists');
268+
console.log(`Error message: ${error instanceof Error ? error.message : String(error)}`);
269+
}
270+
} catch (error) {
271+
console.error('Error during file deletion demonstration:', error);
272+
}
273+
} else {
274+
console.log('No files available for deletion demonstration.');
275+
}
276+
}
277+
225278
// Helper function to format file sizes
226279
function formatFileSize(bytes: number): string {
227280
if (bytes < 1024) return `${bytes} B`;

src/files/manager.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,4 +192,41 @@ export class FilesManager {
192192
throw error;
193193
}
194194
}
195+
196+
/**
197+
* Deletes a file by its ID.
198+
*
199+
* @param fileId - The numeric identifier of the file to delete.
200+
* @returns A promise that resolves to the deleted file object.
201+
*
202+
* @throws {FileNotFoundError} if the file with the specified ID does not exist.
203+
* @throws {FileForbiddenError} if the user does not have permission to delete the file.
204+
* @throws {HTTPError} if the HTTP client encounters connection issues, the server is unreachable, or authentication fails.
205+
*
206+
* @example
207+
* ```typescript
208+
* const filesManager = new FilesManager(httpClient);
209+
*
210+
* await filesManager.delete(1);
211+
* console.log('File deleted successfully');
212+
* ```
213+
*/
214+
async delete(fileId: number): Promise<FileResponse> {
215+
debug('deleting file with ID %o', fileId);
216+
217+
try {
218+
const url = `${FILE_API_PREFIX}/files/${fileId}`;
219+
const client = this.createFileHttpClient(fileId);
220+
221+
const response = await client.delete(url);
222+
const json = await response.json();
223+
224+
debug('successfully deleted file with ID %o', fileId);
225+
226+
return json;
227+
} catch (error) {
228+
debug('error deleting file with ID %o: %o', fileId, error);
229+
throw error;
230+
}
231+
}
195232
}

tests/unit/files/files-client-integration.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,4 +218,75 @@ describe('Strapi Client - Files Integration', () => {
218218
}
219219
});
220220
});
221+
222+
describe('files.delete integration', () => {
223+
it('should correctly call the delete endpoint', async () => {
224+
// Arrange
225+
mockFetch.mockResolvedValueOnce({
226+
ok: true,
227+
status: 200,
228+
json: jest.fn().mockResolvedValueOnce({ id: 1, name: 'deleted-file.jpg' }),
229+
});
230+
231+
// Act
232+
const result = await strapi.files.delete(1);
233+
234+
// Assert
235+
const requestArg = mockFetch.mock.calls[0][0];
236+
expect(requestArg.url).toBe('http://example.com/api/upload/files/1');
237+
expect(requestArg.method).toBe('DELETE');
238+
expect(result).toEqual({ id: 1, name: 'deleted-file.jpg' });
239+
});
240+
241+
it('should pass authentication headers when deleting a file', async () => {
242+
// Arrange
243+
mockFetch.mockResolvedValueOnce({
244+
ok: true,
245+
status: 200,
246+
json: jest.fn().mockResolvedValueOnce({ id: 1, name: 'deleted-file.jpg' }),
247+
});
248+
249+
// Create an authenticated Strapi client
250+
const authenticatedStrapi = new Strapi({
251+
baseURL: 'http://example.com/api',
252+
auth: {
253+
strategy: 'api-token',
254+
options: { token: 'test_token' },
255+
},
256+
});
257+
258+
// Act
259+
await authenticatedStrapi.files.delete(1);
260+
261+
// Assert
262+
const requestArg = mockFetch.mock.calls[0][0];
263+
expect(requestArg.url).toBe('http://example.com/api/upload/files/1');
264+
expect(requestArg.method).toBe('DELETE');
265+
expect(requestArg.headers.get('Authorization')).toBe('Bearer test_token');
266+
});
267+
268+
it('should handle file not found errors when deleting', async () => {
269+
// Arrange
270+
const fileId = 999;
271+
const mockRequest = new Request(`http://example.com/api/upload/files/${fileId}`);
272+
const mockResponse = new Response('Not Found', { status: 404, statusText: 'Not Found' });
273+
274+
// Create a not found error that will be thrown by HttpClient
275+
const httpNotFoundError = new HTTPNotFoundError(mockResponse, mockRequest);
276+
mockFetch.mockRejectedValueOnce(httpNotFoundError);
277+
278+
// Act & Assert
279+
try {
280+
await strapi.files.delete(fileId);
281+
fail('Expected error was not thrown');
282+
} catch (error) {
283+
// The error should be mapped to a FileNotFoundError
284+
expect(error).toBeInstanceOf(FileNotFoundError);
285+
if (error instanceof FileNotFoundError) {
286+
expect(error.fileId).toBe(fileId);
287+
expect(error.message).toContain(`File with ID ${fileId} not found`);
288+
}
289+
}
290+
});
291+
});
221292
});

tests/unit/files/files-manager.test.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,4 +630,103 @@ describe('FilesManager', () => {
630630
expect(createSpy).toHaveBeenCalledWith(1);
631631
});
632632
});
633+
634+
describe('delete', () => {
635+
it('should successfully delete a file by ID', async () => {
636+
// Arrange
637+
mockFetch.mockResolvedValueOnce({
638+
ok: true,
639+
status: 200,
640+
json: jest.fn().mockResolvedValueOnce({ id: 1, name: 'deleted-file.jpg' }),
641+
});
642+
643+
// Act
644+
const result = await filesManager.delete(1);
645+
646+
// Assert
647+
expect(mockFetch).toHaveBeenCalledTimes(1);
648+
const requestArg = mockFetch.mock.calls[0][0];
649+
650+
// Check that the URL and method are correct
651+
expect(requestArg.url).toBe('http://example.com/api/upload/files/1');
652+
expect(requestArg.method).toBe('DELETE');
653+
654+
// Check the returned result
655+
expect(result).toEqual({ id: 1, name: 'deleted-file.jpg' });
656+
});
657+
658+
it('should throw FileNotFoundError when deleting a non-existent file', async () => {
659+
// Arrange
660+
const fileId = 999;
661+
const mockRequest = new Request(`http://example.com/api/upload/files/${fileId}`);
662+
const mockResponse = new Response('Not Found', { status: 404, statusText: 'Not Found' });
663+
664+
// Create an HTTPNotFoundError that would be thrown by the HttpClient
665+
const httpNotFoundError = new HTTPNotFoundError(mockResponse, mockRequest);
666+
mockFetch.mockRejectedValueOnce(httpNotFoundError);
667+
668+
// Act & Assert
669+
try {
670+
await filesManager.delete(fileId);
671+
fail('Expected an error to be thrown');
672+
} catch (error) {
673+
// Check that the error is properly mapped to a FileNotFoundError
674+
expect(error).toBeInstanceOf(FileNotFoundError);
675+
if (error instanceof FileNotFoundError) {
676+
// Verify the error contains the file ID and a relevant message
677+
expect(error.fileId).toBe(fileId);
678+
expect(error.message).toContain(`File with ID ${fileId} not found`);
679+
// Verify the original request and response are preserved
680+
expect(error.request).toBe(mockRequest);
681+
expect(error.response).toBe(mockResponse);
682+
}
683+
}
684+
});
685+
686+
it('should throw FileForbiddenError when user does not have permission', async () => {
687+
// Arrange
688+
const fileId = 1;
689+
const mockRequest = new Request(`http://example.com/api/upload/files/${fileId}`);
690+
const mockResponse = new Response('Forbidden', { status: 403, statusText: 'Forbidden' });
691+
692+
// Create an HTTPForbiddenError that would be thrown by the HttpClient
693+
const httpForbiddenError = new HTTPForbiddenError(mockResponse, mockRequest);
694+
mockFetch.mockRejectedValueOnce(httpForbiddenError);
695+
696+
// Act & Assert
697+
try {
698+
await filesManager.delete(fileId);
699+
fail('Expected an error to be thrown');
700+
} catch (error) {
701+
// Check that the error is properly mapped to a FileForbiddenError
702+
expect(error).toBeInstanceOf(FileForbiddenError);
703+
if (error instanceof FileForbiddenError) {
704+
// Verify the error contains the file ID and a relevant message
705+
expect(error.fileId).toBe(fileId);
706+
expect(error.message).toContain(`Access to file with ID ${fileId} is forbidden`);
707+
// Verify the original request and response are preserved
708+
expect(error.request).toBe(mockRequest);
709+
expect(error.response).toBe(mockResponse);
710+
}
711+
}
712+
});
713+
714+
it('should pass fileId to createFileHttpClient', async () => {
715+
// Arrange
716+
// Create spy on the private method using any
717+
const createSpy = jest.spyOn(filesManager as any, 'createFileHttpClient');
718+
719+
mockFetch.mockResolvedValueOnce({
720+
ok: true,
721+
status: 200,
722+
json: jest.fn().mockResolvedValueOnce({ id: 1, name: 'deleted-file.jpg' }),
723+
});
724+
725+
// Act
726+
await filesManager.delete(1);
727+
728+
// Assert
729+
expect(createSpy).toHaveBeenCalledWith(1);
730+
});
731+
});
633732
});

0 commit comments

Comments
 (0)