Skip to content

Commit

Permalink
feat: Add Cloud Code triggers Parse.Cloud.beforeSave and `Parse.Clo…
Browse files Browse the repository at this point in the history
…ud.afterSave` for Parse Config (#9232)
  • Loading branch information
dplewis authored Jul 20, 2024
1 parent 4d86ace commit 90a1e4a
Show file tree
Hide file tree
Showing 5 changed files with 308 additions and 5 deletions.
73 changes: 72 additions & 1 deletion spec/CloudCode.Validator.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ const validatorFail = () => {
const validatorSuccess = () => {
return true;
};
function testConfig() {
return Parse.Config.save({ internal: 'i', string: 's', number: 12 }, { internal: true });
}

describe('cloud validator', () => {
it('complete validator', async done => {
Expand Down Expand Up @@ -731,6 +734,38 @@ describe('cloud validator', () => {
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) {
Parse.Cloud.beforeSave(
'BeforeSave',
Expand Down Expand Up @@ -1441,7 +1476,7 @@ describe('cloud validator', () => {
});

it('validate afterSaveFile fail', async done => {
Parse.Cloud.beforeSave(Parse.File, () => {}, validatorFail);
Parse.Cloud.afterSave(Parse.File, () => {}, validatorFail);
try {
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
await file.save({ useMasterKey: true });
Expand Down Expand Up @@ -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 => {
Parse.Cloud.define(
'myFunction',
Expand Down
156 changes: 156 additions & 0 deletions spec/CloudCode.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
it('can send email via Parse.Cloud', async done => {
const emailAdapter = {
Expand Down
45 changes: 41 additions & 4 deletions src/Routers/GlobalConfigRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
import Parse from 'parse/node';
import PromiseRouter from '../PromiseRouter';
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 {
getGlobalConfig(req) {
Expand Down Expand Up @@ -30,7 +39,7 @@ export class GlobalConfigRouter extends PromiseRouter {
});
}

updateGlobalConfig(req) {
async updateGlobalConfig(req) {
if (req.auth.isReadOnly) {
throw new Parse.Error(
Parse.Error.OPERATION_FORBIDDEN,
Expand All @@ -45,9 +54,37 @@ export class GlobalConfigRouter extends PromiseRouter {
acc[`masterKeyOnly.${key}`] = masterKeyOnly[key] || false;
return acc;
}, {});
return req.config.database
.update('_GlobalConfig', { objectId: '1' }, update, { upsert: true }, true)
.then(() => ({ response: { result: true } }));
const className = triggers.getClassName(Parse.Config);
const hasBeforeSaveHook = triggers.triggerExists(className, triggers.Types.beforeSave, req.config.applicationId);
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() {
Expand Down
4 changes: 4 additions & 0 deletions src/cloud-code/Parse.Cloud.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,14 @@ const getRoute = parseClass => {
_User: 'users',
_Session: 'sessions',
'@File': 'files',
'@Config' : 'config',
}[parseClass] || 'classes';
if (parseClass === '@File') {
return `/${route}/:id?(.*)`;
}
if (parseClass === '@Config') {
return `/${route}`;
}
return `/${route}/${parseClass}/:id?(.*)`;
};
/** @namespace
Expand Down
35 changes: 35 additions & 0 deletions src/triggers.js
Original file line number Diff line number Diff line change
Expand Up @@ -1027,3 +1027,38 @@ export async function maybeRunFileTrigger(triggerType, fileObject, config, auth)
}
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;
}

0 comments on commit 90a1e4a

Please sign in to comment.