Skip to content

feat: Allow File adapter to create file with specific locations or dynamic filenames#9557

Open
AdrianCurtin wants to merge 17 commits intoparse-community:alphafrom
AdrianCurtin:alpha
Open

feat: Allow File adapter to create file with specific locations or dynamic filenames#9557
AdrianCurtin wants to merge 17 commits intoparse-community:alphafrom
AdrianCurtin:alpha

Conversation

@AdrianCurtin
Copy link
Copy Markdown

@AdrianCurtin AdrianCurtin commented Jan 16, 2025

Allow file adapter to override file name and location + give createFile access to config to allow getFileLocation calls internally

File adapter may specify a different filename or location during creation which should be returned after content storage.

Example, adapter timestamps upload filenames. getFileLocation may result in a slightly different url than the one used to create the file.

In the case where the url returned is identical this is not a problem. Additionally file adapter could call getFile location internally if it wanted to (and should probably have access to config for that reason).

In principle, if user wanted to name every file uploaded in an ordinal fashion (eg 1.jpg,2.jpg,3.jpg), they should allowed to do this.

As a separate improvement, this allows the url to be assigned during createFile as well (to prevent extra calls if the url is already known). Passing config to createFile allows getFileLocation to be called internally). Otherwise if url is not assigned, then getFileLocation can be called to retrieve it with the updated filename.

If url is not provided and filename is unchanged by createFile, then this code will not alter current functionality

Pull Request

Issue

Closes: #9556

Approach

  1. Allow file location and filename as generated to be overwritten by adapter.createFile if the file adapter desires
  2. Pass config as additional argument to fileadapter so that the adapter can call getFileLocation during createFile if desired (optional)

Tasks

  • Add tests
  • Add changes to documentation (guides, repository pages, code comments)
  • Add security check

Summary by CodeRabbit

  • Tests

    • Updated file save behavior tests to verify configuration is passed to file adapter
    • Added comprehensive file controller tests for different adapter response scenarios
  • Refactor

    • File adapter API updated to accept configuration parameter

@parse-github-assistant
Copy link
Copy Markdown

parse-github-assistant Bot commented Jan 16, 2025

Thanks for opening this pull request!

@AdrianCurtin AdrianCurtin changed the title Allow File adapter to create file with specific locations Feature: Allow File adapter to create file with specific locations or dynamic filenames Jan 16, 2025
@AdrianCurtin AdrianCurtin changed the title Feature: Allow File adapter to create file with specific locations or dynamic filenames feat: Allow File adapter to create file with specific locations or dynamic filenames Jan 16, 2025
Copy link
Copy Markdown
Member

@mtrezza mtrezza left a comment

Choose a reason for hiding this comment

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

Could you add a test for this?

Comment thread src/Adapters/Files/FilesAdapter.js Outdated
@AdrianCurtin
Copy link
Copy Markdown
Author

@mtrezza
Tests added

@AdrianCurtin
Copy link
Copy Markdown
Author

@mtrezza
Adjusted cloud code createFileSpy and mock adapter in tests, if you can re-run

@mtrezza mtrezza requested a review from a team February 7, 2025 01:19
@mtrezza
Copy link
Copy Markdown
Member

mtrezza commented Mar 7, 2025

@AdrianCurtin Thank you for picking this up again, I've restarted the CI, let's see...

Comment thread src/Adapters/Files/FilesAdapter.js
Comment thread spec/FilesController.spec.js Outdated
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Aug 13, 2025

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR extends the FilesAdapter.createFile method signature to accept a server config parameter, enabling file adapters to access configuration context during file creation. Test assertions are updated to verify the config is passed, and new test cases verify the controller correctly handles various adapter return value shapes.

Changes

File Adapter Configuration Extension

Layer / File(s) Summary
Adapter Interface Update
src/Adapters/Files/FilesAdapter.js
createFile method signature adds a 5th parameter config: Config. JSDoc is updated to document the config parameter, fix a "stored" typo in metadata description, and expand return type to indicate { url?, name?, location? } or undefined.
CloudCode Integration Tests
spec/CloudCode.spec.js
Three beforeSave(Parse.File) test cases updated to assert that createFileSpy receives the config object jasmine.objectContaining({ applicationId: 'test' }) as the 5th argument.
FilesController Contract Tests
spec/FilesController.spec.js
Four new test cases added verifying FilesController#createFile correctly handles adapter responses: returns { name, url }, returns undefined, returns { url } only, or returns { name } only; controller applies getFileLocation for URL generation when needed.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • #10076: Modifies FilesControllerFilesAdapter interaction to handle directory resolution and adapter return value shapes, directly complementing this config parameter addition.
🚥 Pre-merge checks | ✅ 6 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Engage In Review Feedback ⚠️ Warning FilesController.js was not modified to pass config parameter to adapter.createFile() or consume the adapter's return value as required by review feedback. Pass config as 5th parameter to adapter.createFile(), capture its return value, and use adapter-returned url/name fields with defaults as fallback.
✅ Passed checks (6 passed)
Check name Status Explanation
Title check ✅ Passed The PR title begins with 'feat:' prefix and clearly summarizes the main change: allowing file adapters to create files with specific locations or dynamic filenames.
Description check ✅ Passed The PR description follows the template structure with Issue, Approach, and Tasks sections properly completed with relevant details.
Linked Issues check ✅ Passed The code changes fully address issue #9556 requirements: adapters can now override filename/location via createFile return value, config is passed for getFileLocation access, and backward compatibility is maintained.
Out of Scope Changes check ✅ Passed All changes in CloudCode.spec.js, FilesController.spec.js, and FilesAdapter.js are directly scoped to supporting the new config parameter and file adapter response handling objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Security Check ✅ Passed The FilesAdapter.createFile method's config parameter is currently documented but not passed by FilesController, preventing config exposure. Return values are not validated, but existing security measures apply.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@parseplatformorg
Copy link
Copy Markdown
Contributor

parseplatformorg commented Aug 13, 2025

🎉 Snyk checks have passed. No issues have been found so far.

security/snyk check is complete. No issues have been found. (View Details)

coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 13, 2025
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 13, 2025
@AdrianCurtin
Copy link
Copy Markdown
Author

@mtrezza

In order to get this to pass the CI checks, I limited the server config access to just app id, mount and the legacy filekey, since that's what it looks like all of the current file adapters are using. If something else is required we could add it or weaken the tests so that a less precise config could be checked. lmk what you think

Comment thread spec/FilesController.spec.js Outdated
@codecov
Copy link
Copy Markdown

codecov Bot commented Aug 14, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.57%. Comparing base (82fdb0d) to head (46e277b).

Additional details and impacted files
@@            Coverage Diff             @@
##            alpha    #9557      +/-   ##
==========================================
- Coverage   92.99%   92.57%   -0.43%     
==========================================
  Files         187      187              
  Lines       15096    15097       +1     
  Branches      174      174              
==========================================
- Hits        14039    13976      -63     
- Misses       1045     1105      +60     
- Partials       12       16       +4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread spec/CloudCode.spec.js Outdated
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 14, 2025
@AdrianCurtin
Copy link
Copy Markdown
Author

AdrianCurtin commented Sep 10, 2025

@Moumouls
In the s3 adapter modification, the config is only used to return the getFileLocation internally AFTER creating the file/ modifying the file key

So really calling getFileLocation here is just a thing that would happen if the url wasn't provided by the createFile itself (current behavior). As long as getFileLocation respects the public url it should still work as expected.

As implemented, a user who does not use some argument (like generate key) to modify the filename would ideally not see any difference in behavior and the createdFile.url || getFileLocation fallback would simply prevent getFileLocation from being called twice when it already gave you an answer once. (getFileLocation function always being accessible within the file adapter itself, but requiring the config to be called).

On the other hand, if an adapter createFile opted to return a url that was different than the call from getFileLocation (for instance hypothetically a resolved ip address or something) then this would break the functionality you mentioned. But if they instead opted to not return anything or call getFileLocation internally, then there would be no change in behavior.

I delegated this to the file adapter in the case the getFileLocation might be pre-computed or some other variation and prevent some redundant processing, or some file adapter that might need to modify the filename based on the result of getFileLocation (for instance an invalid character) or something. (but I didn't do any of this in the s3 adapter pr, it just calls getFileLocation so that create file directly returns whatever it was supposed to.

@Moumouls
Copy link
Copy Markdown
Member

So to summarize, if developer use generateKey and public server url, the system give priority to created URL not the public one.

question: does the current generateKey usage with public server url and parse file proxy activated behave this way ? it could be interesting to cover this, and check if we have a related bug, or to unsure we don't break existing parse server using generateKey with parse proxy, but seems weird since your current PR here and on S3 actually fix the implementation of generateKey which is currently broken if i understand correctly

@AdrianCurtin
Copy link
Copy Markdown
Author

@Moumouls

generate_key i think is a specific s3 adapter functionality and the preserve filenames also must be enabled too, so its a double-opt-in there to even begin to play with the filenames.

Frankly i don't know if the parse file proxy works at all with S3 adapter (i have never tried).

But in theory if it worked, generate key should have nothing to do with the parse file proxy since it's exclusively modifying the filename (and potentially s3 path by extension).

The generate key does not play with the s3 root itself.

But lets say that the internal call in the s3 adapter was not to url = await getFileLocation, but instead to url = await getResolvedFileLocationNoProxy()

Then in this hypothetical example the url's would not match because getFileLocation and getResolvedFileLocationNoProxy would produce different results.

So the only way to break parse proxy behavior is to either a) use a getFileLocation that does not respect parse proxy behavior in the first place, or b) use a novel getFileLocation internally or method that does not respect the parse proxy behavior.

But option a) would obviously break that function anyways and b) might be more particular.

Maybe a note for future developers or a test should basically say that createFileresult.url should match getFileLocation unless there is a specific requirement for them to differ.

We could use an additional test for this, but that actually could be a valid behavior? Consider a situation where you want the file to be accessible once and never again, createFileResult.url could be an accessor returned by file creation and getFileLocation for generic file access.

In either case this is an opt-in behavior by the developer of the adapter, opt-out should simply return the file location.

@AdrianCurtin
Copy link
Copy Markdown
Author

Maybe a simpler answer would be that yes it gives preference to the created url, but in most cases this is expected to be the same as the public server url. Generate key should have nothing to do with it. The developer of the adapter would need to intentionally return a different created url than would be expected.

@Moumouls
Copy link
Copy Markdown
Member

okay @mtrezza seems good to me may be we need another final approval from another contributor @dplewis ? to move forward ?

Thanks @AdrianCurtin for your detailed answers and the time spent into this PR !

@mtrezza
Copy link
Copy Markdown
Member

mtrezza commented Sep 11, 2025

Could you please look at the open conversations and close the ones that are done? Would also be good to have another AI agent look over the changes and potential implications.

@AdrianCurtin
Copy link
Copy Markdown
Author

@Moumouls I think most of the conversations here are resolved. Anything I got back for this part from AI agents was mostly concerning passing the full config for the server var into the file adapter. Which could contain information if the file adapter wasn't privileged to know it. However the fact that the file adapter gets the information from getFileLocation anyways kind of makes that a moot point.

It wouldn't be a bad idea to in the future restrict which extensions/adapters get the full file adapter info, but for the sake of this particular PR that would be a much more significant revision and could break behaviors where other people use such terms. (not during create file specifically but elsewhere).

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR aims to extend Parse Server’s file upload pipeline so a files adapter can (optionally) return an updated filename and/or URL during createFile, and so adapters receive config during createFile to enable internal getFileLocation calls and other config-dependent behavior.

Changes:

  • Updates the FilesAdapter#createFile interface documentation/signature to include a config argument and allow returning { url, name, location }.
  • Adds unit tests asserting FilesController#createFile prefers adapter-returned name/url when provided, otherwise falls back to getFileLocation.
  • Updates Cloud Code save-file hook tests to expect config is forwarded to adapter.createFile.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
src/Adapters/Files/FilesAdapter.js Extends adapter interface/docs for createFile to accept config and optionally return updated file info.
spec/FilesController.spec.js Adds tests for new adapter-return behavior (name/url override vs fallback).
spec/CloudCode.spec.js Updates expectations that createFile is called with config as an additional argument.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

* @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)
*/
createFile(filename: string, data, contentType: string, options: Object): Promise {}
createFile(filename: string, data, contentType: string, options: Object, config: Config): Promise {}
Comment on lines +222 to +245
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 thread spec/CloudCode.spec.js
Comment on lines 4148 to 4154
expect(createFileSpy).toHaveBeenCalledWith(
jasmine.any(String),
newData,
'text/plain',
newOptions
newOptions,
jasmine.objectContaining({ applicationId: 'test' })
);
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Adapters/Files/FilesAdapter.js`:
- Around line 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.
- 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.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f040517c-d94d-4bf8-996d-482675f6534f

📥 Commits

Reviewing files that changed from the base of the PR and between 46e277b and 715ea9b.

📒 Files selected for processing (3)
  • spec/CloudCode.spec.js
  • spec/FilesController.spec.js
  • src/Adapters/Files/FilesAdapter.js
✅ Files skipped from review due to trivial changes (1)
  • spec/CloudCode.spec.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • spec/FilesController.spec.js

Comment on lines +36 to +41
* @param {Config} config - (Optional) server configuration
* @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)
*/
createFile(filename: string, data, contentType: string, options: Object): Promise {}
createFile(filename: string, data, contentType: string, options: Object, config: Config): Promise {}
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.

* @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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

File adapters cannot dynamically set file location or filename during file creation because location used may not yet exist

5 participants