Skip to content

Commit b2beaa8

Browse files
authored
feat: Add Cloud Code triggers Parse.Cloud.beforeFind(Parse.File)and Parse.Cloud.afterFind(Parse.File) (#8700)
1 parent 042a920 commit b2beaa8

File tree

3 files changed

+181
-20
lines changed

3 files changed

+181
-20
lines changed

spec/CloudCode.spec.js

+120
Original file line numberDiff line numberDiff line change
@@ -3929,6 +3929,126 @@ describe('saveFile hooks', () => {
39293929
});
39303930
});
39313931

3932+
describe('Parse.File hooks', () => {
3933+
it('find hooks should run', async () => {
3934+
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
3935+
await file.save({ useMasterKey: true });
3936+
const user = await Parse.User.signUp('username', 'password');
3937+
const hooks = {
3938+
beforeFind(req) {
3939+
expect(req).toBeDefined();
3940+
expect(req.file).toBeDefined();
3941+
expect(req.triggerName).toBe('beforeFind');
3942+
expect(req.master).toBeFalse();
3943+
expect(req.log).toBeDefined();
3944+
},
3945+
afterFind(req) {
3946+
expect(req).toBeDefined();
3947+
expect(req.file).toBeDefined();
3948+
expect(req.triggerName).toBe('afterFind');
3949+
expect(req.master).toBeFalse();
3950+
expect(req.log).toBeDefined();
3951+
expect(req.forceDownload).toBeFalse();
3952+
},
3953+
};
3954+
for (const hook in hooks) {
3955+
spyOn(hooks, hook).and.callThrough();
3956+
Parse.Cloud[hook](Parse.File, hooks[hook]);
3957+
}
3958+
await request({
3959+
url: file.url(),
3960+
headers: {
3961+
'X-Parse-Application-Id': 'test',
3962+
'X-Parse-REST-API-Key': 'rest',
3963+
'X-Parse-Session-Token': user.getSessionToken(),
3964+
},
3965+
});
3966+
for (const hook in hooks) {
3967+
expect(hooks[hook]).toHaveBeenCalled();
3968+
}
3969+
});
3970+
3971+
it('beforeFind can throw', async () => {
3972+
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
3973+
await file.save({ useMasterKey: true });
3974+
const user = await Parse.User.signUp('username', 'password');
3975+
const hooks = {
3976+
beforeFind() {
3977+
throw 'unauthorized';
3978+
},
3979+
afterFind() {},
3980+
};
3981+
for (const hook in hooks) {
3982+
spyOn(hooks, hook).and.callThrough();
3983+
Parse.Cloud[hook](Parse.File, hooks[hook]);
3984+
}
3985+
await expectAsync(
3986+
request({
3987+
url: file.url(),
3988+
headers: {
3989+
'X-Parse-Application-Id': 'test',
3990+
'X-Parse-REST-API-Key': 'rest',
3991+
'X-Parse-Session-Token': user.getSessionToken(),
3992+
},
3993+
}).catch(e => {
3994+
throw new Parse.Error(e.data.code, e.data.error);
3995+
})
3996+
).toBeRejectedWith(new Parse.Error(Parse.Error.SCRIPT_FAILED, 'unauthorized'));
3997+
3998+
expect(hooks.beforeFind).toHaveBeenCalled();
3999+
expect(hooks.afterFind).not.toHaveBeenCalled();
4000+
});
4001+
4002+
it('afterFind can throw', async () => {
4003+
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
4004+
await file.save({ useMasterKey: true });
4005+
const user = await Parse.User.signUp('username', 'password');
4006+
const hooks = {
4007+
beforeFind() {},
4008+
afterFind() {
4009+
throw 'unauthorized';
4010+
},
4011+
};
4012+
for (const hook in hooks) {
4013+
spyOn(hooks, hook).and.callThrough();
4014+
Parse.Cloud[hook](Parse.File, hooks[hook]);
4015+
}
4016+
await expectAsync(
4017+
request({
4018+
url: file.url(),
4019+
headers: {
4020+
'X-Parse-Application-Id': 'test',
4021+
'X-Parse-REST-API-Key': 'rest',
4022+
'X-Parse-Session-Token': user.getSessionToken(),
4023+
},
4024+
}).catch(e => {
4025+
throw new Parse.Error(e.data.code, e.data.error);
4026+
})
4027+
).toBeRejectedWith(new Parse.Error(Parse.Error.SCRIPT_FAILED, 'unauthorized'));
4028+
for (const hook in hooks) {
4029+
expect(hooks[hook]).toHaveBeenCalled();
4030+
}
4031+
});
4032+
4033+
it('can force download', async () => {
4034+
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
4035+
await file.save({ useMasterKey: true });
4036+
const user = await Parse.User.signUp('username', 'password');
4037+
Parse.Cloud.afterFind(Parse.File, req => {
4038+
req.forceDownload = true;
4039+
});
4040+
const response = await request({
4041+
url: file.url(),
4042+
headers: {
4043+
'X-Parse-Application-Id': 'test',
4044+
'X-Parse-REST-API-Key': 'rest',
4045+
'X-Parse-Session-Token': user.getSessionToken(),
4046+
},
4047+
});
4048+
expect(response.headers['content-disposition']).toBe(`attachment;filename=${file._name}`);
4049+
});
4050+
});
4051+
39324052
describe('Cloud Config hooks', () => {
39334053
function testConfig() {
39344054
return Parse.Config.save({ internal: 'i', string: 's', number: 12 }, { internal: true });

src/Routers/FilesRouter.js

+58-20
Original file line numberDiff line numberDiff line change
@@ -73,30 +73,68 @@ export class FilesRouter {
7373
res.json({ code: err.code, error: err.message });
7474
return;
7575
}
76-
const filesController = config.filesController;
77-
const filename = req.params.filename;
78-
const mime = (await import('mime')).default;
79-
const contentType = mime.getType(filename);
80-
if (isFileStreamable(req, filesController)) {
81-
filesController.handleFileStream(config, filename, req, res, contentType).catch(() => {
82-
res.status(404);
83-
res.set('Content-Type', 'text/plain');
84-
res.end('File not found.');
85-
});
86-
} else {
87-
filesController
88-
.getFileData(config, filename)
89-
.then(data => {
90-
res.status(200);
91-
res.set('Content-Type', contentType);
92-
res.set('Content-Length', data.length);
93-
res.end(data);
94-
})
95-
.catch(() => {
76+
77+
let filename = req.params.filename;
78+
try {
79+
const filesController = config.filesController;
80+
const mime = (await import('mime')).default;
81+
let contentType = mime.getType(filename);
82+
let file = new Parse.File(filename, { base64: '' }, contentType);
83+
const triggerResult = await triggers.maybeRunFileTrigger(
84+
triggers.Types.beforeFind,
85+
{ file },
86+
config,
87+
req.auth
88+
);
89+
if (triggerResult?.file?._name) {
90+
filename = triggerResult?.file?._name;
91+
contentType = mime.getType(filename);
92+
}
93+
94+
if (isFileStreamable(req, filesController)) {
95+
filesController.handleFileStream(config, filename, req, res, contentType).catch(() => {
9696
res.status(404);
9797
res.set('Content-Type', 'text/plain');
9898
res.end('File not found.');
9999
});
100+
return;
101+
}
102+
103+
let data = await filesController.getFileData(config, filename).catch(() => {
104+
res.status(404);
105+
res.set('Content-Type', 'text/plain');
106+
res.end('File not found.');
107+
});
108+
if (!data) {
109+
return;
110+
}
111+
file = new Parse.File(filename, { base64: data.toString('base64') }, contentType);
112+
const afterFind = await triggers.maybeRunFileTrigger(
113+
triggers.Types.afterFind,
114+
{ file, forceDownload: false },
115+
config,
116+
req.auth
117+
);
118+
119+
if (afterFind?.file) {
120+
contentType = mime.getType(afterFind.file._name);
121+
data = Buffer.from(afterFind.file._data, 'base64');
122+
}
123+
124+
res.status(200);
125+
res.set('Content-Type', contentType);
126+
res.set('Content-Length', data.length);
127+
if (afterFind.forceDownload) {
128+
res.set('Content-Disposition', `attachment;filename=${afterFind.file._name}`);
129+
}
130+
res.end(data);
131+
} catch (e) {
132+
const err = triggers.resolveError(e, {
133+
code: Parse.Error.SCRIPT_FAILED,
134+
message: `Could not find file: ${filename}.`,
135+
});
136+
res.status(403);
137+
res.json({ code: err.code, error: err.message });
100138
}
101139
}
102140

src/triggers.js

+3
Original file line numberDiff line numberDiff line change
@@ -1004,6 +1004,9 @@ export async function maybeRunFileTrigger(triggerType, fileObject, config, auth)
10041004
return fileObject;
10051005
}
10061006
const result = await fileTrigger(request);
1007+
if (request.forceDownload) {
1008+
fileObject.forceDownload = true;
1009+
}
10071010
logTriggerSuccessBeforeHook(
10081011
triggerType,
10091012
'Parse.File',

0 commit comments

Comments
 (0)