Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Why afterFind is triggered always before beforeSave? #6088

Closed
santiagosemhan opened this issue Sep 26, 2019 · 12 comments · Fixed by #6127
Closed

Why afterFind is triggered always before beforeSave? #6088

santiagosemhan opened this issue Sep 26, 2019 · 12 comments · Fixed by #6127

Comments

@santiagosemhan
Copy link

There are situations you want to add extra fields to return to the client that alters the request.objects. For instance, when you want to calculate some field in server and then add it to the response.
This "extra field" is passing always to the beforeSave, so this "extra" is pretended to be saved into the object. Currently, I make an unset for the "extra field" in beforeSave and problem solved.
So, my question here is, Why afterFind is triggered before beforeSave? and Is there another way to do this (add extra field to the response)?

Thanks!

@davimacedo
Copy link
Member

It is actually possible to change the response in an afterSave trigger. Take a look in this PR: #5814

@davimacedo davimacedo added the type:question Support or code-level question label Sep 27, 2019
@santiagosemhan
Copy link
Author

Thanks for reply! I mean, afterFind alters the input of beforeSave?

@davimacedo
Copy link
Member

Actually the afterFind trigger does not run before beforeSave trigger by default. This behavior is probably something particular to your app. Can you please share the codes you are using for the triggers, for saving the object and the logs you got?

@santiagosemhan
Copy link
Author

Thanks, @davimacedo! I reviewed the code and yes, It was an issue with my app. There was a method that in beforeSave was searching duplicated data in the same Class.

@santiagosemhan
Copy link
Author

santiagosemhan commented Sep 27, 2019

Mmm, recently I comment the code in beforeSave:

  static async beforeSave(request){
    super.beforeSave(request);
    request.object.unset("sensors");
    const query = new Parse.Query(new Device());
    const uuid = request.object.get("uuid");
    query.equalTo("uuid", uuid);
    const result = await query.first({ useMasterKey: true });

    if (result && request.object.id !== result.id) throw new Parse.Error(400, JSON.stringify({ 
      uuid: [`${uuid} is already registered.`]
    }));

    const key = hat();
    if (request.object.isNew()) {
      request.object.set("key", key);
    }
  }

  static async afterFind(request){
    const { objects } = request;
    
    // If no Devices, early return empty array 
    if (objects.length === 0 ) return [];

    const Sensor = Parse.Object.extend("Sensor");
    const response = await Promise.all(objects.map(async device => {
      const query = new Parse.Query(new Sensor());
      query.equalTo("device", device);
      return query.find().then(sensors => device.set("sensors", sensors.map( s => s.toJSON()))); 
    }));
    return response;
  }

and this is the trace log:

info: afterFind triggered for Device for user undefined:
core-api_1  |   Input: "AfterFind"
core-api_1  |   Result: "[{\"objectId\":\"0UQiEErAlH\",\"uuid\":\"test\",\"description\":\"add\",\"active\":true,\"createdBy\":{\"__type\":\"Pointer\",\"className\":\"_User\",\"objectId\":\"m8sGWujfsP\"},\"key\":\"ee8bf640246757d3f07f6ad95fcbd09b\",\"connected\":false,\"createdAt\":\"2019-09-24T14:22:03.655Z\",\"updatedAt\":\"2019-09-27T23:33:44.143Z\"}]" {"className":"Device","triggerType":"afterFind"}
core-api_1  | info: afterFind triggered for Device for user undefined:
core-api_1  |   Input: "[{\"uuid\":\"test\",\"description\":\"add\",\"active\":true,\"createdBy\":{\"__type\":\"Pointer\",\"className\":\"_User\",\"objectId\":\"m8sGWujfsP\"},\"key\":\"ee8bf640246757d3f07f6ad95fcbd09b\",\"connected\":false,\"createdAt\":\"2019-09-24T14:22:03.655Z\",\"updatedAt\":\"2019-09-27T23:33:44.143Z\",\"sensors\":[{\"device\":{\"__type\":\"Pointer\",\"className\":\"Device\",\"objectId\":\"0UQiEErAlH\"},\"name\":\"test\",\"createdBy\":{\"__type\":\"Pointer\",\"className\":\"_User\",\"objectId\":\"m8sGWujfsP\"},\"createdAt\":\"2019-09-24T14:22:27.726Z\",\"updatedAt\":\"2019-09-24T14:22:27.726Z\",\"objectId\":\"xsENQz81sw\"}],\"objectId\":\"0UQiEErAlH\"}]" {"className":"Device","triggerType":"afterFind"}
core-api_1  | info: beforeSave triggered for Device for user undefined:
core-api_1  |   Input: {"uuid":"testing","description":"add","active":true,"createdBy":{"__type":"Pointer","className":"_User","objectId":"m8sGWujfsP"},"key":"ee8bf640246757d3f07f6ad95fcbd09b","connected":false,"createdAt":"2019-09-24T14:22:03.655Z","updatedAt":"2019-09-27T23:33:44.143Z","sensors":[{"device":{"__type":"Pointer","className":"Device","objectId":"0UQiEErAlH"},"name":"test","createdBy":{"__type":"Pointer","className":"_User","objectId":"m8sGWujfsP"},"createdAt":"2019-09-24T14:22:27.726Z","updatedAt":"2019-09-24T14:22:27.726Z","objectId":"xsENQz81sw"}],"objectId":"0UQiEErAlH"}
core-api_1  |   Result: {"object":{"uuid":"testing"}} {"className":"Device","triggerType":"beforeSave"}

Parse Server version: 3.9.0

@davimacedo
Copy link
Member

The afterFind trigger is called because of const result = await query.first({ useMasterKey: true });, right? Anyway, the afterSave trigger should not change your request.object var inside your beforeSave trigger.

@santiagosemhan
Copy link
Author

santiagosemhan commented Sep 27, 2019

Take a look at this

  static async beforeSave(request){
    console.log(request.object.toJSON());
    // super.beforeSave(request);
    // request.object.unset("sensors");
    // const query = new Parse.Query(new Device());
    // const uuid = request.object.get("uuid");
    // query.equalTo("uuid", uuid);
    // const result = await query.first({ useMasterKey: true });

    // if (result && request.object.id !== result.id) throw new Parse.Error(400, JSON.stringify({ 
    //   uuid: [`${uuid} is already registered.`]
    // }));

    // const key = hat();
    // if (request.object.isNew()) {
    //   request.object.set("key", key);
    // }
  }

  static async afterFind(request){
    const { objects } = request;
    
    // If no Devices, early return empty array 
    if (objects.length === 0 ) return [];

    const Sensor = Parse.Object.extend("Sensor");
    const response = await Promise.all(objects.map(async device => {
      const query = new Parse.Query(new Sensor());
      query.equalTo("device", device);
      return query.find().then(sensors => device.set("sensors", sensors.map( s => s.toJSON()))); 
    }));
    return response;
  }

logs:

core-api_1  | info: afterFind triggered for Device for user undefined:
core-api_1  |   Input: "AfterFind"
core-api_1  |   Result: "[{\"objectId\":\"0UQiEErAlH\",\"uuid\":\"testing\",\"description\":\"add\",\"active\":true,\"createdBy\":{\"__type\":\"Pointer\",\"className\":\"_User\",\"objectId\":\"m8sGWujfsP\"},\"key\":\"ee8bf640246757d3f07f6ad95fcbd09b\",\"connected\":false,\"createdAt\":\"2019-09-24T14:22:03.655Z\",\"updatedAt\":\"2019-09-27T23:46:48.414Z\"}]" {"className":"Device","triggerType":"afterFind"}
core-api_1  | info: afterFind triggered for Device for user undefined:
core-api_1  |   Input: "[{\"uuid\":\"testing\",\"description\":\"add\",\"active\":true,\"createdBy\":{\"__type\":\"Pointer\",\"className\":\"_User\",\"objectId\":\"m8sGWujfsP\"},\"key\":\"ee8bf640246757d3f07f6ad95fcbd09b\",\"connected\":false,\"createdAt\":\"2019-09-24T14:22:03.655Z\",\"updatedAt\":\"2019-09-27T23:46:48.414Z\",\"sensors\":[{\"device\":{\"__type\":\"Pointer\",\"className\":\"Device\",\"objectId\":\"0UQiEErAlH\"},\"name\":\"test\",\"createdBy\":{\"__type\":\"Pointer\",\"className\":\"_User\",\"objectId\":\"m8sGWujfsP\"},\"createdAt\":\"2019-09-24T14:22:27.726Z\",\"updatedAt\":\"2019-09-24T14:22:27.726Z\",\"objectId\":\"xsENQz81sw\"}],\"objectId\":\"0UQiEErAlH\"}]" {"className":"Device","triggerType":"afterFind"}
core-api_1  | { uuid: 'test',
core-api_1  |   description: 'add',
core-api_1  |   active: true,
core-api_1  |   createdBy:
core-api_1  |    { __type: 'Pointer', className: '_User', objectId: 'm8sGWujfsP' },
core-api_1  |   key: 'ee8bf640246757d3f07f6ad95fcbd09b',
core-api_1  |   connected: false,
core-api_1  |   createdAt: '2019-09-24T14:22:03.655Z',
core-api_1  |   updatedAt: '2019-09-27T23:46:48.414Z',
core-api_1  |   sensors:
core-api_1  |    [ { device: [Object],
core-api_1  |        name: 'test',
core-api_1  |        createdBy: [Object],
core-api_1  |        createdAt: '2019-09-24T14:22:27.726Z',
core-api_1  |        updatedAt: '2019-09-24T14:22:27.726Z',
core-api_1  |        objectId: 'xsENQz81sw' } ],
core-api_1  |   objectId: '0UQiEErAlH' }
core-api_1  | info: beforeSave triggered for Device for user undefined:
core-api_1  |   Input: {"uuid":"test","description":"add","active":true,"createdBy":{"__type":"Pointer","className":"_User","objectId":"m8sGWujfsP"},"key":"ee8bf640246757d3f07f6ad95fcbd09b","connected":false,"createdAt":"2019-09-24T14:22:03.655Z","updatedAt":"2019-09-27T23:46:48.414Z","sensors":[{"device":{"__type":"Pointer","className":"Device","objectId":"0UQiEErAlH"},"name":"test","createdBy":{"__type":"Pointer","className":"_User","objectId":"m8sGWujfsP"},"createdAt":"2019-09-24T14:22:27.726Z","updatedAt":"2019-09-24T14:22:27.726Z","objectId":"xsENQz81sw"}],"objectId":"0UQiEErAlH"}
core-api_1  |   Result: {"object":{"uuid":"test"}} {"className":"Device","triggerType":"beforeSave"}

I'm just only editing the object via Parse Dashboard.

@davimacedo
Copy link
Member

Can you try to do the same using the API Console and send the logs? I am afraid that the Dashboard is performing the find request to retrieve the object data.

@santiagosemhan
Copy link
Author

Using the console

core-api_1  | info: afterFind triggered for Device for user undefined:
core-api_1  |   Input: "AfterFind"
core-api_1  |   Result: "[{\"objectId\":\"0UQiEErAlH\",\"uuid\":\"test\",\"description\":\"add\",\"active\":true,\"createdBy\":{\"__type\":\"Pointer\",\"className\":\"_User\",\"objectId\":\"m8sGWujfsP\"},\"key\":\"ee8bf640246757d3f07f6ad95fcbd09b\",\"connected\":false,\"createdAt\":\"2019-09-24T14:22:03.655Z\",\"updatedAt\":\"2019-09-28T00:20:31.269Z\"}]" {"className":"Device","triggerType":"afterFind"}
core-api_1  | info: afterFind triggered for Device for user undefined:
core-api_1  |   Input: "[{\"uuid\":\"test\",\"description\":\"add\",\"active\":true,\"createdBy\":{\"__type\":\"Pointer\",\"className\":\"_User\",\"objectId\":\"m8sGWujfsP\"},\"key\":\"ee8bf640246757d3f07f6ad95fcbd09b\",\"connected\":false,\"createdAt\":\"2019-09-24T14:22:03.655Z\",\"updatedAt\":\"2019-09-28T00:20:31.269Z\",\"sensors\":[{\"device\":{\"__type\":\"Pointer\",\"className\":\"Device\",\"objectId\":\"0UQiEErAlH\"},\"name\":\"test\",\"createdBy\":{\"__type\":\"Pointer\",\"className\":\"_User\",\"objectId\":\"m8sGWujfsP\"},\"createdAt\":\"2019-09-24T14:22:27.726Z\",\"updatedAt\":\"2019-09-24T14:22:27.726Z\",\"objectId\":\"xsENQz81sw\"}],\"objectId\":\"0UQiEErAlH\"}]" {"className":"Device","triggerType":"afterFind"}
core-api_1  | info: beforeSave triggered for Device for user undefined:
core-api_1  |   Input: {"uuid":"test","description":"add","active":true,"createdBy":{"__type":"Pointer","className":"_User","objectId":"m8sGWujfsP"},"key":"ee8bf640246757d3f07f6ad95fcbd09b","connected":false,"createdAt":"2019-09-24T14:22:03.655Z","updatedAt":"2019-09-28T00:20:31.269Z","sensors":[{"device":{"__type":"Pointer","className":"Device","objectId":"0UQiEErAlH"},"name":"test","createdBy":{"__type":"Pointer","className":"_User","objectId":"m8sGWujfsP"},"createdAt":"2019-09-24T14:22:27.726Z","updatedAt":"2019-09-24T14:22:27.726Z","objectId":"xsENQz81sw"}],"objectId":"0UQiEErAlH"}
core-api_1  |   Result: {"object":{"uuid":"test"}} {"className":"Device","triggerType":"beforeSave"}

@santiagosemhan
Copy link
Author

latest test using the console:

  static async beforeSave(request){
    // console.log(request.object.toJSON());
    // super.beforeSave(request);
    // request.object.unset("sensors");
    // const query = new Parse.Query(new Device());
    // const uuid = request.object.get("uuid");
    // query.equalTo("uuid", uuid);
    // const result = await query.first({ useMasterKey: true });

    // if (result && request.object.id !== result.id) throw new Parse.Error(400, JSON.stringify({ 
    //   uuid: [`${uuid} is already registered.`]
    // }));

    // const key = hat();
    // if (request.object.isNew()) {
    //   request.object.set("key", key);
    // }
  }

  static async afterFind(request){
    // const { objects } = request;
    
    // // If no Devices, early return empty array 
    // if (objects.length === 0 ) return [];

    // const Sensor = Parse.Object.extend("Sensor");
    // const response = await Promise.all(objects.map(async device => {
    //   const query = new Parse.Query(new Sensor());
    //   query.equalTo("device", device);
    //   return query.find().then(sensors => device.set("sensors", sensors.map( s => s.toJSON()))); 
    // }));
    // return response;
    return [];
  }

logs:

core-api_1  | info: afterFind triggered for Device for user undefined:
core-api_1  |   Input: "AfterFind"
core-api_1  |   Result: "[{\"objectId\":\"0UQiEErAlH\",\"uuid\":\"testing\",\"description\":\"add\",\"active\":true,\"createdBy\":{\"__type\":\"Pointer\",\"className\":\"_User\",\"objectId\":\"m8sGWujfsP\"},\"key\":\"ee8bf640246757d3f07f6ad95fcbd09b\",\"connected\":false,\"createdAt\":\"2019-09-24T14:22:03.655Z\",\"updatedAt\":\"2019-09-28T00:35:47.698Z\"}]" {"className":"Device","triggerType":"afterFind"}
core-api_1  | info: afterFind triggered for Device for user undefined:
core-api_1  |   Input: "[]" {"className":"Device","triggerType":"afterFind"}
core-api_1  | info: beforeSave triggered for Device for user undefined:
core-api_1  |   Input: {"uuid":"testing","objectId":"0UQiEErAlH"}
core-api_1  |   Result: {"object":{"uuid":"testing"}} {"className":"Device","triggerType":"beforeSave"}

@davimacedo davimacedo added needs investigation and removed type:question Support or code-level question labels Sep 28, 2019
@davimacedo
Copy link
Member

You're right. I've just run the test below and it didn't pass. I will try to understand why.

fit('afterFind should not be triggered when saving an object', async() => {
    let beforeSaves = 0;
    Parse.Cloud.beforeSave('SavingTest', () => {
      beforeSaves++;
    });

    let afterSaves = 0;
    Parse.Cloud.afterSave('SavingTest', () => {
      afterSaves++;
    });

    let beforeFinds = 0;
    Parse.Cloud.beforeFind('SavingTest', () => {
      beforeFinds++;
    });

    let afterFinds = 0;
    Parse.Cloud.afterFind('SavingTest', () => {
      afterFinds++;
    });

    const obj = new Parse.Object('SavingTest');
    obj.set('someField', 'some value 1');
    await obj.save();

    expect(beforeSaves).toEqual(1);
    expect(afterSaves).toEqual(1);
    expect(beforeFinds).toEqual(0);
    expect(afterFinds).toEqual(0);

    obj.set('someField', 'some value 2');
    await obj.save();

    expect(beforeSaves).toEqual(2);
    expect(afterSaves).toEqual(2);
    expect(beforeFinds).toEqual(0);
    expect(afterFinds).toEqual(0);

    await obj.fetch();

    expect(beforeSaves).toEqual(2);
    expect(afterSaves).toEqual(2);
    expect(beforeFinds).toEqual(1);
    expect(afterFinds).toEqual(1);

    obj.set('someField', 'some value 3');
    await obj.save();

    expect(beforeSaves).toEqual(3);
    expect(afterSaves).toEqual(3);
    expect(beforeFinds).toEqual(1);
    expect(afterFinds).toEqual(1);
  });

@santiagosemhan
Copy link
Author

May be, when updating an object parse retrieve the original one. So this could be causing the afterFind trigger.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants