feat: Add Cloud Code triggers Parse.Cloud.beforeSave and Parse.Cloud.afterSave for Parse Config (#9232)
This commit is contained in:
@@ -6,6 +6,9 @@ const validatorFail = () => {
|
|||||||
const validatorSuccess = () => {
|
const validatorSuccess = () => {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
function testConfig() {
|
||||||
|
return Parse.Config.save({ internal: 'i', string: 's', number: 12 }, { internal: true });
|
||||||
|
}
|
||||||
|
|
||||||
describe('cloud validator', () => {
|
describe('cloud validator', () => {
|
||||||
it('complete validator', async done => {
|
it('complete validator', async done => {
|
||||||
@@ -731,6 +734,38 @@ describe('cloud validator', () => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('basic beforeSave Parse.Config skipWithMasterKey', async () => {
|
||||||
|
Parse.Cloud.beforeSave(
|
||||||
|
Parse.Config,
|
||||||
|
() => {
|
||||||
|
throw 'beforeSaveFile should have resolved using master key.';
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skipWithMasterKey: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const config = await testConfig();
|
||||||
|
expect(config.get('internal')).toBe('i');
|
||||||
|
expect(config.get('string')).toBe('s');
|
||||||
|
expect(config.get('number')).toBe(12);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('basic afterSave Parse.Config skipWithMasterKey', async () => {
|
||||||
|
Parse.Cloud.afterSave(
|
||||||
|
Parse.Config,
|
||||||
|
() => {
|
||||||
|
throw 'beforeSaveFile should have resolved using master key.';
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skipWithMasterKey: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const config = await testConfig();
|
||||||
|
expect(config.get('internal')).toBe('i');
|
||||||
|
expect(config.get('string')).toBe('s');
|
||||||
|
expect(config.get('number')).toBe(12);
|
||||||
|
});
|
||||||
|
|
||||||
it('beforeSave validateMasterKey and skipWithMasterKey fail', async function (done) {
|
it('beforeSave validateMasterKey and skipWithMasterKey fail', async function (done) {
|
||||||
Parse.Cloud.beforeSave(
|
Parse.Cloud.beforeSave(
|
||||||
'BeforeSave',
|
'BeforeSave',
|
||||||
@@ -1441,7 +1476,7 @@ describe('cloud validator', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('validate afterSaveFile fail', async done => {
|
it('validate afterSaveFile fail', async done => {
|
||||||
Parse.Cloud.beforeSave(Parse.File, () => {}, validatorFail);
|
Parse.Cloud.afterSave(Parse.File, () => {}, validatorFail);
|
||||||
try {
|
try {
|
||||||
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
|
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
|
||||||
await file.save({ useMasterKey: true });
|
await file.save({ useMasterKey: true });
|
||||||
@@ -1496,6 +1531,42 @@ describe('cloud validator', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('validate beforeSave Parse.Config', async () => {
|
||||||
|
Parse.Cloud.beforeSave(Parse.Config, () => {}, validatorSuccess);
|
||||||
|
const config = await testConfig();
|
||||||
|
expect(config.get('internal')).toBe('i');
|
||||||
|
expect(config.get('string')).toBe('s');
|
||||||
|
expect(config.get('number')).toBe(12);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validate beforeSave Parse.Config fail', async () => {
|
||||||
|
Parse.Cloud.beforeSave(Parse.Config, () => {}, validatorFail);
|
||||||
|
try {
|
||||||
|
await testConfig();
|
||||||
|
fail('cloud function should have failed.');
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.code).toBe(Parse.Error.VALIDATION_ERROR);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validate afterSave Parse.Config', async () => {
|
||||||
|
Parse.Cloud.afterSave(Parse.Config, () => {}, validatorSuccess);
|
||||||
|
const config = await testConfig();
|
||||||
|
expect(config.get('internal')).toBe('i');
|
||||||
|
expect(config.get('string')).toBe('s');
|
||||||
|
expect(config.get('number')).toBe(12);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validate afterSave Parse.Config fail', async () => {
|
||||||
|
Parse.Cloud.afterSave(Parse.Config, () => {}, validatorFail);
|
||||||
|
try {
|
||||||
|
await testConfig();
|
||||||
|
fail('cloud function should have failed.');
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.code).toBe(Parse.Error.VALIDATION_ERROR);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('Should have validator', async done => {
|
it('Should have validator', async done => {
|
||||||
Parse.Cloud.define(
|
Parse.Cloud.define(
|
||||||
'myFunction',
|
'myFunction',
|
||||||
|
|||||||
@@ -3921,6 +3921,162 @@ describe('saveFile hooks', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Cloud Config hooks', () => {
|
||||||
|
function testConfig() {
|
||||||
|
return Parse.Config.save({ internal: 'i', string: 's', number: 12 }, { internal: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
it('beforeSave(Parse.Config) can run hook with new config', async () => {
|
||||||
|
let count = 0;
|
||||||
|
Parse.Cloud.beforeSave(Parse.Config, (req) => {
|
||||||
|
expect(req.object).toBeDefined();
|
||||||
|
expect(req.original).toBeUndefined();
|
||||||
|
expect(req.user).toBeUndefined();
|
||||||
|
expect(req.headers).toBeDefined();
|
||||||
|
expect(req.ip).toBeDefined();
|
||||||
|
expect(req.installationId).toBeDefined();
|
||||||
|
expect(req.context).toBeDefined();
|
||||||
|
const config = req.object;
|
||||||
|
expect(config.get('internal')).toBe('i');
|
||||||
|
expect(config.get('string')).toBe('s');
|
||||||
|
expect(config.get('number')).toBe(12);
|
||||||
|
count += 1;
|
||||||
|
});
|
||||||
|
await testConfig();
|
||||||
|
const config = await Parse.Config.get({ useMasterKey: true });
|
||||||
|
expect(config.get('internal')).toBe('i');
|
||||||
|
expect(config.get('string')).toBe('s');
|
||||||
|
expect(config.get('number')).toBe(12);
|
||||||
|
expect(count).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('beforeSave(Parse.Config) can run hook with existing config', async () => {
|
||||||
|
let count = 0;
|
||||||
|
Parse.Cloud.beforeSave(Parse.Config, (req) => {
|
||||||
|
if (count === 0) {
|
||||||
|
expect(req.object.get('number')).toBe(12);
|
||||||
|
expect(req.original).toBeUndefined();
|
||||||
|
}
|
||||||
|
if (count === 1) {
|
||||||
|
expect(req.object.get('number')).toBe(13);
|
||||||
|
expect(req.original.get('number')).toBe(12);
|
||||||
|
}
|
||||||
|
count += 1;
|
||||||
|
});
|
||||||
|
await testConfig();
|
||||||
|
await Parse.Config.save({ number: 13 });
|
||||||
|
expect(count).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('beforeSave(Parse.Config) should not change config if nothing is returned', async () => {
|
||||||
|
let count = 0;
|
||||||
|
Parse.Cloud.beforeSave(Parse.Config, () => {
|
||||||
|
count += 1;
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
await testConfig();
|
||||||
|
const config = await Parse.Config.get({ useMasterKey: true });
|
||||||
|
expect(config.get('internal')).toBe('i');
|
||||||
|
expect(config.get('string')).toBe('s');
|
||||||
|
expect(config.get('number')).toBe(12);
|
||||||
|
expect(count).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('beforeSave(Parse.Config) throw custom error', async () => {
|
||||||
|
Parse.Cloud.beforeSave(Parse.Config, () => {
|
||||||
|
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'It should fail');
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await testConfig();
|
||||||
|
fail('error should have thrown');
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.code).toBe(Parse.Error.SCRIPT_FAILED);
|
||||||
|
expect(e.message).toBe('It should fail');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('beforeSave(Parse.Config) throw string error', async () => {
|
||||||
|
Parse.Cloud.beforeSave(Parse.Config, () => {
|
||||||
|
throw 'before save failed';
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await testConfig();
|
||||||
|
fail('error should have thrown');
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.code).toBe(Parse.Error.SCRIPT_FAILED);
|
||||||
|
expect(e.message).toBe('before save failed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('beforeSave(Parse.Config) throw empty error', async () => {
|
||||||
|
Parse.Cloud.beforeSave(Parse.Config, () => {
|
||||||
|
throw null;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await testConfig();
|
||||||
|
fail('error should have thrown');
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.code).toBe(Parse.Error.SCRIPT_FAILED);
|
||||||
|
expect(e.message).toBe('Script failed. Unknown error.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('afterSave(Parse.Config) can run hook with new config', async () => {
|
||||||
|
let count = 0;
|
||||||
|
Parse.Cloud.afterSave(Parse.Config, (req) => {
|
||||||
|
expect(req.object).toBeDefined();
|
||||||
|
expect(req.original).toBeUndefined();
|
||||||
|
expect(req.user).toBeUndefined();
|
||||||
|
expect(req.headers).toBeDefined();
|
||||||
|
expect(req.ip).toBeDefined();
|
||||||
|
expect(req.installationId).toBeDefined();
|
||||||
|
expect(req.context).toBeDefined();
|
||||||
|
const config = req.object;
|
||||||
|
expect(config.get('internal')).toBe('i');
|
||||||
|
expect(config.get('string')).toBe('s');
|
||||||
|
expect(config.get('number')).toBe(12);
|
||||||
|
count += 1;
|
||||||
|
});
|
||||||
|
await testConfig();
|
||||||
|
const config = await Parse.Config.get({ useMasterKey: true });
|
||||||
|
expect(config.get('internal')).toBe('i');
|
||||||
|
expect(config.get('string')).toBe('s');
|
||||||
|
expect(config.get('number')).toBe(12);
|
||||||
|
expect(count).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('afterSave(Parse.Config) can run hook with existing config', async () => {
|
||||||
|
let count = 0;
|
||||||
|
Parse.Cloud.afterSave(Parse.Config, (req) => {
|
||||||
|
if (count === 0) {
|
||||||
|
expect(req.object.get('number')).toBe(12);
|
||||||
|
expect(req.original).toBeUndefined();
|
||||||
|
}
|
||||||
|
if (count === 1) {
|
||||||
|
expect(req.object.get('number')).toBe(13);
|
||||||
|
expect(req.original.get('number')).toBe(12);
|
||||||
|
}
|
||||||
|
count += 1;
|
||||||
|
});
|
||||||
|
await testConfig();
|
||||||
|
await Parse.Config.save({ number: 13 });
|
||||||
|
expect(count).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('afterSave(Parse.Config) should throw error', async () => {
|
||||||
|
Parse.Cloud.afterSave(Parse.Config, () => {
|
||||||
|
throw new Parse.Error(400, 'It should fail');
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await testConfig();
|
||||||
|
fail('error should have thrown');
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.code).toBe(400);
|
||||||
|
expect(e.message).toBe('It should fail');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('sendEmail', () => {
|
describe('sendEmail', () => {
|
||||||
it('can send email via Parse.Cloud', async done => {
|
it('can send email via Parse.Cloud', async done => {
|
||||||
const emailAdapter = {
|
const emailAdapter = {
|
||||||
|
|||||||
@@ -2,6 +2,15 @@
|
|||||||
import Parse from 'parse/node';
|
import Parse from 'parse/node';
|
||||||
import PromiseRouter from '../PromiseRouter';
|
import PromiseRouter from '../PromiseRouter';
|
||||||
import * as middleware from '../middlewares';
|
import * as middleware from '../middlewares';
|
||||||
|
import * as triggers from '../triggers';
|
||||||
|
|
||||||
|
const getConfigFromParams = params => {
|
||||||
|
const config = new Parse.Config();
|
||||||
|
for (const attr in params) {
|
||||||
|
config.attributes[attr] = Parse._decode(undefined, params[attr]);
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
|
||||||
export class GlobalConfigRouter extends PromiseRouter {
|
export class GlobalConfigRouter extends PromiseRouter {
|
||||||
getGlobalConfig(req) {
|
getGlobalConfig(req) {
|
||||||
@@ -30,7 +39,7 @@ export class GlobalConfigRouter extends PromiseRouter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateGlobalConfig(req) {
|
async updateGlobalConfig(req) {
|
||||||
if (req.auth.isReadOnly) {
|
if (req.auth.isReadOnly) {
|
||||||
throw new Parse.Error(
|
throw new Parse.Error(
|
||||||
Parse.Error.OPERATION_FORBIDDEN,
|
Parse.Error.OPERATION_FORBIDDEN,
|
||||||
@@ -45,9 +54,37 @@ export class GlobalConfigRouter extends PromiseRouter {
|
|||||||
acc[`masterKeyOnly.${key}`] = masterKeyOnly[key] || false;
|
acc[`masterKeyOnly.${key}`] = masterKeyOnly[key] || false;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
return req.config.database
|
const className = triggers.getClassName(Parse.Config);
|
||||||
.update('_GlobalConfig', { objectId: '1' }, update, { upsert: true }, true)
|
const hasBeforeSaveHook = triggers.triggerExists(className, triggers.Types.beforeSave, req.config.applicationId);
|
||||||
.then(() => ({ response: { result: true } }));
|
const hasAfterSaveHook = triggers.triggerExists(className, triggers.Types.afterSave, req.config.applicationId);
|
||||||
|
let originalConfigObject;
|
||||||
|
let updatedConfigObject;
|
||||||
|
const configObject = new Parse.Config();
|
||||||
|
configObject.attributes = params;
|
||||||
|
|
||||||
|
const results = await req.config.database.find('_GlobalConfig', { objectId: '1' }, { limit: 1 });
|
||||||
|
const isNew = results.length !== 1;
|
||||||
|
if (!isNew && (hasBeforeSaveHook || hasAfterSaveHook)) {
|
||||||
|
originalConfigObject = getConfigFromParams(results[0].params);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await triggers.maybeRunGlobalConfigTrigger(triggers.Types.beforeSave, req.auth, configObject, originalConfigObject, req.config, req.context);
|
||||||
|
if (isNew) {
|
||||||
|
await req.config.database.update('_GlobalConfig', { objectId: '1' }, update, { upsert: true }, true)
|
||||||
|
updatedConfigObject = configObject;
|
||||||
|
} else {
|
||||||
|
const result = await req.config.database.update('_GlobalConfig', { objectId: '1' }, update, {}, true);
|
||||||
|
updatedConfigObject = getConfigFromParams(result.params);
|
||||||
|
}
|
||||||
|
await triggers.maybeRunGlobalConfigTrigger(triggers.Types.afterSave, req.auth, updatedConfigObject, originalConfigObject, req.config, req.context);
|
||||||
|
return { response: { result: true } }
|
||||||
|
} catch (err) {
|
||||||
|
const error = triggers.resolveError(err, {
|
||||||
|
code: Parse.Error.SCRIPT_FAILED,
|
||||||
|
message: 'Script failed. Unknown error.',
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mountRoutes() {
|
mountRoutes() {
|
||||||
|
|||||||
@@ -79,10 +79,14 @@ const getRoute = parseClass => {
|
|||||||
_User: 'users',
|
_User: 'users',
|
||||||
_Session: 'sessions',
|
_Session: 'sessions',
|
||||||
'@File': 'files',
|
'@File': 'files',
|
||||||
|
'@Config' : 'config',
|
||||||
}[parseClass] || 'classes';
|
}[parseClass] || 'classes';
|
||||||
if (parseClass === '@File') {
|
if (parseClass === '@File') {
|
||||||
return `/${route}/:id?(.*)`;
|
return `/${route}/:id?(.*)`;
|
||||||
}
|
}
|
||||||
|
if (parseClass === '@Config') {
|
||||||
|
return `/${route}`;
|
||||||
|
}
|
||||||
return `/${route}/${parseClass}/:id?(.*)`;
|
return `/${route}/${parseClass}/:id?(.*)`;
|
||||||
};
|
};
|
||||||
/** @namespace
|
/** @namespace
|
||||||
|
|||||||
@@ -1027,3 +1027,38 @@ export async function maybeRunFileTrigger(triggerType, fileObject, config, auth)
|
|||||||
}
|
}
|
||||||
return fileObject;
|
return fileObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function maybeRunGlobalConfigTrigger(triggerType, auth, configObject, originalConfigObject, config, context) {
|
||||||
|
const GlobalConfigClassName = getClassName(Parse.Config);
|
||||||
|
const configTrigger = getTrigger(GlobalConfigClassName, triggerType, config.applicationId);
|
||||||
|
if (typeof configTrigger === 'function') {
|
||||||
|
try {
|
||||||
|
const request = getRequestObject(triggerType, auth, configObject, originalConfigObject, config, context);
|
||||||
|
await maybeRunValidator(request, `${triggerType}.${GlobalConfigClassName}`, auth);
|
||||||
|
if (request.skipWithMasterKey) {
|
||||||
|
return configObject;
|
||||||
|
}
|
||||||
|
const result = await configTrigger(request);
|
||||||
|
logTriggerSuccessBeforeHook(
|
||||||
|
triggerType,
|
||||||
|
'Parse.Config',
|
||||||
|
configObject,
|
||||||
|
result,
|
||||||
|
auth,
|
||||||
|
config.logLevels.triggerBeforeSuccess
|
||||||
|
);
|
||||||
|
return result || configObject;
|
||||||
|
} catch (error) {
|
||||||
|
logTriggerErrorBeforeHook(
|
||||||
|
triggerType,
|
||||||
|
'Parse.Config',
|
||||||
|
configObject,
|
||||||
|
auth,
|
||||||
|
error,
|
||||||
|
config.logLevels.triggerBeforeError
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return configObject;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user