Skip to content

Commit 1923db0

Browse files
authored
feat: Add support for Parse.File.setDirectory() with master key to save file in directory (#2929)
1 parent f88aac7 commit 1923db0

File tree

3 files changed

+183
-6
lines changed

3 files changed

+183
-6
lines changed

src/ParseFile.ts

Lines changed: 32 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,15 @@ class ParseFile {
271273
return this._tags;
272274
}
273275

276+
/**
277+
* Gets the directory of the file.
278+
*
279+
* @returns {string | undefined}
280+
*/
281+
directory(): string | undefined {
282+
return this._directory;
283+
}
284+
274285
/**
275286
* Saves the file to the Parse cloud.
276287
*
@@ -305,17 +316,19 @@ class ParseFile {
305316
options.requestTask = task => (this._requestTask = task);
306317
options.metadata = this._metadata;
307318
options.tags = this._tags;
319+
options.directory = this._directory;
308320

309321
const controller = CoreManager.getFileController();
310322
if (!this._previousSave) {
311323
if (this._source.format === 'buffer' || this._source.format === 'stream') {
312-
const hasMetadataOrTags =
324+
const hasFileData =
313325
(this._metadata && Object.keys(this._metadata).length > 0) ||
314-
(this._tags && Object.keys(this._tags).length > 0);
326+
(this._tags && Object.keys(this._tags).length > 0) ||
327+
!!this._directory;
315328

316-
if (this._source.format === 'stream' && hasMetadataOrTags) {
329+
if (this._source.format === 'stream' && hasFileData) {
317330
throw new Error(
318-
'Cannot save a stream-based file with metadata or tags. Use a Buffer instead.'
331+
'Cannot save a stream-based file with metadata, tags, or directory. Use a Buffer instead.'
319332
);
320333
}
321334
if (this._source.format === 'stream' && !controller.saveBinary) {
@@ -324,7 +337,7 @@ class ParseFile {
324337
);
325338
}
326339

327-
if (!hasMetadataOrTags && controller.saveBinary) {
340+
if (!hasFileData && controller.saveBinary) {
328341
// Binary upload via ajax
329342
this._previousSave = controller
330343
.saveBinary(this._name, this._source, options)
@@ -504,6 +517,18 @@ class ParseFile {
504517
}
505518
}
506519

520+
/**
521+
* Sets the directory where the file will be stored.
522+
* Requires the Master Key when saving.
523+
*
524+
* @param {string} directory the directory path
525+
*/
526+
setDirectory(directory: string) {
527+
if (typeof directory === 'string' && directory.length > 0) {
528+
this._directory = directory;
529+
}
530+
}
531+
507532
static fromJSON(obj): ParseFile {
508533
if (obj.__type !== 'File') {
509534
throw new TypeError('JSON object does not represent a ParseFile');
@@ -570,10 +595,12 @@ const DefaultController = {
570595
fileData: {
571596
metadata: { ...options.metadata },
572597
tags: { ...options.tags },
598+
...(options.directory ? { directory: options.directory } : {}),
573599
},
574600
};
575601
delete options.metadata;
576602
delete options.tags;
603+
delete options.directory;
577604
if (source.type) {
578605
data._ContentType = source.type;
579606
}

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: 15 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,12 @@ declare class ParseFile {
136138
* @returns {object}
137139
*/
138140
tags(): Record<string, any>;
141+
/**
142+
* Gets the directory of the file.
143+
*
144+
* @returns {string | undefined}
145+
*/
146+
directory(): string | undefined;
139147
/**
140148
* Saves the file to the Parse cloud.
141149
*
@@ -216,6 +224,13 @@ declare class ParseFile {
216224
* @param {*} value tag
217225
*/
218226
addTag(key: string, value: string): void;
227+
/**
228+
* Sets the directory where the file will be stored.
229+
* Requires the Master Key when saving.
230+
*
231+
* @param {string} directory the directory path
232+
*/
233+
setDirectory(directory: string): void;
219234
static fromJSON(obj: any): ParseFile;
220235
static encodeBase64(bytes: number[] | Uint8Array): string;
221236
}

0 commit comments

Comments
 (0)