owsas/parse-cloud-class

An example?

Closed this issue · 9 comments

This really is not an issue, more of a question and hopefully a little help if it makes sense...

I am trying to figure out how to perform a cloud function and saw this in my search. I assume this lib was built to make cloud code easier...? or more functional..?

Here is what I am trying to do ~

  1. For new user registration only save email, username and password in Users table
  2. Any other information provided in the registration request (for example firstName, lastName, phone, sex, dateOfBirth, etc) stored in a UserProfile Collection
  3. The new User will have a 1:1 relation with the new UserProfile as user_profile_object
  4. The UserProfile will have a 1:1 relation with the new User as user_object

I can get 50% there creating profile and adding the profile to the User on beforeSave - but everything I try to get the User saved to the UserProfile fails. Here is what I have currently

Parse.Cloud.beforeSave(Parse.User, function (request) {
  var Profile = Parse.Object.extend('UserProfile')
  var profile = new Profile()
  profile.set('first_name', request.object.get('first_name'))
  profile.set('last_name', request.object.get('last_name'))
  profile.set('full_name', request.object.get('first_name') + ' ' + request.object.get('last_name'))
  profile.set('email', request.object.get('email'))

  return profile.save(null, {
    useMasterKey: true
  }).then(function (userProfile) {
    request.object.set('user_profile_object', userProfile)
    request.object.unset('first_name')
    request.object.unset('last_name')
  })
})

which returns to the client...

{
	"objectId": "V3DPPRR1MGtC",
	"username": "test1",
	"email": "test.1@tektango.com",
	"user_profile_object": {
		"__type": "Pointer",
		"className": "UserProfile",
		"objectId": "NmlaleRPgC4N"
	},
	"emailVerified": true,
	"createdAt": "2018-09-11T16:55:38.387Z",
	"updatedAt": "2018-09-11T16:57:26.186Z",
	"ACL": {
		"*": {
			"read": true
		},
		"V3DPPRR1MGtC": {
			"read": true,
			"write": true
		}
	},
	"sessionToken": "r:bc33da9a4dce66c6a2b4659a5083504d"
}

what I would like returned is:

{
	"objectId": "V3DPPRR1MGtC",
	"user_profile_object": "NmlaleRPgC4N"
	"emailVerified": false,
	"createdAt": "2018-09-11T16:55:38.387Z",
	"updatedAt": "2018-09-11T16:57:26.186Z",
	"sessionToken": "r:bc33da9a4dce66c6a2b4659a5083504d"
}

I am putting time in trying to reason cloud code and would very much welcome something a little more organized and clearer. Does your lib facilitate what I am looking for above?

Hello!

Well, this library helps you organize a little bit more the Cloud Code on your Parse Server, defining your beforeSave, afterSave, beforeDelete and afterDelete functions in a class that can be extended and use addons.

That said, this class definitely could help you do what you are trying to do, however your question has to do more with the way you program with parse-server and not with parse-server-addon-cloud-class

Regarding your code, I see that every time you save your user you create a new UserProfile object, which will end up in you having lots of UserProfile records only for one user and I don't think that is exactly what you want.

You could avoid creating new UserProfile objects every time by doing the following change in your code:

var profile = request.object.get('user_profile_object') || new Profile();

In your case, I understand that you want to also have an user_object key in the user profile as a 1:1 relation. Please consider that the objects that hit the beforeSave function on your Parse Server don't always have an id, because some of them might not be already in the database at that point, so if you create a relation in the way you are trying to do, then you can expect to have Parse errors telling you that You cannot create a pointer to an unsaved Parse.Object.

What I would to is to split this functionality between beforeSave and afterSave, wich is accomplishable by doing the following and using this library (untested code):

const { ParseCloudClass } = require('parse-server-addon-cloud-class');

class MyCustomUserClass extends ParseCloudClass {
  async processBeforeSave(request) {
    // keep addon, minimum values and required keys checking functionality
    // from ParseCloudClass
    return super.processBeforeSave(request)
    .then((user) => {
      const profile = user.get('user_profile_object') || new Parse.Object('Profile');
  
      profile.set('first_name', user.get('first_name'));
      profile.set('last_name', user.get('last_name'));
      profile.set('full_name', user.get('first_name') + ' ' + user.get('last_name'));
      profile.set('email', user.get('email'));
  
      // Save the profile (async version)
      return profile.save(null, { useMasterKey: true })
      .then((userProfile) => {
        user.set('user_profile_object', userProfile);
        user.unset('first_name');
        user.unset('last_name');
    
        return user;
      });      
    });
  }

  async afterSave(request) {
    // keep afterSave and addons functionality from ParseCloudClass
    return super.afterSave(request)
      .then((user) => {
        const profile = user.get('user_profile_object');

        profile.set('user_object', user);
        return profile.save(null, { useMasterKey: true })
          .then(() => {
            return user;
          });          
      });
  }
}

An async version of it would be:

class MyCustomUserClass extends ParseCloudClass {
  async processBeforeSave(request) {
    // keep addon, minimum values and required keys checking functionality
    // from ParseCloudClass
    const user = await super.processBeforeSave(request); 
    const profile = user.get('user_profile_object') || new Parse.Object('Profile');

    profile.set('first_name', user.get('first_name'));
    profile.set('last_name', user.get('last_name'));
    profile.set('full_name', user.get('first_name') + ' ' + user.get('last_name'));
    profile.set('email', user.get('email'));

    // Save the profile (async version)
    await profile.save(null, { useMasterKey: true });

    user.set('user_profile_object', profile);
    user.unset('first_name');
    user.unset('last_name');

    return user;
  }

  async afterSave(request) {
    // keep afterSave and addons functionality from ParseCloudClass
    const user = await super.afterSave(request); 
    const profile = user.get('user_profile_object');

    profile.set('user_object', user);
    await profile.save(null, { useMasterKey: true });
    
    return user;
  }
}

Please take into account that as parse-server-addon-cloud-class lets you create addons, you can create this functionality as an addon and publish it on npm for sharing it with others or using it on your own projects later. You would do this:

class MyCustomUserClass extends ParseCloudClass {
  async processBeforeSave(request) {
    const user = await super.processBeforeSave(request);
    const profile = user.get('user_profile_object') || new Parse.Object('Profile');

    profile.set('first_name', user.get('first_name'));
    profile.set('last_name', user.get('last_name'));
    profile.set('full_name', user.get('first_name') + ' ' + user.get('last_name'));
    profile.set('email', user.get('email'));

    // Save the profile (async version)
    await profile.save(null, { useMasterKey: true });

    user.set('user_profile_object', profile);
    user.unset('first_name');
    user.unset('last_name');

    return user;
  }

  async afterSave(request) {
    const user = await super.afterSave(request); 
    const profile = user.get('user_profile_object');

    profile.set('user_object', user);
    await profile.save(null, { useMasterKey: true });
    
    return user;
  }
}

and then publish it on npm as parse-server-addon-cloud-<something>

You rock!

So here is the cloud code I tried to implement...

const {
  ParseCloudClass
} = require('parse-server-addon-cloud-class')

class UserProfile extends ParseCloudClass {
  async processBeforeSave (request) {
    const user = request.object
    const profile = user.get('user_profile') || new Parse.Object('Profile')

    profile.set('first_name', user.get('first_name'))
    profile.set('last_name', user.get('last_name'))
    profile.set('full_name', user.get('first_name') + ' ' + user.get('last_name'))
    profile.set('email', user.get('email'))

    return profile.save(null, {
      useMasterKey: true
    }).then((userProfile) => {
      user.set('user_profile', userProfile)
      user.unset('first_name')
      user.unset('last_name')

      return user
    })
  }

  async afterSave (request) {
    const user = request.object
    const profile = user.get('user_profile')

    profile.set('user_object', user)
    return profile.save(null, {
      useMasterKey: true
    })
      .then(() => {
        return user
      })
  }
}

The response:

{
	"objectId": "SWSURrZJQ6wD",
	"createdAt": "2018-09-12T17:40:04.060Z"
}

But when I check the data browser (after reload) there is no associated UserProfile class row created and the first_name and last_name were inserted into the new User record.

On a side note, I take it you/your team are a Parse experts? We should connect. Also, a very good friend and wizard developer lives in Bogata. Check out https://github.com/kevoj and if you are in same location you should think about contacting him...

PS I changed only MyCustomClass to UserProfile and param user_profile_id to user_profile otherwise everything is the same. I also tried the async version with same results

What I don't see is how Parse knows that it is supposed to call this service when a new User is created. The "old" way was Parse.Cloud.beforeSave(Parse.User, function (request) but I see no reference to the User class in your sample.

Helloo!

Well I will definetely contact kevoj, thank you!

I will have to create a new Parse server and test your code. Maybe you have your code as a public repo to share?

To call the service when a new User is created you should write this in the Cloud Code's main.js file:

// Import the library
import { ParseCloudClass } from 'parse-server-addon-cloud-class';

// Import the class you created
import { UserProfile } from './<path-to-the-class>'; 

// Create an instance of it
const userProfile = new UserProfile();

// Connect it to the Parse _User class
ParseCloudClass.configureClass(Parse, '_User', userProfile);

Now, you could be willing to have extra functionalities on your _User class, so you could create another class that does some extra processing and connect both of them via addons. Let's see:

// Import the library
import { ParseCloudClass } from 'parse-server-addon-cloud-class';

// Import the classes you created
import { UserProfile } from './<path-to-the-class>'; 

// Imaginary class created by you that connects any user
// to a mailchimp list on afterSave
import { UserToMailchimp } from './<path-to-the-class>'; 

// Create the instances
const userProfile = new UserProfile();
const userToMailchimp = new UserToMailchimp();

// Connect both classes through addons
userProfile.useAddon(userToMailchimp);

// Connect to the Parse _User class
ParseCloudClass.configureClass(Parse, '_User', userProfile);

In this case, you would have both functionalities working. You could create the UserProfile you are needing, and then send the user to mailchimp. This means that you can grow in functionalities creating new classes that extend ParseCloudClass and test them separately.

A complete working example can be found here!! 😄

https://github.com/owsas/parse-cloud-class-example

Helloo I'm back!

Well, I have had a lot of work to do and I am coming back to this issue. Well, in your case you may want to create new Parse.Roles with those CREATE, READ, UPDATE, and DELETE names when you create a new company. For instance, on afterSave on Company, you create the roles for that object, with names like Company-${company.id}.

Then, you could write a new addon using parse-server-addon-cloud-class that checks if the request.user is linked in the users relation and throw errors on processBeforeSave, processBeforeDelete, beforeFind, and others.

Hope this helps,

Juan