Skip to content

Commit a9dba44

Browse files
Add file triggers and file meta data (#6344)
* added hint to aggregate * added support for hint in query * added else clause to aggregate * fixed tests * updated tests * Add tests and clean up * added beforeSaveFile and afterSaveFile triggers * Add support for explain * added some validation * added support for metadata and tags * tests? * trying tests * added tests * fixed failing tests * added some docs for fileObject * updated hooks to use Parse.File * added test for already saved file being returned in hook * added beforeDeleteFile and afterDeleteFile hooks * removed contentLength because it's already in the header * added fileSize param to FileTriggerRequest * added support for client side metadata and tags * removed fit test * removed unused import * added loging to file triggers * updated error message * updated error message * fixed tests * fixed typos * Update package.json * fixed failing test * fixed error message * fixed failing tests (hopefully) * TESTS!!! * Update FilesAdapter.js fixed comment * added test for changing file name * updated comments Co-authored-by: Diamond Lewis <[email protected]>
1 parent d48de7d commit a9dba44

File tree

9 files changed

+604
-43
lines changed

9 files changed

+604
-43
lines changed

spec/CloudCode.spec.js

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@ const request = require('../lib/request');
55
const InMemoryCacheAdapter = require('../lib/Adapters/Cache/InMemoryCacheAdapter')
66
.InMemoryCacheAdapter;
77

8+
const mockAdapter = {
9+
createFile: async (filename) => ({
10+
name: filename,
11+
location: `http://www.somewhere.com/${filename}`,
12+
}),
13+
deleteFile: () => {},
14+
getFileData: () => {},
15+
getFileLocation: (config, filename) => `http://www.somewhere.com/${filename}`,
16+
validateFilename: () => {
17+
return null;
18+
},
19+
};
20+
821
describe('Cloud Code', () => {
922
it('can load absolute cloud code file', done => {
1023
reconfigureServer({
@@ -2595,6 +2608,246 @@ describe('beforeLogin hook', () => {
25952608
expect(beforeFinds).toEqual(1);
25962609
expect(afterFinds).toEqual(1);
25972610
});
2611+
2612+
it('beforeSaveFile should not change file if nothing is returned', async () => {
2613+
await reconfigureServer({ filesAdapter: mockAdapter });
2614+
Parse.Cloud.beforeSaveFile(() => {
2615+
return;
2616+
});
2617+
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
2618+
const result = await file.save({ useMasterKey: true });
2619+
expect(result).toBe(file);
2620+
});
2621+
2622+
it('beforeSaveFile should return file that is already saved and not save anything to files adapter', async () => {
2623+
await reconfigureServer({ filesAdapter: mockAdapter });
2624+
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
2625+
Parse.Cloud.beforeSaveFile(() => {
2626+
const newFile = new Parse.File('some-file.txt');
2627+
newFile._url = 'http://www.somewhere.com/parse/files/some-app-id/some-file.txt';
2628+
return newFile;
2629+
});
2630+
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
2631+
const result = await file.save({ useMasterKey: true });
2632+
expect(result).toBe(file);
2633+
expect(result._name).toBe('some-file.txt');
2634+
expect(result._url).toBe('http://www.somewhere.com/parse/files/some-app-id/some-file.txt');
2635+
expect(createFileSpy).not.toHaveBeenCalled();
2636+
});
2637+
2638+
it('beforeSaveFile should throw error', async () => {
2639+
await reconfigureServer({ filesAdapter: mockAdapter });
2640+
Parse.Cloud.beforeSaveFile(() => {
2641+
throw new Parse.Error(400, 'some-error-message');
2642+
});
2643+
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
2644+
try {
2645+
await file.save({ useMasterKey: true });
2646+
} catch (error) {
2647+
expect(error.message).toBe('some-error-message');
2648+
}
2649+
});
2650+
2651+
it('beforeSaveFile should change values of uploaded file by editing fileObject directly', async () => {
2652+
await reconfigureServer({ filesAdapter: mockAdapter });
2653+
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
2654+
Parse.Cloud.beforeSaveFile(async (req) => {
2655+
expect(req.triggerName).toEqual('beforeSaveFile');
2656+
expect(req.master).toBe(true);
2657+
req.file.addMetadata('foo', 'bar');
2658+
req.file.addTag('tagA', 'some-tag');
2659+
});
2660+
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
2661+
const result = await file.save({ useMasterKey: true });
2662+
expect(result).toBe(file);
2663+
const newData = new Buffer([1, 2, 3]);
2664+
const newOptions = {
2665+
tags: {
2666+
tagA: 'some-tag',
2667+
},
2668+
metadata: {
2669+
foo: 'bar',
2670+
},
2671+
};
2672+
expect(createFileSpy).toHaveBeenCalledWith(jasmine.any(String), newData, 'text/plain', newOptions);
2673+
});
2674+
2675+
it('beforeSaveFile should change values by returning new fileObject', async () => {
2676+
await reconfigureServer({ filesAdapter: mockAdapter });
2677+
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
2678+
Parse.Cloud.beforeSaveFile(async (req) => {
2679+
expect(req.triggerName).toEqual('beforeSaveFile');
2680+
expect(req.fileSize).toBe(3);
2681+
const newFile = new Parse.File('donald_duck.pdf', [4, 5, 6], 'application/pdf');
2682+
newFile.setMetadata({ foo: 'bar' });
2683+
newFile.setTags({ tagA: 'some-tag' });
2684+
return newFile;
2685+
});
2686+
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
2687+
const result = await file.save({ useMasterKey: true });
2688+
expect(result).toBeInstanceOf(Parse.File);
2689+
const newData = new Buffer([4, 5, 6]);
2690+
const newContentType = 'application/pdf';
2691+
const newOptions = {
2692+
tags: {
2693+
tagA: 'some-tag',
2694+
},
2695+
metadata: {
2696+
foo: 'bar',
2697+
},
2698+
};
2699+
expect(createFileSpy).toHaveBeenCalledWith(jasmine.any(String), newData, newContentType, newOptions);
2700+
const expectedFileName = 'donald_duck.pdf';
2701+
expect(file._name.indexOf(expectedFileName)).toBe(file._name.length - expectedFileName.length);
2702+
});
2703+
2704+
it('beforeSaveFile should contain metadata and tags saved from client', async () => {
2705+
await reconfigureServer({ filesAdapter: mockAdapter });
2706+
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
2707+
Parse.Cloud.beforeSaveFile(async (req) => {
2708+
expect(req.triggerName).toEqual('beforeSaveFile');
2709+
expect(req.fileSize).toBe(3);
2710+
expect(req.file).toBeInstanceOf(Parse.File);
2711+
expect(req.file.name()).toBe('popeye.txt');
2712+
expect(req.file.metadata()).toEqual({ foo: 'bar' });
2713+
expect(req.file.tags()).toEqual({ bar: 'foo' });
2714+
});
2715+
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
2716+
file.setMetadata({ foo: 'bar' });
2717+
file.setTags({ bar: 'foo' });
2718+
const result = await file.save({ useMasterKey: true });
2719+
expect(result).toBeInstanceOf(Parse.File);
2720+
const options = {
2721+
metadata: { foo: 'bar' },
2722+
tags: { bar: 'foo' },
2723+
};
2724+
expect(createFileSpy).toHaveBeenCalledWith(jasmine.any(String), jasmine.any(Buffer), 'text/plain', options);
2725+
});
2726+
2727+
it('beforeSaveFile should return same file data with new file name', async () => {
2728+
await reconfigureServer({ filesAdapter: mockAdapter });
2729+
const config = Config.get('test');
2730+
config.filesController.options.preserveFileName = true;
2731+
Parse.Cloud.beforeSaveFile(async ({ file }) => {
2732+
expect(file.name()).toBe('popeye.txt');
2733+
const fileData = await file.getData();
2734+
const newFile = new Parse.File('2020-04-01.txt', { base64: fileData });
2735+
return newFile;
2736+
});
2737+
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
2738+
const result = await file.save({ useMasterKey: true });
2739+
expect(result.name()).toBe('2020-04-01.txt');
2740+
});
2741+
2742+
it('afterSaveFile should set fileSize to null if beforeSave returns an already saved file', async () => {
2743+
await reconfigureServer({ filesAdapter: mockAdapter });
2744+
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
2745+
Parse.Cloud.beforeSaveFile((req) => {
2746+
expect(req.fileSize).toBe(3);
2747+
const newFile = new Parse.File('some-file.txt');
2748+
newFile._url = 'http://www.somewhere.com/parse/files/some-app-id/some-file.txt';
2749+
return newFile;
2750+
});
2751+
Parse.Cloud.afterSaveFile((req) => {
2752+
expect(req.fileSize).toBe(null);
2753+
});
2754+
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
2755+
const result = await file.save({ useMasterKey: true });
2756+
expect(result).toBe(result);
2757+
expect(result._name).toBe('some-file.txt');
2758+
expect(result._url).toBe('http://www.somewhere.com/parse/files/some-app-id/some-file.txt');
2759+
expect(createFileSpy).not.toHaveBeenCalled();
2760+
});
2761+
2762+
it('afterSaveFile should throw error', async () => {
2763+
await reconfigureServer({ filesAdapter: mockAdapter });
2764+
Parse.Cloud.afterSaveFile(async () => {
2765+
throw new Parse.Error(400, 'some-error-message');
2766+
});
2767+
const filename = 'donald_duck.pdf';
2768+
const file = new Parse.File(filename, [1, 2, 3], 'text/plain');
2769+
try {
2770+
await file.save({ useMasterKey: true });
2771+
} catch (error) {
2772+
expect(error.message).toBe('some-error-message');
2773+
}
2774+
});
2775+
2776+
it('afterSaveFile should call with fileObject', async (done) => {
2777+
await reconfigureServer({ filesAdapter: mockAdapter });
2778+
Parse.Cloud.beforeSaveFile(async (req) => {
2779+
req.file.setTags({ tagA: 'some-tag' });
2780+
req.file.setMetadata({ foo: 'bar' });
2781+
});
2782+
Parse.Cloud.afterSaveFile(async (req) => {
2783+
expect(req.master).toBe(true);
2784+
expect(req.file._tags).toEqual({ tagA: 'some-tag' });
2785+
expect(req.file._metadata).toEqual({ foo: 'bar' });
2786+
done();
2787+
});
2788+
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
2789+
await file.save({ useMasterKey: true });
2790+
});
2791+
2792+
it('afterSaveFile should change fileSize when file data changes', async (done) => {
2793+
await reconfigureServer({ filesAdapter: mockAdapter });
2794+
Parse.Cloud.beforeSaveFile(async (req) => {
2795+
expect(req.fileSize).toBe(3);
2796+
expect(req.master).toBe(true);
2797+
const newFile = new Parse.File('donald_duck.pdf', [4, 5, 6, 7, 8, 9], 'application/pdf');
2798+
return newFile;
2799+
});
2800+
Parse.Cloud.afterSaveFile(async (req) => {
2801+
expect(req.fileSize).toBe(6);
2802+
expect(req.master).toBe(true);
2803+
done();
2804+
});
2805+
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
2806+
await file.save({ useMasterKey: true });
2807+
});
2808+
2809+
it('beforeDeleteFile should call with fileObject', async () => {
2810+
await reconfigureServer({ filesAdapter: mockAdapter });
2811+
Parse.Cloud.beforeDeleteFile((req) => {
2812+
expect(req.file).toBeInstanceOf(Parse.File);
2813+
expect(req.file._name).toEqual('popeye.txt');
2814+
expect(req.file._url).toEqual('http://www.somewhere.com/popeye.txt');
2815+
expect(req.fileSize).toBe(null);
2816+
});
2817+
const file = new Parse.File('popeye.txt');
2818+
await file.destroy({ useMasterKey: true });
2819+
});
2820+
2821+
it('beforeDeleteFile should throw error', async (done) => {
2822+
await reconfigureServer({ filesAdapter: mockAdapter });
2823+
Parse.Cloud.beforeDeleteFile(() => {
2824+
throw new Error('some error message');
2825+
});
2826+
const file = new Parse.File('popeye.txt');
2827+
try {
2828+
await file.destroy({ useMasterKey: true });
2829+
} catch (error) {
2830+
expect(error.message).toBe('some error message');
2831+
done();
2832+
}
2833+
})
2834+
2835+
it('afterDeleteFile should call with fileObject', async (done) => {
2836+
await reconfigureServer({ filesAdapter: mockAdapter });
2837+
Parse.Cloud.beforeDeleteFile((req) => {
2838+
expect(req.file).toBeInstanceOf(Parse.File);
2839+
expect(req.file._name).toEqual('popeye.txt');
2840+
expect(req.file._url).toEqual('http://www.somewhere.com/popeye.txt');
2841+
});
2842+
Parse.Cloud.afterDeleteFile((req) => {
2843+
expect(req.file).toBeInstanceOf(Parse.File);
2844+
expect(req.file._name).toEqual('popeye.txt');
2845+
expect(req.file._url).toEqual('http://www.somewhere.com/popeye.txt');
2846+
done();
2847+
});
2848+
const file = new Parse.File('popeye.txt');
2849+
await file.destroy({ useMasterKey: true });
2850+
});
25982851
});
25992852

26002853
describe('afterLogin hook', () => {

spec/FilesController.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ describe('FilesController', () => {
7070
expect(log1.level).toBe('error');
7171

7272
const log2 = logs.find(
73-
x => x.message === 'Could not store file: yolo.txt.'
73+
x => x.message === 'it failed with xyz'
7474
);
7575
expect(log2.level).toBe('error');
7676
expect(log2.code).toBe(130);

spec/ParseFile.spec.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -626,7 +626,11 @@ describe('Parse.File testing', () => {
626626
}).then(fail, response => {
627627
expect(response.status).toBe(400);
628628
const body = response.text;
629-
expect(body).toEqual('{"code":153,"error":"Could not delete file."}');
629+
expect(typeof body).toBe('string');
630+
const { code, error } = JSON.parse(body);
631+
expect(code).toBe(153);
632+
expect(typeof error).toBe('string');
633+
expect(error.length).toBeGreaterThan(0);
630634
done();
631635
});
632636
});

src/Adapters/Files/FilesAdapter.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,19 @@ export class FilesAdapter {
3131
* @param {*} data - the buffer of data from the file
3232
* @param {string} contentType - the supposed contentType
3333
* @discussion the contentType can be undefined if the controller was not able to determine it
34+
* @param {object} options - (Optional) options to be passed to file adapter (S3 File Adapter Only)
35+
* - tags: object containing key value pairs that will be stored with file
36+
* - 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)
37+
* @discussion options are not supported by all file adapters. Check the your adapter's documentation for compatibility
3438
*
3539
* @return {Promise} a promise that should fail if the storage didn't succeed
3640
*/
37-
createFile(filename: string, data, contentType: string): Promise {}
41+
createFile(
42+
filename: string,
43+
data,
44+
contentType: string,
45+
options: Object
46+
): Promise {}
3847

3948
/** Responsible for deleting the specified file
4049
*

src/Controllers/FilesController.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export class FilesController extends AdaptableController {
1515
return this.adapter.getFileData(filename);
1616
}
1717

18-
createFile(config, filename, data, contentType) {
18+
createFile(config, filename, data, contentType, options) {
1919
const extname = path.extname(filename);
2020

2121
const hasExtension = extname.length > 0;
@@ -31,12 +31,14 @@ export class FilesController extends AdaptableController {
3131
}
3232

3333
const location = this.adapter.getFileLocation(config, filename);
34-
return this.adapter.createFile(filename, data, contentType).then(() => {
35-
return Promise.resolve({
36-
url: location,
37-
name: filename,
34+
return this.adapter
35+
.createFile(filename, data, contentType, options)
36+
.then(() => {
37+
return Promise.resolve({
38+
url: location,
39+
name: filename,
40+
});
3841
});
39-
});
4042
}
4143

4244
deleteFile(config, filename) {

0 commit comments

Comments
 (0)