Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions spec/CloudCode.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4144,11 +4144,13 @@ describe('saveFile hooks', () => {
foo: 'bar',
},
};

expect(createFileSpy).toHaveBeenCalledWith(
jasmine.any(String),
newData,
'text/plain',
newOptions
newOptions,
jasmine.objectContaining({ applicationId: 'test' })
);
Comment on lines 4148 to 4154
});

Expand Down Expand Up @@ -4176,11 +4178,15 @@ describe('saveFile hooks', () => {
foo: 'bar',
},
};

expect(createFileSpy).toHaveBeenCalledWith(
jasmine.any(String),
newData,
newContentType,
newOptions
newOptions,
jasmine.objectContaining({
applicationId: 'test'
})
);
const expectedFileName = 'donald_duck.pdf';
expect(file._name.indexOf(expectedFileName)).toBe(file._name.length - expectedFileName.length);
Expand All @@ -4206,11 +4212,14 @@ describe('saveFile hooks', () => {
metadata: { foo: 'bar' },
tags: { bar: 'foo' },
};


expect(createFileSpy).toHaveBeenCalledWith(
jasmine.any(String),
jasmine.any(Buffer),
'text/plain',
options
options,
jasmine.objectContaining({ applicationId: 'test' })
);
});

Expand Down
98 changes: 98 additions & 0 deletions spec/FilesController.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,4 +218,102 @@ describe('FilesController', () => {
expect(gridFSAdapter.validateFilename(fileName)).not.toBe(null);
done();
});

it('should return filename and url when adapter returns both', async () => {
const config = Config.get(Parse.applicationId);
const adapterWithReturn = { ...mockAdapter };
adapterWithReturn.createFile = () => {
return Promise.resolve({
name: 'newFilename.txt',
url: 'http://example.com/newFilename.txt'
});
};
adapterWithReturn.getFileLocation = () => {
return Promise.resolve('http://example.com/file.txt');
};
const controllerWithReturn = new FilesController(adapterWithReturn, null, { preserveFileName: true });

const result = await controllerWithReturn.createFile(
config,
'originalFile.txt',
'data',
'text/plain'
);

expect(result.name).toBe('newFilename.txt');
expect(result.url).toBe('http://example.com/newFilename.txt');
});
Comment on lines +222 to +245

it('should use original filename and generate url when adapter returns nothing', async () => {
const config = Config.get(Parse.applicationId);
const adapterWithoutReturn = { ...mockAdapter };
adapterWithoutReturn.createFile = () => {
return Promise.resolve();
};
adapterWithoutReturn.getFileLocation = (config, filename) => {
return Promise.resolve(`http://example.com/${filename}`);
};

const controllerWithoutReturn = new FilesController(adapterWithoutReturn, null, { preserveFileName: true });
const result = await controllerWithoutReturn.createFile(
config,
'originalFile.txt',
'data',
'text/plain',
{}
);

expect(result.name).toBe('originalFile.txt');
expect(result.url).toBe('http://example.com/originalFile.txt');
});

it('should use original filename when adapter returns only url', async () => {
const config = Config.get(Parse.applicationId);
const adapterWithOnlyURL = { ...mockAdapter };
adapterWithOnlyURL.createFile = () => {
return Promise.resolve({
url: 'http://example.com/partialFile.txt'
});
};
adapterWithOnlyURL.getFileLocation = () => {
return Promise.resolve('http://example.com/file.txt');
};

const controllerWithPartial = new FilesController(adapterWithOnlyURL, null, { preserveFileName: true });
const result = await controllerWithPartial.createFile(
config,
'originalFile.txt',
'data',
'text/plain',
{}
);

expect(result.name).toBe('originalFile.txt');
expect(result.url).toBe('http://example.com/partialFile.txt');
});

it('should use adapter filename and generate url when adapter returns only filename', async () => {
const config = Config.get(Parse.applicationId);
const adapterWithOnlyFilename = { ...mockAdapter };
adapterWithOnlyFilename.createFile = () => {
return Promise.resolve({
name: 'newname.txt'
});
};
adapterWithOnlyFilename.getFileLocation = (config, filename) => {
return Promise.resolve(`http://example.com/${filename}`);
};

const controllerWithOnlyFilename = new FilesController(adapterWithOnlyFilename, null, { preserveFileName: true });
const result = await controllerWithOnlyFilename.createFile(
config,
'originalFile.txt',
'data',
'text/plain',
{}
);

expect(result.name).toBe('newname.txt');
expect(result.url).toBe('http://example.com/newname.txt');
});
});
8 changes: 5 additions & 3 deletions src/Adapters/Files/FilesAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@ export class FilesAdapter {
* @discussion the contentType can be undefined if the controller was not able to determine it
* @param {object} options - (Optional) options to be passed to file adapter (S3 File Adapter Only)
* - tags: object containing key value pairs that will be stored with file
* - metadata: object containing key value pairs that will be sotred with file (https://docs.aws.amazon.com/AmazonS3/latest/user-guide/add-object-metadata.html)
* - metadata: object containing key value pairs that will be stored with file (https://docs.aws.amazon.com/AmazonS3/latest/user-guide/add-object-metadata.html)
* @discussion options are not supported by all file adapters. Check the your adapter's documentation for compatibility
* @param {Config} config - (Optional) server configuration
Comment thread
mtrezza marked this conversation as resolved.
* @discussion config may be passed to adapter to allow for more complex configuration and internal call of getFileLocation (if needed). This argument is not supported by all file adapters. Check the your adapter's documentation for compatibility
*
* @return {Promise} a promise that should fail if the storage didn't succeed
* @return {Promise<{url?: string, name?: string, location?: string}>|Promise<undefined>} Either a plain promise that should fail if storage didn't succeed, or a promise resolving to an object containing url and/or an updated filename and/or location (if relevant)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

location in the return type is never consumed and ambiguous vs. url

The documented return shape has both url and location, but FilesRouter (Context Snippet 4) only reads createFileResult.url and createFileResult.name. An adapter that returns { location: 'https://…' } instead of { url: 'https://…' } will silently produce an undefined URL for the stored file.

Either drop the location field from the documented return type, or (if it's intentional as an alias) document exactly how the controller will resolve precedence between url and location, and ensure the controller code actually implements that resolution.

📝 Proposed fix
-   * `@return` {Promise<{url?: string, name?: string, location?: string}>|Promise<undefined>} Either a plain promise that should fail if storage didn't succeed, or a promise resolving to an object containing url and/or an updated filename and/or location (if relevant)
+   * `@return` {Promise<{url?: string, name?: string}>|Promise<undefined>} Either a plain promise that should fail if storage didn't succeed, or a promise resolving to an object containing the file URL (url) and/or the stored filename (name) if they differ from the input.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Adapters/Files/FilesAdapter.js` at line 39, The return type docs for
FilesAdapter list both url and location but FilesRouter only reads
createFileResult.url and createFileResult.name, so adapters returning {location:
'…'} produce undefined URLs; either remove the ambiguous location field from
FilesAdapter.js JSDoc, or (preferred) update the controller/FilesRouter to
resolve an alias by reading createFileResult.url ?? createFileResult.location
(use url if present, otherwise location) and document that precedence; search
for FilesAdapter, FilesRouter, and createFileResult in the diff to implement the
consistent resolution and update the JSDoc accordingly.

*/
createFile(filename: string, data, contentType: string, options: Object): Promise {}
createFile(filename: string, data, contentType: string, options: Object, config: Config): Promise {}
Comment on lines +36 to +41
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether FilesController.createFile passes config to adapter and consumes return value
rg -n "adapter\.createFile" src/Controllers/FilesController.js -A 3

Repository: parse-community/parse-server

Length of output: 213


FilesController.createFile must pass config to the adapter and use its return value

The adapter interface documents that config is forwarded as a 5th parameter and that an adapter may return {url, name} to override defaults. However, src/Controllers/FilesController.js:49 currently calls the adapter without passing config and discards its return value:

await this.adapter.createFile(filename, data, contentType, options);
return {
  url: location,
  name: filename,
};

This means:

  1. Adapters receive undefined for config, causing any internal getFileLocation call to fail.
  2. Adapter-returned values are silently ignored; callers always receive the pre-adapter filename and URL.

Update the controller to pass config and consume the adapter's response:

Proposed fix
-    const location = await this.adapter.getFileLocation(config, filename);
-    await this.adapter.createFile(filename, data, contentType, options);
-    return {
-      url: location,
-      name: filename,
-    }
+    const adapterResult = await this.adapter.createFile(filename, data, contentType, options, config);
+    const resolvedName = (adapterResult && adapterResult.name) ? adapterResult.name : filename;
+    const resolvedUrl  = (adapterResult && adapterResult.url)
+      ? adapterResult.url
+      : await this.adapter.getFileLocation(config, resolvedName);
+    return {
+      url: resolvedUrl,
+      name: resolvedName,
+    };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Adapters/Files/FilesAdapter.js` around lines 36 - 41,
FilesController.createFile currently calls this.adapter.createFile without the
config and ignores its return value; update the call to await
this.adapter.createFile(filename, data, contentType, options, config) (passing
config as the 5th parameter) and capture its result (e.g., const adapterResult =
await ...). Then merge adapterResult into the response: if adapterResult.name
override the filename, if adapterResult.url or adapterResult.location override
the returned url/location, and include any other adapterResult fields as
appropriate; if adapterResult is undefined, fall back to the original url/name
logic. Ensure the function references FilesController.createFile and the
adapter.createFile signature from FilesAdapter.js.


/** Whether this adapter supports receiving Readable streams in createFile().
* If false (default), streams are buffered to a Buffer before being passed.
Expand Down
Loading