Skip to content

Conversation

@jhoward1994
Copy link
Contributor

@jhoward1994 jhoward1994 commented Mar 18, 2025

What does it do?

It adds a new FilesManager class that provides methods to retrieve files uploaded to Strapi. The implementation includes:

  1. A FilesManager class with two primary methods:

    • find(): Retrieves a list of files with optional filtering and sorting
    • findOne(): Retrieves a single file by its ID
  2. Type definitions for file responses and query parameters:

    • FileResponse interface for individual file data
    • FileListResponse type for handling lists of files
    • FileQueryParams interface for filtering and sorting options
  3. Robust validation for file queries and file IDs:

    • validateFileQueryParams(): Ensures proper format for filters and sorting
    • validateFileId(): Verifies that file IDs are valid positive integers
  4. Integration with the main Strapi client:

    • Exposed via a convenient files getter property on the Strapi client

Why is it needed?

Previously, there was no standardized way to interact with files in the Strapi Media Library through the client.

How to test it?

  1. Environment setup:

    • Ensure you have a Strapi instance running with the upload plugin enabled
    • Configure the client with the correct baseURL: const client = new Strapi({ baseURL: 'http://localhost:1337/api' })
  2. Testing file list retrieval:

    // Retrieve all files
    const allFiles = await client.files.find();
    
    // Retrieve only image files
    const imageFiles = await client.files.find({
      filters: { mime: { $contains: 'image' } }
    });
    
    // Retrieve files sorted by name
    const sortedFiles = await client.files.find({
      sort: 'name:asc'
    });
  3. Testing single file retrieval:

    // Retrieve a file by ID
    const file = await client.files.findOne(1);
    console.log(file.name, file.url, file.mime);
  4. Verify error handling:

    // Should throw validation error
    try {
      await client.files.findOne(-1);
    } catch (error) {
      console.error(error); // Should show "Invalid file ID" error
    }
    
    // Should throw error for non-existent file
    try {
      await client.files.findOne(9999);
    } catch (error) {
      console.error(error); // Should show "File not found" error
    }

Related issue(s)/PR(s)

DX-1822

@jhoward1994 jhoward1994 self-assigned this Mar 18, 2025
@jhoward1994 jhoward1994 marked this pull request as ready for review March 18, 2025 13:45
@jhoward1994 jhoward1994 added the pr: feature New or updates to features label Mar 18, 2025
@jhoward1994 jhoward1994 changed the title Feat/retrieve-files Retrieve file(s) Mar 18, 2025
Copy link
Contributor

@innerdvations innerdvations left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just took a quick look, overall looks good, will test it out tomorrow!

Copy link
Member

@Convly Convly left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall the main logic looks very good to me, some comments in the tests and error handling.

My main, biggest concern is with the terminology: When I'm using client.files.<>, I'm expecting to interact with actual files, not files' info/metadata.

Do we want to maybe rethink the methods' naming (the namespace iself seems fine if we want to add actual files method later on)? IThe thing is I'm not sure I can think of better replacements...

@Convly Convly changed the title Retrieve file(s) feat(files): file interactions. find, findone Mar 19, 2025
@Convly Convly added the source: client-files Source is the files manager label Mar 19, 2025
@jhoward1994
Copy link
Contributor Author

@Convly I've made the changes - I think the only 2 comments left are

#63 (comment)
If you could describe how you see the file examples working?

and #63 (comment)
I matched the file manager implementation closer to e.g. src/content-types/single/manager.ts
So error handling is now the responsibility of src/http/client.ts
Is this as you expect?

@jhoward1994 jhoward1994 requested a review from Convly March 25, 2025 13:13

return json;
} catch (error) {
debug('error finding file with ID %o: %o', fileId, error);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kinda liked the idea of handling and remapping some HTTP errors with domain logic (e.g. 404 -> file with id {id} not found) or other errors).

My comment was mainly about how it's checking for the actual errors + which one it re-throws rather than the idea itself.

I don't know if it's clear though, but basically

if (error instanceof HTTPNotFoundError) {
  throw new FileNotFoundError(fileId, error) // passing the original error to set an origin/source and be able to access the actual request/response objects
}

Or even better, cloning the HTTP client and adding an interceptor (so that it can be re-used by all the methods, it's even possible to add a getter to automatically apply everything and make it available for each method. Very simplified but smth like this

const mapFileErrors = ({ response }) => {
  if (error instanceof HTTPNotFoundError) {
    throw new FileNotFoundError(fileId, error) // passing the original error to set an origin/source and be able to access the actual request/response objects
  }

  return { response };
}

const client = this._httpClient.create();

client.interceptors.response.use(mapFileErrors);

const response = await client.get(url);
const json = await response.json();

What do you think about those options?

@jhoward1994 jhoward1994 requested a review from Convly March 26, 2025 14:40
Copy link
Member

@Convly Convly left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're getting there! Final super minor changes and let's merge 🙌

Also, it seems like you forgot to update the generated types for the demo Strapi app.

diff --git a/demo/.strapi-app/types/generated/contentTypes.d.ts b/demo/.strapi-app/types/generated/contentTypes.d.ts
index 4a2f513..aa0a2e9 100644
--- a/demo/.strapi-app/types/generated/contentTypes.d.ts
+++ b/demo/.strapi-app/types/generated/contentTypes.d.ts
@@ -441,6 +441,7 @@ export interface ApiCategoryCategory extends Struct.CollectionTypeSchema {
     createdAt: Schema.Attribute.DateTime;
     createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> & Schema.Attribute.Private;
     description: Schema.Attribute.Text;
+    image: Schema.Attribute.Media<'images'>;
     locale: Schema.Attribute.String & Schema.Attribute.Private;
     localizations: Schema.Attribute.Relation<'oneToMany', 'api::category.category'> &
       Schema.Attribute.Private;

},
// Error handler
(error) => {
const mapper = FileErrorMapper.createMapper(fileId);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can create this outside the interceptor to avoid too many instantiations

Comment on lines 52 to 54
({ request, response }) => {
return { request, response };
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
({ request, response }) => {
return { request, response };
},
undefined,

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the interceptor isn't doing anything, you can just pass undefined instead

return new FileForbiddenError(error, fileId);
}

return null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return null;
return error;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mapping an error to a null value seems counter-intuitive upon usage, maybe we could return the original error untouched if it's not smth the file error mapper is not interested in?

This way we can always treat the result as an Error instance no matter what

What do you think? (I've included the proposed change in the usage too)

* @returns A function that maps HTTP errors to domain-specific file errors.
*/
static createMapper(fileId?: number) {
return (error: Error): Error | null => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return (error: Error): Error | null => {
return (error: Error): Error => {

Comment on lines 58 to 64
const mappedError = mapper(error as Error);
if (mappedError) {
throw mappedError;
}

// For other errors, rethrow the original error
throw error;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const mappedError = mapper(error as Error);
if (mappedError) {
throw mappedError;
}
// For other errors, rethrow the original error
throw error;
if (error instanceof Error) {
throw mapper(error);
}
// For other errors types, re-throw the original value
throw error;


// Verify the interceptor was added to the client
const createdClient = createSpy.mock.results[0]?.value;
expect(createdClient?.interceptors.response).toBeDefined();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will return and verify that the interceptors' manager (to add a new one) exists, but I'm not sure what else it's supposed to check? Should we instead make sure it contains the actual interceptor?

Thing is you'll need to access the private _handlers property to do that 🤔

@jhoward1994 jhoward1994 requested a review from Convly March 26, 2025 16:16
@jhoward1994 jhoward1994 mentioned this pull request Mar 27, 2025
@jhoward1994 jhoward1994 merged commit 57ffa35 into main Mar 27, 2025
7 checks passed
@jhoward1994 jhoward1994 deleted the feat/retrieve-files branch March 27, 2025 13:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr: feature New or updates to features source: client-files Source is the files manager

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants