Skip to content

feat: Add support for Parse.File.setDirectory() with master key to save file in directory - #2929

Merged
mtrezza merged 3 commits into
parse-community:alphafrom
mtrezza:feat/file-directory
Feb 25, 2026
Merged

feat: Add support for Parse.File.setDirectory() with master key to save file in directory#2929
mtrezza merged 3 commits into
parse-community:alphafrom
mtrezza:feat/file-directory

Conversation

@mtrezza

@mtrezza mtrezza commented Feb 25, 2026

Copy link
Copy Markdown
Member

Pull Request

Issue

Setting a directory in which to store a Parse.File is currently not possible, as the filename validation in Parse Server as part of the file name. The Parse Server S3 storage adapter allows to set a bucket prefix for all files, but not on a per-file level.

Approach

Add support for directory using master key when saving a Parse.File. Adds a directory property to Parse.File that allows specifying a storage directory path when saving files. The directory is prepended to the filename before passing it to the storage adapter.

The beforeSaveFile trigger can also set the directory server-side. No S3 adapter changes are needed as it already handles / in filenames.

Requires additional PR for parse-server, where integration tests will be added.

Tasks

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

Summary by CodeRabbit

  • New Features

    • File directory support: set and retrieve a storage directory for files; directory is included in save operations to help organize saved files.
  • Bug Fixes / Improvements

    • Validation for directory input (rejects empty/invalid values) and clearer error messaging when saving unsupported stream-based files with directory, metadata, or tags.

@parse-github-assistant

parse-github-assistant Bot commented Feb 25, 2026

Copy link
Copy Markdown

🚀 Thanks for opening this pull request!

@coderabbitai

coderabbitai Bot commented Feb 25, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Added optional directory support to file saves: FileSaveOptions gains directory?: string. ParseFile stores _directory with public directory() and setDirectory(directory) accessors. save() propagates options.directory and includes directory in Base64 save payloads when present.

Changes

Cohort / File(s) Summary
Core file API
src/ParseFile.ts
Added directory?: string to FileSaveOptions. Added internal _directory, directory() and setDirectory(directory: string). save() sets options.directory = this._directory and includes directory in Base64 save payloads, removing it from options after use. Adjusted save control flow and error messages to mention directory where relevant.
Tests
src/__tests__/ParseFile-test.js
Added tests for setDirectory()/directory() behavior: default undefined, rejection of non-string/empty values, inclusion/omission of directory in fileData for multiple save paths (direct, base64, buffer fallback), and updated error-message expectations.
Type declarations
types/ParseFile.d.ts
Added directory?: string to FileSaveOptions and declared `directory(): string

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client (ParseFile)
  participant API as API Controller
  participant Store as File Storage Adapter

  Client->>Client: setDirectory(dir) / prepare file
  Client->>API: POST /files with file data + options (includes directory)
  note right of API: options.directory is read and included\nin the fileData payload for Base64 saves
  API->>Store: saveFile(fileData { name, data/base64, directory? })
  Store-->>API: save result (url, name)
  API-->>Client: save response (file metadata)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description addresses all required template sections: it explains the issue, describes the approach, and marks both test and documentation tasks as complete.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title accurately describes the main feature added: support for Parse.File.setDirectory() with master key to save files in a directory.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

parseplatformorg commented Feb 25, 2026

Copy link
Copy Markdown
Contributor

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

Status Scanner Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@codecov

codecov Bot commented Feb 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (a66bb06) to head (ec1e666).
⚠️ Report is 10 commits behind head on alpha.

Additional details and impacted files
@@            Coverage Diff            @@
##             alpha     #2929   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           64        64           
  Lines         6318      6327    +9     
  Branches      1517      1532   +15     
=========================================
+ Hits          6318      6327    +9     

☔ 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.

@mtrezza mtrezza changed the title feat: Add support for directory using master key when saving a Parse.File feat: Add support for setting a directory using master key when saving a Parse.File Feb 25, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/ParseFile.ts (1)

319-365: ⚠️ Potential issue | 🔴 Critical

hasMetadataOrTags doesn't account for _directory, silently dropping directory on buffer-backed files.

When a Buffer-backed file has a directory set but no metadata/tags:

  1. hasMetadataOrTags is false (Lines 324–326 don't check _directory).
  2. The !hasMetadataOrTags && controller.saveBinary branch (Line 339) is taken.
  3. saveBinary uploads via raw binary headers and never includes directory — the value is silently discarded.

The fix is to include _directory in the condition that decides whether to use the base64/JSON encoding path:

🐛 Proposed fix
-       const hasMetadataOrTags =
+       const hasMetadataOrTagsOrDirectory =
          (this._metadata && Object.keys(this._metadata).length > 0) ||
-         (this._tags && Object.keys(this._tags).length > 0);
+         (this._tags && Object.keys(this._tags).length > 0) ||
+         !!this._directory;

-       if (this._source.format === 'stream' && hasMetadataOrTags) {
+       if (this._source.format === 'stream' && hasMetadataOrTagsOrDirectory) {
          throw new Error(
            'Cannot save a stream-based file with metadata or tags. Use a Buffer instead.'
          );
        }
        ...
-       if (!hasMetadataOrTags && controller.saveBinary) {
+       if (!hasMetadataOrTagsOrDirectory && controller.saveBinary) {
          // Binary upload via ajax
          ...
-       } else if (this._source.format === 'buffer') {
+       } else if (this._source.format === 'buffer') {

Note: The stream + directory combination will now throw the existing "Cannot save a stream-based file with metadata or tags" error, which is appropriate since streams also can't carry directory to the server. Consider updating that error message to mention directory as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ParseFile.ts` around lines 319 - 365, hasMetadataOrTags currently omits
this._directory so buffer-backed files with a directory can be routed to the
saveBinary path and lose their directory; update the hasMetadataOrTags
computation (used in the save logic around the variable name hasMetadataOrTags
and the branches that call controller.saveBinary and controller.saveBase64) to
also consider a non-empty this._directory (e.g. include (this._directory &&
this._directory.length > 0) in the OR chain), and while here update the stream
error thrown in the this._source.format === 'stream' check to mention directory
as well so it reads that streams cannot be saved with metadata, tags, or
directory.
🤖 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/__tests__/ParseFile-test.js`:
- Around line 808-868: The test points out that ParseFile instances constructed
from a Buffer that have setDirectory(...) end up using the saveBinary path which
never includes the directory in the request; update ParseFile.save (and/or
saveBinary) so that when this.directory is set (and metadata/tags are empty) the
file is uploaded via the same code path that injects fileData.directory (i.e.,
fall back to the base64/json upload or add directory to the binary payload),
specifically modify the logic in ParseFile.save and the helper saveBinary method
in ParseFile.ts to include this.directory in the outgoing request (or route to
saveBase64) so the request body contains fileData.directory for buffer-backed
files with setDirectory().

In `@src/ParseFile.ts`:
- Around line 519-529: setDirectory currently accepts any string (including
empty) and save() does not enforce the JSDoc requirement that saving with a
custom directory requires the master key; update setDirectory(directory: string)
to validate that directory is a non-empty string (throw a synchronous Error if
invalid) and modify save(options?) to synchronously check if this._directory is
set and options?.useMasterKey !== true, throwing a clear Error (e.g. "Saving to
a custom directory requires useMasterKey: true") before any async work;
reference the setDirectory, save, _directory and useMasterKey symbols when
making the changes.
- Around line 596-602: The payload construction omits an explicitly set empty
string directory because the code uses a truthy check on options.directory;
update the merge to use an explicit undefined check so empty strings are
preserved: replace the ternary that builds { ...(options.directory ? {
directory: options.directory } : {}) } with an explicit check such as {
...(options.directory !== undefined ? { directory: options.directory } : {}) }
(or alternately enforce non-empty strings in setDirectory by changing its guard
to typeof directory === 'string' && directory.length > 0); refer to the
setDirectory setter/_directory and the payload construction where
options.directory is merged into fileData.

---

Outside diff comments:
In `@src/ParseFile.ts`:
- Around line 319-365: hasMetadataOrTags currently omits this._directory so
buffer-backed files with a directory can be routed to the saveBinary path and
lose their directory; update the hasMetadataOrTags computation (used in the save
logic around the variable name hasMetadataOrTags and the branches that call
controller.saveBinary and controller.saveBase64) to also consider a non-empty
this._directory (e.g. include (this._directory && this._directory.length > 0) in
the OR chain), and while here update the stream error thrown in the
this._source.format === 'stream' check to mention directory as well so it reads
that streams cannot be saved with metadata, tags, or directory.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f88aac7 and 06b52a3.

📒 Files selected for processing (2)
  • src/ParseFile.ts
  • src/__tests__/ParseFile-test.js

Comment thread src/__tests__/ParseFile-test.js
Comment thread src/ParseFile.ts
Comment thread src/ParseFile.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@types/ParseFile.d.ts`:
- Around line 227-233: Update the JSDoc for ParseFile.save() to document the
"directory" save option (matching setDirectory(directory: string): void) and
note that providing a directory requires the Master Key; ensure the save()
options list includes the "directory" field, its type (string), purpose
(directory path where the file will be stored), and the same master-key
constraint as setDirectory so API docs remain consistent with setDirectory and
ParseFile.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 06b52a3 and 0937d22.

📒 Files selected for processing (1)
  • types/ParseFile.d.ts

Comment thread types/ParseFile.d.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/ParseFile.ts (1)

314-319: ⚠️ Potential issue | 🟠 Major

save() still doesn't enforce useMasterKey when _directory is set.

The JSDoc on setDirectory (Line 522) states "Requires the Master Key when saving", but save() has no client-side guard. A caller who sets a directory without useMasterKey: true will only get a server-side rejection instead of an immediate, clear error.

🛡️ Proposed fix — early guard in save()
  save(options?: FileSaveOptions & { requestTask?: any }): Promise<ParseFile> | undefined {
    options = options || {};
+   if (this._directory && !options.useMasterKey) {
+     throw new Error('Saving a Parse.File with a directory requires the Master Key.');
+   }
    options.requestTask = task => (this._requestTask = task);

Based on learnings: In src/ParseFile.ts, follow the existing pattern of throwing synchronous errors for validation checks that occur before any asynchronous operations begin.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ParseFile.ts` around lines 314 - 319, The save() method must enforce the
"requires master key" rule for files with a directory set: inside ParseFile.save
(before any async work), if this._directory is truthy and the passed options (or
defaulted options) do not have useMasterKey === true, throw a synchronous Error
with a clear message; mirror the validation pattern used elsewhere (e.g.,
setDirectory JSDoc) so callers get an immediate failure instead of relying on a
server-side rejection. Ensure you reference the same options object used later
(options.requestTask assignment) and use this._directory to detect the
requirement.
🧹 Nitpick comments (2)
src/ParseFile.ts (1)

596-599: Consider normalizing/validating directory value before sending to server.

The directory value from the user is passed straight into the REST payload without any sanitization (e.g., stripping leading/trailing slashes, blocking .. path traversal sequences, or rejecting special characters). While server-side validation is the ultimate safeguard, a client-side check in setDirectory could catch obvious mistakes early — for example, rejecting paths containing .. segments.

💡 Optional: basic client-side validation in setDirectory
  setDirectory(directory: string) {
-   if (typeof directory === 'string' && directory.length > 0) {
+   if (typeof directory === 'string' && directory.length > 0 && !directory.includes('..')) {
      this._directory = directory;
    }
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ParseFile.ts` around lines 596 - 599, The directory value is currently
passed directly into the REST payload; add client-side normalization and
validation in the setter (setDirectory) so the value sent in
metadata/tags/payload is safe: trim whitespace, remove leading/trailing slashes,
collapse duplicate slashes, reject or throw on any '..' path segments or
disallowed chars (e.g., control chars or null bytes), and optionally reject
empty results; ensure the normalizedDirectory (the cleaned value from
setDirectory) is used where the payload assembles directory (the object that
currently spreads ...(options.directory ? { directory: options.directory } :
{})) so only the validated/normalized value is sent.
src/__tests__/ParseFile-test.js (1)

814-874: Consider adding a test for directory propagation through the URI save path.

The URI path ({ uri: '...' }) downloads and then calls saveBase64 with the same options object. There's no test verifying that directory survives the download → saveBase64 chain for URI-backed files. A test like this would close the gap:

it('should include directory in fileData payload when saving with uri', async () => {
  const request = jest.fn((method, path) => {
    const name = path.substr(path.indexOf('/') + 1);
    return Promise.resolve({ name, url: 'https://files.example.com/a/' + name });
  });
  CoreManager.setRESTController({ request, ajax: jest.fn() });
  jest.spyOn(defaultController, 'download').mockResolvedValueOnce({
    base64: 'ParseA==',
    contentType: 'image/png',
  });

  const file = new ParseFile('parse.png', { uri: 'https://example.com/image.png' });
  file.setDirectory('user-uploads/photos');
  await file.save();

  expect(request).toHaveBeenCalledWith(
    'POST',
    'files/parse.png',
    expect.objectContaining({
      fileData: expect.objectContaining({ directory: 'user-uploads/photos' }),
    }),
    expect.any(Object)
  );
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/ParseFile-test.js` around lines 814 - 874, Missing test: ensure
directory set via ParseFile.setDirectory propagates through the URI save path
which downloads and calls saveBase64; add a unit test that creates a ParseFile
with { uri: '...' }, calls setDirectory('user-uploads/...'), mocks
defaultController.download to return base64 and contentType, sets CoreManager
RESTController with a jest.fn() request, calls file.save(), and asserts the POST
to 'files/<name>' includes fileData.directory in the payload (use
expect.objectContaining or similar to only assert directory) so the directory
survives download -> saveBase64 chain.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/ParseFile.ts`:
- Around line 314-319: The save() method must enforce the "requires master key"
rule for files with a directory set: inside ParseFile.save (before any async
work), if this._directory is truthy and the passed options (or defaulted
options) do not have useMasterKey === true, throw a synchronous Error with a
clear message; mirror the validation pattern used elsewhere (e.g., setDirectory
JSDoc) so callers get an immediate failure instead of relying on a server-side
rejection. Ensure you reference the same options object used later
(options.requestTask assignment) and use this._directory to detect the
requirement.

---

Nitpick comments:
In `@src/__tests__/ParseFile-test.js`:
- Around line 814-874: Missing test: ensure directory set via
ParseFile.setDirectory propagates through the URI save path which downloads and
calls saveBase64; add a unit test that creates a ParseFile with { uri: '...' },
calls setDirectory('user-uploads/...'), mocks defaultController.download to
return base64 and contentType, sets CoreManager RESTController with a jest.fn()
request, calls file.save(), and asserts the POST to 'files/<name>' includes
fileData.directory in the payload (use expect.objectContaining or similar to
only assert directory) so the directory survives download -> saveBase64 chain.

In `@src/ParseFile.ts`:
- Around line 596-599: The directory value is currently passed directly into the
REST payload; add client-side normalization and validation in the setter
(setDirectory) so the value sent in metadata/tags/payload is safe: trim
whitespace, remove leading/trailing slashes, collapse duplicate slashes, reject
or throw on any '..' path segments or disallowed chars (e.g., control chars or
null bytes), and optionally reject empty results; ensure the normalizedDirectory
(the cleaned value from setDirectory) is used where the payload assembles
directory (the object that currently spreads ...(options.directory ? {
directory: options.directory } : {})) so only the validated/normalized value is
sent.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0937d22 and ec1e666.

📒 Files selected for processing (2)
  • src/ParseFile.ts
  • src/__tests__/ParseFile-test.js

@mtrezza mtrezza changed the title feat: Add support for setting a directory using master key when saving a Parse.File feat: Add support for Parse.File.setDirectory() with master key to set a directory to save the file in Feb 25, 2026
@mtrezza mtrezza changed the title feat: Add support for Parse.File.setDirectory() with master key to set a directory to save the file in feat: Add support for Parse.File.setDirectory() with master key to save the file in a directory Feb 25, 2026
@mtrezza mtrezza changed the title feat: Add support for Parse.File.setDirectory() with master key to save the file in a directory feat: Add support for Parse.File.setDirectory() with master key to save file in directory Feb 25, 2026
@mtrezza
mtrezza merged commit 1923db0 into parse-community:alpha Feb 25, 2026
13 checks passed
@mtrezza
mtrezza deleted the feat/file-directory branch February 25, 2026 20:44
parseplatformorg pushed a commit that referenced this pull request Feb 25, 2026
# [8.3.0-alpha.1](8.2.0...8.3.0-alpha.1) (2026-02-25)

### Features

* Add support for `Parse.File.setDirectory()` with master key to save file in directory ([#2929](#2929)) ([1923db0](1923db0))
@parseplatformorg

Copy link
Copy Markdown
Contributor

🎉 This change has been released in version 8.3.0-alpha.1

@parseplatformorg parseplatformorg added the state:released-alpha Released as alpha version label Feb 25, 2026
parseplatformorg pushed a commit that referenced this pull request Feb 25, 2026
# [8.3.0](8.2.0...8.3.0) (2026-02-25)

### Features

* Add support for `Parse.File.setDirectory()` with master key to save file in directory ([#2929](#2929)) ([1923db0](1923db0))
@parseplatformorg

Copy link
Copy Markdown
Contributor

🎉 This change has been released in version 8.3.0

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

Labels

state:released Released as stable version state:released-alpha Released as alpha version

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants