Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions src/parse-mockdb.js
Original file line number Diff line number Diff line change
Expand Up @@ -678,8 +678,11 @@ function handleGetRequest(request) {
* @param {string} className The name of the class to get the hook on.
* @param {string} hookType One of 'beforeSave', 'afterSave', 'beforeDelete', 'afterDelete'
* @param {Object} data The Data that is to be hydrated into an instance of className class.
* @param {Object} originalData The original data (as it was before an update) or undefined
* if it was just created; will also be hydrated into an instance
* of className class.
*/
function runHook(className, hookType, data) {
function runHook(className, hookType, data, originalData) {
let hook = getHook(className, hookType);
if (hook) {
const hydrate = (rawData) => {
Expand All @@ -690,14 +693,11 @@ function runHook(className, hookType, data) {
);
return Parse.Object.fromJSON(modelJSON);
};
const model = hydrate(data, className);
const model = hydrate(data);
hook = hook.bind(model);

const collection = getCollection(className);
let original;
if (collection[model.id]) {
original = hydrate(collection[model.id]);
}
const original = (originalData !== undefined) ? hydrate(originalData) : undefined;

// TODO Stub out Parse.Cloud.useMasterKey() so that we can report the correct 'master'
// value here.
return hook(makeRequestObject(original, model, false)).then((beforeSaveOverrideValue) => {
Expand Down Expand Up @@ -792,7 +792,7 @@ function handlePutRequest(request) {
applyOps(updatedObject, ops, className);
const toOmit = ['createdAt', 'objectId'].concat(Array.from(getMask(className)));

return runHook(className, 'beforeSave', updatedObject).then(result => {
return runHook(className, 'beforeSave', updatedObject, currentObject).then(result => {
const changedKeys = getChangedKeys(updatedObject, result);

collection[request.objectId] = updatedObject;
Expand All @@ -802,7 +802,7 @@ function handlePutRequest(request) {
);
return Promise.resolve(respond(200, response));
}).then((result) => {
runHook(className, 'afterSave', updatedObject);
runHook(className, 'afterSave', updatedObject, currentObject);
return result;
});
}
Expand Down
111 changes: 111 additions & 0 deletions test/original_in_aftersave/SCENARIO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
### Problem

The `afterSave` hook in **Parse MockDB** behaves differently to a
real Parse Server (v4.4.0 at time of writing).

In a real server, the `afterSave` hook will provide the original object
from before the save as `request.original`, as well as the saved object
in `request.object` as expected. In other words, it should provide the
same objects on the `request` object as you would see in `beforeSave`.

Thus, any cloud code which relies on using this information to perform
certain actions when particular data has changed isn't able to be unit
tested via Parse MockDB.

### Testing

- Cloud Code is here: [`main.js`](./main.js)
- Client-side simulation script is here: [`test_context.js`](./test_context.js)

The cloud code just logs out the `request.original` and `request.object`
in the `beforeSave` hook and the `afterSave` hook.

The test script just creates an object and then updates it.

Run the test Parse Server via Docker as below:

```bash
$ docker run --rm --name test-mongo -d mongo && \
> docker run --rm --name test-parse-server -p 1337:1337 -e VERBOSE=0 \
> -v $(pwd):/parse-server/cloud --link test-mongo parseplatform/parse-server \
> --appId test-app --masterKey "M@stErK3y" \
> --databaseURI "mongodb://test-mongo/test" --cloud /parse-server/cloud/main.js
...

[1] parse-server running on http://localhost:1337/parse
...
```

After running the test script in a separate terminal:

```bash
$ node test_context.js
Created { foo: 'bar',
createdAt: '2020-11-09T07:42:11.956Z',
updatedAt: '2020-11-09T07:42:11.956Z',
objectId: 'zwZdxCML9F' }
Updated { foo: 'baaaah',
createdAt: '2020-11-09T07:42:11.956Z',
updatedAt: '2020-11-09T07:42:12.043Z',
objectId: 'zwZdxCML9F' }
```

You will see this in the server logs (split into two
chunks: (1) create then (2) update):

**Create**
```
BEFORE: undefined -> { foo: 'bar' }
info: beforeSave triggered for Thing for user undefined:
Input: {"foo":"bar"}
Result: {"object":{"foo":"bar"}} {"className":"Thing","triggerType":"beforeSave"}
AFTER: undefined -> {
foo: 'bar',
createdAt: '2020-11-09T07:42:11.956Z',
updatedAt: '2020-11-09T07:42:11.956Z',
objectId: 'zwZdxCML9F'
}
info: afterSave triggered for Thing for user undefined:
Input: {"foo":"bar","createdAt":"2020-11-09T07:42:11.956Z","updatedAt":"2020-11-09T07:42:11.956Z","objectId":"zwZdxCML9F"} {"className":"Thing","triggerType":"afterSave"}
info: afterSave triggered for Thing for user undefined:
Input: {"foo":"bar","createdAt":"2020-11-09T07:42:11.956Z","updatedAt":"2020-11-09T07:42:11.956Z","objectId":"zwZdxCML9F"}
Result: undefined {"className":"Thing","triggerType":"afterSave"}
```

**Update**
```
BEFORE: {
foo: 'bar',
createdAt: '2020-11-09T07:42:11.956Z',
updatedAt: '2020-11-09T07:42:11.956Z',
objectId: 'zwZdxCML9F'
} -> {
foo: 'baaaah',
createdAt: '2020-11-09T07:42:11.956Z',
updatedAt: '2020-11-09T07:42:11.956Z',
objectId: 'zwZdxCML9F'
}
info: beforeSave triggered for Thing for user undefined:
Input: {"foo":"baaaah","createdAt":"2020-11-09T07:42:11.956Z","updatedAt":"2020-11-09T07:42:11.956Z","objectId":"zwZdxCML9F"}
Result: {"object":{"foo":"baaaah"}} {"className":"Thing","triggerType":"beforeSave"}
AFTER: {
foo: 'bar',
createdAt: '2020-11-09T07:42:11.956Z',
updatedAt: '2020-11-09T07:42:11.956Z',
objectId: 'zwZdxCML9F'
} -> {
foo: 'baaaah',
createdAt: '2020-11-09T07:42:11.956Z',
updatedAt: '2020-11-09T07:42:12.043Z',
objectId: 'zwZdxCML9F'
}
info: afterSave triggered for Thing for user undefined:
Input: {"foo":"baaaah","createdAt":"2020-11-09T07:42:11.956Z","updatedAt":"2020-11-09T07:42:12.043Z","objectId":"zwZdxCML9F"} {"className":"Thing","triggerType":"afterSave"}
info: afterSave triggered for Thing for user undefined:
Input: {"foo":"baaaah","createdAt":"2020-11-09T07:42:11.956Z","updatedAt":"2020-11-09T07:42:12.043Z","objectId":"zwZdxCML9F"}
Result: undefined {"className":"Thing","triggerType":"afterSave"}
```

As you can see in the final part of the logs above, the
original object and the saved object are sent through to
`afterSave`.
11 changes: 11 additions & 0 deletions test/original_in_aftersave/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/* global Parse */

Parse.Cloud.beforeSave('Thing', (req /* , res */) => {
console.log('BEFORE:',
req.original !== undefined ? req.original.toJSON() : undefined, '->', req.object.toJSON());
});

Parse.Cloud.afterSave('Thing', (req /* , res */) => {
console.log('AFTER:',
req.original !== undefined ? req.original.toJSON() : undefined, '->', req.object.toJSON());
});
14 changes: 14 additions & 0 deletions test/original_in_aftersave/test_context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const Parse = require('parse/node');

Parse.initialize('test-app', null, 'M@stErK3y');
Parse.serverURL = 'http://localhost:1337/parse';

const Thing = Parse.Object.extend('Thing');
const thing = new Thing();

thing.save({ foo: 'bar' })
.then((t1) => {
console.log('Created %o', t1.toJSON());
return t1.save({ foo: 'baaaah' });
})
.then((t2) => console.log('Updated %o', t2.toJSON()));
22 changes: 21 additions & 1 deletion test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,15 +185,19 @@ function behavesLikeParseObjectOnAfterSave(typeName, ParseObjectOrUserSubclass)
context('when object has afterSave hook registered', () => {
let didAfterSave;
let objectInAfterSave;
let originalObjectInAfterSave;
function afterSavePromise(request) {
const { original, object } = request;
didAfterSave = true;
objectInAfterSave = request.object;
originalObjectInAfterSave = original;
objectInAfterSave = object;
return Promise.resolve();
}

beforeEach(() => {
didAfterSave = false;
objectInAfterSave = {};
originalObjectInAfterSave = {};
});

context('when saving a new object', () => {
Expand Down Expand Up @@ -289,6 +293,22 @@ function behavesLikeParseObjectOnAfterSave(typeName, ParseObjectOrUserSubclass)
});
});

it('provide the original and saved object to the afterSave hook', () => {
ParseMockDB.registerHook(typeName, 'afterSave', afterSavePromise);
object.set('name', 'updated');
return object.save().then(() => {
assert.equal(
originalObjectInAfterSave.get('name'),
'original'
);

assert.equal(
objectInAfterSave.get('name'),
'updated'
);
});
});

context('when the afterSave hook hits an error', () => {
beforeEach(() => {
const badHook = () => Promise.reject(new Error('Something went wrong'));
Expand Down