Skip to content

Commit 3999371

Browse files
authored
build: Release (#2931)
2 parents f88aac7 + cf2fa70 commit 3999371

6 files changed

Lines changed: 197 additions & 9 deletions

File tree

changelogs/CHANGELOG_alpha.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
# [8.3.0-alpha.1](https://github.com/parse-community/Parse-SDK-JS/compare/8.2.0...8.3.0-alpha.1) (2026-02-25)
2+
3+
4+
### Features
5+
6+
* Add support for `Parse.File.setDirectory()` with master key to save file in directory ([#2929](https://github.com/parse-community/Parse-SDK-JS/issues/2929)) ([1923db0](https://github.com/parse-community/Parse-SDK-JS/commit/1923db0a4cb08394266137f99f7183218c3e3ebd))
7+
18
# [8.2.0-alpha.2](https://github.com/parse-community/Parse-SDK-JS/compare/8.2.0-alpha.1...8.2.0-alpha.2) (2026-02-20)
29

310

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "parse",
3-
"version": "8.2.0",
3+
"version": "8.3.0-alpha.1",
44
"description": "Parse JavaScript SDK",
55
"homepage": "https://parseplatform.org",
66
"keywords": [

src/ParseFile.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type FileData = number[] | Base64 | Blob | Uri;
1818
export type FileSaveOptions = FullOptions & {
1919
metadata?: Record<string, any>;
2020
tags?: Record<string, any>;
21+
directory?: string;
2122
};
2223
export type FileSource =
2324
| {
@@ -80,6 +81,7 @@ class ParseFile {
8081
_requestTask?: any;
8182
_metadata?: Record<string, any>;
8283
_tags?: Record<string, any>;
84+
_directory?: string;
8385

8486
/**
8587
* @param name {String} The file's name. This will be prefixed by a unique
@@ -271,6 +273,16 @@ class ParseFile {
271273
return this._tags;
272274
}
273275

276+
/**
277+
* Gets the directory of the file.
278+
* Requires Parse Server >= 9.4.0.
279+
*
280+
* @returns {string | undefined}
281+
*/
282+
directory(): string | undefined {
283+
return this._directory;
284+
}
285+
274286
/**
275287
* Saves the file to the Parse cloud.
276288
*
@@ -305,17 +317,19 @@ class ParseFile {
305317
options.requestTask = task => (this._requestTask = task);
306318
options.metadata = this._metadata;
307319
options.tags = this._tags;
320+
options.directory = this._directory;
308321

309322
const controller = CoreManager.getFileController();
310323
if (!this._previousSave) {
311324
if (this._source.format === 'buffer' || this._source.format === 'stream') {
312-
const hasMetadataOrTags =
325+
const hasFileData =
313326
(this._metadata && Object.keys(this._metadata).length > 0) ||
314-
(this._tags && Object.keys(this._tags).length > 0);
327+
(this._tags && Object.keys(this._tags).length > 0) ||
328+
!!this._directory;
315329

316-
if (this._source.format === 'stream' && hasMetadataOrTags) {
330+
if (this._source.format === 'stream' && hasFileData) {
317331
throw new Error(
318-
'Cannot save a stream-based file with metadata or tags. Use a Buffer instead.'
332+
'Cannot save a stream-based file with metadata, tags, or directory. Use a Buffer instead.'
319333
);
320334
}
321335
if (this._source.format === 'stream' && !controller.saveBinary) {
@@ -324,7 +338,7 @@ class ParseFile {
324338
);
325339
}
326340

327-
if (!hasMetadataOrTags && controller.saveBinary) {
341+
if (!hasFileData && controller.saveBinary) {
328342
// Binary upload via ajax
329343
this._previousSave = controller
330344
.saveBinary(this._name, this._source, options)
@@ -504,6 +518,19 @@ class ParseFile {
504518
}
505519
}
506520

521+
/**
522+
* Sets the directory where the file will be stored.
523+
* Requires the Master Key when saving.
524+
* Requires Parse Server >= 9.4.0.
525+
*
526+
* @param {string} directory the directory path
527+
*/
528+
setDirectory(directory: string) {
529+
if (typeof directory === 'string' && directory.length > 0) {
530+
this._directory = directory;
531+
}
532+
}
533+
507534
static fromJSON(obj): ParseFile {
508535
if (obj.__type !== 'File') {
509536
throw new TypeError('JSON object does not represent a ParseFile');
@@ -570,10 +597,12 @@ const DefaultController = {
570597
fileData: {
571598
metadata: { ...options.metadata },
572599
tags: { ...options.tags },
600+
...(options.directory ? { directory: options.directory } : {}),
573601
},
574602
};
575603
delete options.metadata;
576604
delete options.tags;
605+
delete options.directory;
577606
if (source.type) {
578607
data._ContentType = source.type;
579608
}

src/__tests__/ParseFile-test.js

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,33 @@ describe('ParseFile', () => {
351351
{
352352
metadata: { foo: 'bar' },
353353
tags: { bar: 'foo' },
354+
directory: undefined,
355+
requestTask: expect.any(Function),
356+
}
357+
);
358+
});
359+
360+
it('should save file with directory option', async () => {
361+
const fileController = {
362+
saveFile: jest.fn().mockResolvedValue({}),
363+
saveBase64: () => {},
364+
download: () => {},
365+
};
366+
CoreManager.setFileController(fileController);
367+
const file = new ParseFile('donald_duck.txt', new File(['Parse'], 'donald_duck.txt'));
368+
file.setDirectory('user-uploads/avatars');
369+
await file.save();
370+
expect(fileController.saveFile).toHaveBeenCalledWith(
371+
'donald_duck.txt',
372+
{
373+
file: expect.any(File),
374+
format: 'file',
375+
type: '',
376+
},
377+
{
378+
metadata: {},
379+
tags: {},
380+
directory: 'user-uploads/avatars',
354381
requestTask: expect.any(Function),
355382
}
356383
);
@@ -403,6 +430,29 @@ describe('ParseFile', () => {
403430
expect(file.tags()).toEqual({});
404431
});
405432

433+
it('should set directory', () => {
434+
const file = new ParseFile('parse.txt', [61, 170, 236, 120]);
435+
file.setDirectory('user-uploads/avatars');
436+
expect(file.directory()).toBe('user-uploads/avatars');
437+
});
438+
439+
it('should not set directory if value is not a string', () => {
440+
const file = new ParseFile('parse.txt', [61, 170, 236, 120]);
441+
file.setDirectory(123);
442+
expect(file.directory()).toBeUndefined();
443+
});
444+
445+
it('should not set directory if value is an empty string', () => {
446+
const file = new ParseFile('parse.txt', [61, 170, 236, 120]);
447+
file.setDirectory('');
448+
expect(file.directory()).toBeUndefined();
449+
});
450+
451+
it('should return undefined directory by default', () => {
452+
const file = new ParseFile('parse.txt', [61, 170, 236, 120]);
453+
expect(file.directory()).toBeUndefined();
454+
});
455+
406456
it('can create files with a Buffer', () => {
407457
const buffer = Buffer.from([61, 170, 236, 120]);
408458
const file = new ParseFile('parse.txt', buffer, 'application/octet-stream');
@@ -761,6 +811,68 @@ describe('FileController', () => {
761811
);
762812
});
763813

814+
it('should include directory in fileData payload when saving', async () => {
815+
const request = jest.fn((method, path) => {
816+
const name = path.substr(path.indexOf('/') + 1);
817+
return Promise.resolve({
818+
name: name,
819+
url: 'https://files.example.com/a/' + name,
820+
});
821+
});
822+
const ajax = function () {
823+
return Promise.resolve({ response: {} });
824+
};
825+
CoreManager.setRESTController({ request, ajax });
826+
827+
const file = new ParseFile('parse.txt', { base64: 'ParseA==' });
828+
file.setDirectory('user-uploads/avatars');
829+
await file.save();
830+
expect(request).toHaveBeenCalledWith(
831+
'POST',
832+
'files/parse.txt',
833+
{
834+
base64: 'ParseA==',
835+
_ContentType: 'text/plain',
836+
fileData: {
837+
metadata: {},
838+
tags: {},
839+
directory: 'user-uploads/avatars',
840+
},
841+
},
842+
{ requestTask: expect.any(Function) }
843+
);
844+
});
845+
846+
it('should not include directory in fileData payload when not set', async () => {
847+
const request = jest.fn((method, path) => {
848+
const name = path.substr(path.indexOf('/') + 1);
849+
return Promise.resolve({
850+
name: name,
851+
url: 'https://files.example.com/a/' + name,
852+
});
853+
});
854+
const ajax = function () {
855+
return Promise.resolve({ response: {} });
856+
};
857+
CoreManager.setRESTController({ request, ajax });
858+
859+
const file = new ParseFile('parse.txt', { base64: 'ParseA==' });
860+
await file.save();
861+
expect(request).toHaveBeenCalledWith(
862+
'POST',
863+
'files/parse.txt',
864+
{
865+
base64: 'ParseA==',
866+
_ContentType: 'text/plain',
867+
fileData: {
868+
metadata: {},
869+
tags: {},
870+
},
871+
},
872+
{ requestTask: expect.any(Function) }
873+
);
874+
});
875+
764876
it('saves files via object saveAll options', async () => {
765877
const ajax = async () => {};
766878
const request = jest.fn(async (method, path, data, options) => {
@@ -1139,7 +1251,7 @@ describe('FileController', () => {
11391251
expect(true).toBe(false);
11401252
} catch (e) {
11411253
expect(e.message).toBe(
1142-
'Cannot save a stream-based file with metadata or tags. Use a Buffer instead.'
1254+
'Cannot save a stream-based file with metadata, tags, or directory. Use a Buffer instead.'
11431255
);
11441256
}
11451257
});
@@ -1160,6 +1272,29 @@ describe('FileController', () => {
11601272
expect(request).not.toHaveBeenCalled();
11611273
});
11621274

1275+
it('buffer with directory falls back to saveBase64', async () => {
1276+
const request = jest.fn().mockResolvedValue({
1277+
name: 'parse.txt',
1278+
url: 'https://files.example.com/a/parse.txt',
1279+
});
1280+
const ajax = jest.fn();
1281+
CoreManager.setRESTController({ request, ajax });
1282+
1283+
const file = new ParseFile('parse.txt', Buffer.from([61, 170, 236, 120]), 'text/plain');
1284+
file.setDirectory('user-uploads/avatars');
1285+
await file.save();
1286+
1287+
expect(ajax).not.toHaveBeenCalled();
1288+
expect(request).toHaveBeenCalledWith(
1289+
'POST',
1290+
'files/parse.txt',
1291+
expect.objectContaining({
1292+
fileData: expect.objectContaining({ directory: 'user-uploads/avatars' }),
1293+
}),
1294+
expect.any(Object)
1295+
);
1296+
});
1297+
11631298
it('falls back to saveBase64 when controller lacks saveBinary', async () => {
11641299
CoreManager.setFileController({
11651300
saveFile: jest.fn(),

types/ParseFile.d.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ type FileData = number[] | Base64 | Blob | Uri;
99
export type FileSaveOptions = FullOptions & {
1010
metadata?: Record<string, any>;
1111
tags?: Record<string, any>;
12+
directory?: string;
1213
};
1314
export type FileSource = {
1415
format: 'file';
@@ -47,6 +48,7 @@ declare class ParseFile {
4748
_requestTask?: any;
4849
_metadata?: Record<string, any>;
4950
_tags?: Record<string, any>;
51+
_directory?: string;
5052
/**
5153
* @param name {String} The file's name. This will be prefixed by a unique
5254
* value once the file has finished saving. The file name must begin with
@@ -136,6 +138,13 @@ declare class ParseFile {
136138
* @returns {object}
137139
*/
138140
tags(): Record<string, any>;
141+
/**
142+
* Gets the directory of the file.
143+
* Requires Parse Server >= 9.4.0.
144+
*
145+
* @returns {string | undefined}
146+
*/
147+
directory(): string | undefined;
139148
/**
140149
* Saves the file to the Parse cloud.
141150
*
@@ -216,6 +225,14 @@ declare class ParseFile {
216225
* @param {*} value tag
217226
*/
218227
addTag(key: string, value: string): void;
228+
/**
229+
* Sets the directory where the file will be stored.
230+
* Requires the Master Key when saving.
231+
* Requires Parse Server >= 9.4.0.
232+
*
233+
* @param {string} directory the directory path
234+
*/
235+
setDirectory(directory: string): void;
219236
static fromJSON(obj: any): ParseFile;
220237
static encodeBase64(bytes: number[] | Uint8Array): string;
221238
}

0 commit comments

Comments
 (0)