Skip to content

Commit 90a1e4a

Browse files
authored
feat: Add Cloud Code triggers Parse.Cloud.beforeSave and Parse.Cloud.afterSave for Parse Config (#9232)
1 parent 4d86ace commit 90a1e4a

File tree

5 files changed

+308
-5
lines changed

5 files changed

+308
-5
lines changed

spec/CloudCode.Validator.spec.js

+72-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ const validatorFail = () => {
66
const validatorSuccess = () => {
77
return true;
88
};
9+
function testConfig() {
10+
return Parse.Config.save({ internal: 'i', string: 's', number: 12 }, { internal: true });
11+
}
912

1013
describe('cloud validator', () => {
1114
it('complete validator', async done => {
@@ -731,6 +734,38 @@ describe('cloud validator', () => {
731734
done();
732735
});
733736

737+
it('basic beforeSave Parse.Config skipWithMasterKey', async () => {
738+
Parse.Cloud.beforeSave(
739+
Parse.Config,
740+
() => {
741+
throw 'beforeSaveFile should have resolved using master key.';
742+
},
743+
{
744+
skipWithMasterKey: true,
745+
}
746+
);
747+
const config = await testConfig();
748+
expect(config.get('internal')).toBe('i');
749+
expect(config.get('string')).toBe('s');
750+
expect(config.get('number')).toBe(12);
751+
});
752+
753+
it('basic afterSave Parse.Config skipWithMasterKey', async () => {
754+
Parse.Cloud.afterSave(
755+
Parse.Config,
756+
() => {
757+
throw 'beforeSaveFile should have resolved using master key.';
758+
},
759+
{
760+
skipWithMasterKey: true,
761+
}
762+
);
763+
const config = await testConfig();
764+
expect(config.get('internal')).toBe('i');
765+
expect(config.get('string')).toBe('s');
766+
expect(config.get('number')).toBe(12);
767+
});
768+
734769
it('beforeSave validateMasterKey and skipWithMasterKey fail', async function (done) {
735770
Parse.Cloud.beforeSave(
736771
'BeforeSave',
@@ -1441,7 +1476,7 @@ describe('cloud validator', () => {
14411476
});
14421477

14431478
it('validate afterSaveFile fail', async done => {
1444-
Parse.Cloud.beforeSave(Parse.File, () => {}, validatorFail);
1479+
Parse.Cloud.afterSave(Parse.File, () => {}, validatorFail);
14451480
try {
14461481
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
14471482
await file.save({ useMasterKey: true });
@@ -1496,6 +1531,42 @@ describe('cloud validator', () => {
14961531
}
14971532
});
14981533

1534+
it('validate beforeSave Parse.Config', async () => {
1535+
Parse.Cloud.beforeSave(Parse.Config, () => {}, validatorSuccess);
1536+
const config = await testConfig();
1537+
expect(config.get('internal')).toBe('i');
1538+
expect(config.get('string')).toBe('s');
1539+
expect(config.get('number')).toBe(12);
1540+
});
1541+
1542+
it('validate beforeSave Parse.Config fail', async () => {
1543+
Parse.Cloud.beforeSave(Parse.Config, () => {}, validatorFail);
1544+
try {
1545+
await testConfig();
1546+
fail('cloud function should have failed.');
1547+
} catch (e) {
1548+
expect(e.code).toBe(Parse.Error.VALIDATION_ERROR);
1549+
}
1550+
});
1551+
1552+
it('validate afterSave Parse.Config', async () => {
1553+
Parse.Cloud.afterSave(Parse.Config, () => {}, validatorSuccess);
1554+
const config = await testConfig();
1555+
expect(config.get('internal')).toBe('i');
1556+
expect(config.get('string')).toBe('s');
1557+
expect(config.get('number')).toBe(12);
1558+
});
1559+
1560+
it('validate afterSave Parse.Config fail', async () => {
1561+
Parse.Cloud.afterSave(Parse.Config, () => {}, validatorFail);
1562+
try {
1563+
await testConfig();
1564+
fail('cloud function should have failed.');
1565+
} catch (e) {
1566+
expect(e.code).toBe(Parse.Error.VALIDATION_ERROR);
1567+
}
1568+
});
1569+
14991570
it('Should have validator', async done => {
15001571
Parse.Cloud.define(
15011572
'myFunction',

spec/CloudCode.spec.js

+156
Original file line numberDiff line numberDiff line change
@@ -3921,6 +3921,162 @@ describe('saveFile hooks', () => {
39213921
});
39223922
});
39233923

3924+
describe('Cloud Config hooks', () => {
3925+
function testConfig() {
3926+
return Parse.Config.save({ internal: 'i', string: 's', number: 12 }, { internal: true });
3927+
}
3928+
3929+
it('beforeSave(Parse.Config) can run hook with new config', async () => {
3930+
let count = 0;
3931+
Parse.Cloud.beforeSave(Parse.Config, (req) => {
3932+
expect(req.object).toBeDefined();
3933+
expect(req.original).toBeUndefined();
3934+
expect(req.user).toBeUndefined();
3935+
expect(req.headers).toBeDefined();
3936+
expect(req.ip).toBeDefined();
3937+
expect(req.installationId).toBeDefined();
3938+
expect(req.context).toBeDefined();
3939+
const config = req.object;
3940+
expect(config.get('internal')).toBe('i');
3941+
expect(config.get('string')).toBe('s');
3942+
expect(config.get('number')).toBe(12);
3943+
count += 1;
3944+
});
3945+
await testConfig();
3946+
const config = await Parse.Config.get({ useMasterKey: true });
3947+
expect(config.get('internal')).toBe('i');
3948+
expect(config.get('string')).toBe('s');
3949+
expect(config.get('number')).toBe(12);
3950+
expect(count).toBe(1);
3951+
});
3952+
3953+
it('beforeSave(Parse.Config) can run hook with existing config', async () => {
3954+
let count = 0;
3955+
Parse.Cloud.beforeSave(Parse.Config, (req) => {
3956+
if (count === 0) {
3957+
expect(req.object.get('number')).toBe(12);
3958+
expect(req.original).toBeUndefined();
3959+
}
3960+
if (count === 1) {
3961+
expect(req.object.get('number')).toBe(13);
3962+
expect(req.original.get('number')).toBe(12);
3963+
}
3964+
count += 1;
3965+
});
3966+
await testConfig();
3967+
await Parse.Config.save({ number: 13 });
3968+
expect(count).toBe(2);
3969+
});
3970+
3971+
it('beforeSave(Parse.Config) should not change config if nothing is returned', async () => {
3972+
let count = 0;
3973+
Parse.Cloud.beforeSave(Parse.Config, () => {
3974+
count += 1;
3975+
return;
3976+
});
3977+
await testConfig();
3978+
const config = await Parse.Config.get({ useMasterKey: true });
3979+
expect(config.get('internal')).toBe('i');
3980+
expect(config.get('string')).toBe('s');
3981+
expect(config.get('number')).toBe(12);
3982+
expect(count).toBe(1);
3983+
});
3984+
3985+
it('beforeSave(Parse.Config) throw custom error', async () => {
3986+
Parse.Cloud.beforeSave(Parse.Config, () => {
3987+
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'It should fail');
3988+
});
3989+
try {
3990+
await testConfig();
3991+
fail('error should have thrown');
3992+
} catch (e) {
3993+
expect(e.code).toBe(Parse.Error.SCRIPT_FAILED);
3994+
expect(e.message).toBe('It should fail');
3995+
}
3996+
});
3997+
3998+
it('beforeSave(Parse.Config) throw string error', async () => {
3999+
Parse.Cloud.beforeSave(Parse.Config, () => {
4000+
throw 'before save failed';
4001+
});
4002+
try {
4003+
await testConfig();
4004+
fail('error should have thrown');
4005+
} catch (e) {
4006+
expect(e.code).toBe(Parse.Error.SCRIPT_FAILED);
4007+
expect(e.message).toBe('before save failed');
4008+
}
4009+
});
4010+
4011+
it('beforeSave(Parse.Config) throw empty error', async () => {
4012+
Parse.Cloud.beforeSave(Parse.Config, () => {
4013+
throw null;
4014+
});
4015+
try {
4016+
await testConfig();
4017+
fail('error should have thrown');
4018+
} catch (e) {
4019+
expect(e.code).toBe(Parse.Error.SCRIPT_FAILED);
4020+
expect(e.message).toBe('Script failed. Unknown error.');
4021+
}
4022+
});
4023+
4024+
it('afterSave(Parse.Config) can run hook with new config', async () => {
4025+
let count = 0;
4026+
Parse.Cloud.afterSave(Parse.Config, (req) => {
4027+
expect(req.object).toBeDefined();
4028+
expect(req.original).toBeUndefined();
4029+
expect(req.user).toBeUndefined();
4030+
expect(req.headers).toBeDefined();
4031+
expect(req.ip).toBeDefined();
4032+
expect(req.installationId).toBeDefined();
4033+
expect(req.context).toBeDefined();
4034+
const config = req.object;
4035+
expect(config.get('internal')).toBe('i');
4036+
expect(config.get('string')).toBe('s');
4037+
expect(config.get('number')).toBe(12);
4038+
count += 1;
4039+
});
4040+
await testConfig();
4041+
const config = await Parse.Config.get({ useMasterKey: true });
4042+
expect(config.get('internal')).toBe('i');
4043+
expect(config.get('string')).toBe('s');
4044+
expect(config.get('number')).toBe(12);
4045+
expect(count).toBe(1);
4046+
});
4047+
4048+
it('afterSave(Parse.Config) can run hook with existing config', async () => {
4049+
let count = 0;
4050+
Parse.Cloud.afterSave(Parse.Config, (req) => {
4051+
if (count === 0) {
4052+
expect(req.object.get('number')).toBe(12);
4053+
expect(req.original).toBeUndefined();
4054+
}
4055+
if (count === 1) {
4056+
expect(req.object.get('number')).toBe(13);
4057+
expect(req.original.get('number')).toBe(12);
4058+
}
4059+
count += 1;
4060+
});
4061+
await testConfig();
4062+
await Parse.Config.save({ number: 13 });
4063+
expect(count).toBe(2);
4064+
});
4065+
4066+
it('afterSave(Parse.Config) should throw error', async () => {
4067+
Parse.Cloud.afterSave(Parse.Config, () => {
4068+
throw new Parse.Error(400, 'It should fail');
4069+
});
4070+
try {
4071+
await testConfig();
4072+
fail('error should have thrown');
4073+
} catch (e) {
4074+
expect(e.code).toBe(400);
4075+
expect(e.message).toBe('It should fail');
4076+
}
4077+
});
4078+
});
4079+
39244080
describe('sendEmail', () => {
39254081
it('can send email via Parse.Cloud', async done => {
39264082
const emailAdapter = {

src/Routers/GlobalConfigRouter.js

+41-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22
import Parse from 'parse/node';
33
import PromiseRouter from '../PromiseRouter';
44
import * as middleware from '../middlewares';
5+
import * as triggers from '../triggers';
6+
7+
const getConfigFromParams = params => {
8+
const config = new Parse.Config();
9+
for (const attr in params) {
10+
config.attributes[attr] = Parse._decode(undefined, params[attr]);
11+
}
12+
return config;
13+
};
514

615
export class GlobalConfigRouter extends PromiseRouter {
716
getGlobalConfig(req) {
@@ -30,7 +39,7 @@ export class GlobalConfigRouter extends PromiseRouter {
3039
});
3140
}
3241

33-
updateGlobalConfig(req) {
42+
async updateGlobalConfig(req) {
3443
if (req.auth.isReadOnly) {
3544
throw new Parse.Error(
3645
Parse.Error.OPERATION_FORBIDDEN,
@@ -45,9 +54,37 @@ export class GlobalConfigRouter extends PromiseRouter {
4554
acc[`masterKeyOnly.${key}`] = masterKeyOnly[key] || false;
4655
return acc;
4756
}, {});
48-
return req.config.database
49-
.update('_GlobalConfig', { objectId: '1' }, update, { upsert: true }, true)
50-
.then(() => ({ response: { result: true } }));
57+
const className = triggers.getClassName(Parse.Config);
58+
const hasBeforeSaveHook = triggers.triggerExists(className, triggers.Types.beforeSave, req.config.applicationId);
59+
const hasAfterSaveHook = triggers.triggerExists(className, triggers.Types.afterSave, req.config.applicationId);
60+
let originalConfigObject;
61+
let updatedConfigObject;
62+
const configObject = new Parse.Config();
63+
configObject.attributes = params;
64+
65+
const results = await req.config.database.find('_GlobalConfig', { objectId: '1' }, { limit: 1 });
66+
const isNew = results.length !== 1;
67+
if (!isNew && (hasBeforeSaveHook || hasAfterSaveHook)) {
68+
originalConfigObject = getConfigFromParams(results[0].params);
69+
}
70+
try {
71+
await triggers.maybeRunGlobalConfigTrigger(triggers.Types.beforeSave, req.auth, configObject, originalConfigObject, req.config, req.context);
72+
if (isNew) {
73+
await req.config.database.update('_GlobalConfig', { objectId: '1' }, update, { upsert: true }, true)
74+
updatedConfigObject = configObject;
75+
} else {
76+
const result = await req.config.database.update('_GlobalConfig', { objectId: '1' }, update, {}, true);
77+
updatedConfigObject = getConfigFromParams(result.params);
78+
}
79+
await triggers.maybeRunGlobalConfigTrigger(triggers.Types.afterSave, req.auth, updatedConfigObject, originalConfigObject, req.config, req.context);
80+
return { response: { result: true } }
81+
} catch (err) {
82+
const error = triggers.resolveError(err, {
83+
code: Parse.Error.SCRIPT_FAILED,
84+
message: 'Script failed. Unknown error.',
85+
});
86+
throw error;
87+
}
5188
}
5289

5390
mountRoutes() {

src/cloud-code/Parse.Cloud.js

+4
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,14 @@ const getRoute = parseClass => {
7979
_User: 'users',
8080
_Session: 'sessions',
8181
'@File': 'files',
82+
'@Config' : 'config',
8283
}[parseClass] || 'classes';
8384
if (parseClass === '@File') {
8485
return `/${route}/:id?(.*)`;
8586
}
87+
if (parseClass === '@Config') {
88+
return `/${route}`;
89+
}
8690
return `/${route}/${parseClass}/:id?(.*)`;
8791
};
8892
/** @namespace

src/triggers.js

+35
Original file line numberDiff line numberDiff line change
@@ -1027,3 +1027,38 @@ export async function maybeRunFileTrigger(triggerType, fileObject, config, auth)
10271027
}
10281028
return fileObject;
10291029
}
1030+
1031+
export async function maybeRunGlobalConfigTrigger(triggerType, auth, configObject, originalConfigObject, config, context) {
1032+
const GlobalConfigClassName = getClassName(Parse.Config);
1033+
const configTrigger = getTrigger(GlobalConfigClassName, triggerType, config.applicationId);
1034+
if (typeof configTrigger === 'function') {
1035+
try {
1036+
const request = getRequestObject(triggerType, auth, configObject, originalConfigObject, config, context);
1037+
await maybeRunValidator(request, `${triggerType}.${GlobalConfigClassName}`, auth);
1038+
if (request.skipWithMasterKey) {
1039+
return configObject;
1040+
}
1041+
const result = await configTrigger(request);
1042+
logTriggerSuccessBeforeHook(
1043+
triggerType,
1044+
'Parse.Config',
1045+
configObject,
1046+
result,
1047+
auth,
1048+
config.logLevels.triggerBeforeSuccess
1049+
);
1050+
return result || configObject;
1051+
} catch (error) {
1052+
logTriggerErrorBeforeHook(
1053+
triggerType,
1054+
'Parse.Config',
1055+
configObject,
1056+
auth,
1057+
error,
1058+
config.logLevels.triggerBeforeError
1059+
);
1060+
throw error;
1061+
}
1062+
}
1063+
return configObject;
1064+
}

0 commit comments

Comments
 (0)