Actions

In a previous section, we created Tweet objects directly using Ruby code (i.e. bypassing the API). Any idea why we did that? Is there a problem with the following API call?

jruby-1.7.18 :023 > xput '/model/tweet', {text: 'Learning about #xnlogic is fun!'}

The problem is that we do not relate between a tweet and the user who created it. In fact, in our data model, it doesn’t make sense to create a tweet without a user, as tweets are always created by users.

This issue can be solved by defining an action. In xnlogic, actions can be thought of as instance methods that can be called from the API.

Let’s see how we define our post_tweet action …

Defining the post_tweet action

We need to update the code in lib/my_app/parts/user.rb to define our post_tweet instance method and expose it to the API.

Note: xnlogic has a consistent design philosophy - We give you “full access” when writing Ruby code that interacts with your data, but require you to explicitly expose the functionality to the API.

We will start by defining the instance method. Add the following code to the User module.

module Vertex
    def post_tweet(t)
        tweet = self.app.create(M::Tweet, {text: t})
        tweet.user = self
        tweet
    end
end

The code above defines the post_tweet instance method that creates a tweet, and sets its from-one user relation to self.
There are a few things to notice: * The method is defined inside a module called Vertex. * This method changes the graph (by creating an object), and, therefore, will have to be called inside a transaction. * For convenience, this method returns the created tweet to the caller.

Note: The statement tweet.user = self creates the relation between the user and the tweet. Alternatively, we could have defined the same relation with the statement self.add_tweets(tweet).

Next, we will expose our post_tweet method to the API action, by adding the following code to the User module.

action :post_tweet, args: [{:t => :text}] do |context, t|
    self.post_tweet(t)
end

The code above declares an action called post_tweet with a single parameter (whose name is t and type is text). It also defines the action as a Ruby block.

Note: Actions are automatically wrapped in a transaction.

Tip: Keep your action definition (i.e. the block) simple. Ideally, it should only contain a call to a corresponding instance method.

At this point, lib/my_app/parts/user.rb should contain the following code.

module MyApp
  module User
    xn_part

    property :username, type: :text
    property :email, type: :text

    to_many :Tweet

    action :post_tweet, args: [{:t => :text}] do |context, t|
        self.post_tweet(t)
    end

    module Vertex
        def post_tweet(t)
            tweet = self.app.create(M::Tweet, {text: t})
            tweet.user = self
            tweet
        end
    end

  end
end

Performing actions via the API

Actions are performed using POST requests.

  • We append action/ACTION_NAME to the path of the request.
  • We send the arguments as a JSON object (keys are parameter names).

For example, if we want user 73 to post the tweet 'Hello World', we make the following request.

jruby-1.7.18 :023 > xpost '/model/user/id/73/action/post_tweet', {t: "Hello World"}
 => [{:meta=>{:xnid=>"/model/job_result/id/86", :model_name=>"job_result", :rendered=>["record", "job"], :format=>"partial", :rel_limit=>50}, :id=>86, :name=>nil, :model_name=>"job_result", :description=>nil, :created_at=>2015-02-18 20:46:55 UTC, :status=>:complete, :messages=>nil, :request_url=>nil, :source_url=>"/model/user/id/73", :action_type=>"action", :action_name=>"post_tweet", :action_args=>{"t"=>"Hello World"}, :part_name=>"user", :value=>nil, :display_name=>"post_tweet"}]

We can verify that we can see our new tweet in the user’s tweets.

jruby-1.7.18 :024 > xget '/model/user/id/73/rel/tweets/properties/text'
[85, "Hello World"] [82, "Learning #xnlogic is fun"]  [81, "No more snow! #wintersucks"]

We can also verify that we can access the user from the tweet.

jruby-1.7.18 :025 > xget '/model/tweet/id/85/rel/user/properties/username'
[73, "cynthia"]

Following and unfollowing users

Let’s see how we can define actions that will allow users to follow/unfollow one another.

As before, all of our code updates will happen in lib/my_app/parts/user.rb, and we start by defining the following instance methods, inside the Vertex module.

def follow(user_id)
    self.add_follows(self.app.graph.vertex(user_id, M::User))
end

def unfollow(user_id)
    self.remove_follows(self.app.graph.vertex(user_id, M::User))
end

We continue by exposing these instance methods as API actions, and add the following code to the User module.

action :follow, args: [{:user_id => :numeric}] do |context, user_id|
    self.follow(user_id)
end

action :unfollow, args: [{:user_id => :numeric}] do |context, user_id|
    self.unfollow(user_id)
end

At this point, lib/my_app/parts/user.rb should contain the following code.

module MyApp
  module User
    xn_part

    property :username, type: :text
    property :email, type: :text


    to_many :Tweet

    to_many   :User, to: :follows
    from_many :User, to: :follows, from: :followed_by


    action :follow, args: [{:user_id => :numeric}] do |context, user_id|
        self.follow(user_id)
    end

    action :unfollow, args: [{:user_id => :numeric}] do |context, user_id|
        self.unfollow(user_id)
    end

    action :post_tweet, args: [{:t => :text}] do |context, t|
        self.post_tweet(t)
    end


    module Vertex

        def post_tweet(t)
            tweet = self.app.create(M::Tweet, {text: t})
            tweet.user = self
            tweet
        end

        def follow(user_id)
            self.add_follows(self.app.graph.vertex(user_id, M::User))
        end

        def unfollow(user_id)
            self.remove_follows(self.app.graph.vertex(user_id, M::User))
        end

    end

  end
end

We are now ready to test our actions (don’t forget to MyApp.reload!, if necessary).

Let’s make user 73 follow a few users.

jruby-1.7.18 :026 > xpost '/model/user/id/73/action/follow', {user_id: 75}
jruby-1.7.18 :027 > xpost '/model/user/id/73/action/follow', {user_id: 77}

We can verify that the following relations have been created, and can be accessed in both directions.

jruby-1.7.18 :028 > xget '/model/user/id/73/rel/follows/properties/username'
[75, "randy.marsh"] [77, "cool.dude"] 
Total: 2 
jruby-1.7.18 :029 > xget '/model/user/id/75/rel/followed_by/properties/username'
[73, "cynthia"]

We should also verify that our unfollow actions works.

jruby-1.7.18 :030 > xpost '/model/user/id/73/action/unfollow', {user_id: 75}
jruby-1.7.18 :031 > xget '/model/user/id/73/rel/follows/properties/username'
[77, "cool.dude"]  
Total: 1
jruby-1.7.18 :032 > xget '/model/user/id/75/rel/followed_by/properties/username'
Total: 0

Summary

  • Actions are instance methods that are exposed to the API.
  • We perform actions using POST request.
    • We specify which action to perform by appending /action/ACTION_NAME to the URL path.
    • We pass the arguments as a JSON object.
  • Defining an action is a two-step process:
    1. Define the logic in an instance method, inside the Vertex module.
    2. Expose the method to the API, using an action declaration.
  • Actions are automatically wrapped in a transaction.