Frontier: An abstraction layer for rapid REST development - Part 2 - Handling payloads

Hello again! Welcome to the Part 2 - Handling payloads! 

If you're new to this series, I'd recommend you to check out the Part 1 - Core concepts. You'll need a basic understanding about how Frontier works before continuing with the Part 2.

  1. Core concepts
    • Getting started
    • Creating a simple request
    • Query parameters
    • Aliasing query parameters
    • Changing output format
    • Rest query parameters
    • Inferring object instances
    • Using literal notation
    • Seamlessly mixing instances with literals
    • Returning streams
  2. Handling payloads
    • ​​How it works
    • Making it useful
    • Unmarshalling payloads into instances
    • Using the unmarshaller to EDIT an existing object
  3. Using the SQL API
    • ​​Creating a simple dynamic query
    • Overwriting the default container property
    • Using cached queries
    • Passing parameters to queries
  4. Sharing data across router methods
  5. Forcing API errors
  6. Managing errors with Reporters

2. Handling payloads

If you don't know what's a payload, it's a piece of information that user agents can send, they're normally used along with HTTP PUT, POST OR PATCH methods. This information is usually structured in a way that can be represented with a specific format that can be read on the server-side and client-side.


Just a few notes, we'll be using the class created on the Part 1. And since this part requires some request configuration beforehand, I'll use Postman to realize them from now. You can learn how to use this tool by reading the documentation on its website, which is great by the way. You're not obligated to use it though, if you have some experience with cURL or Wget you could use them as well.

How it works

Frontier is capable of handling JSON payloads easily, as long as you follow a few rules. Even though there are many ways on how to use payloads, these are always applied as an starting point. To demonstrate which rules I'm talking about, create a new route:

<Route Url="/payload/echo-data" Method="POST" Call="EchoReceivedData"></Route>


And now a method to handle it.
 

ClassMethod EchoReceivedData(receivedData As %DynamicObject) As %DynamicObject
{
  return receivedData
}



The purpose of this method is simple: echo the object back to the client.
And here are the four universal rules that must be followed when working using payloads with Frontier:

  • The argument that represents the payload must be typed from a %DynamicObject or %DynamicArray.
  • If the request intent is to send an array then the argument must be typed as %DynamicArray, otherwise %DynamicObject.
  • The HTTP method must be configured to one of: PUT, POST or even PATCH. Using GETs or OPTIONS won't work.
  • You can only have a total of one payloadable argument, regardless of their type.

Looking  at the screenshot below, and you'll notice that the method indeed echoed the request body:


Now to demonstrate how to handle a request whose body is an array, create a new route.
 

<Route Url="/payload/echo-array-data" Method="POST" Call="EchoReceivedArrayData"></Route>

That is handled by this method:

ClassMethod EchoReceivedArrayData(receivedData As %DynamicArray) As %DynamicArray
{
  return receivedData
}


And the response should be similar to this:

As you can see, the main difference here is that the payload is an array now, so the argument type must also be modified to %DynamicArray.


While you're working with payloads you might try to use more than one payloadable argument just to be welcomed by an error. To simulate this case, modify the method to receive two payloads:

ClassMethod EchoReceivedArrayData(receivedData As %DynamicArray, receivedData2 As %DynamicObject) As %DynamicArray
{
  return receivedData
}

And you should get this response:



This is because an user agent can send only one message body, or payload per request.

Making it useful
 

Up to this pont, we worked strictly with by echoing the payload, but that's not a valid use case, right? So let's try something more sophisticated. Create a new route:

<Route Url="/payload/hello" Method="POST" Call="SayHelloToOurGuests"></Route>

And a method to deal with the request:

ClassMethod SayHelloToOurGuests(
myName As %String(ALIAS="name"),
messageAndGuests As %DynamicObject) As %DynamicObject
{
  
  set hellos = []
  
  for i=0:1:messageAndGuests.guests.%Size() - 1 {
    set guestName = messageAndGuests.guests.%Get(i)
    do hellos.%Push($$$FormatText("Hello %1! I'm %2!", guestName, myName))
  }
  
  return hellos
}

You can guess from the code above, but we basically take an object containing an array of guests names and return an array with a message for each guest using the "myName" aliased as name.

And here's the result:

This demonstrates that what we actually received is not an stream of a string containing the payload, but the parsed JSON already.


And that's it for the basic handling of payloads. So let's move into something even more advanced. As we have seen at Part 1, we can use different types of instances within %Dynamic instances, that's mostly because Frontier's marshalling engine, which normalizes every property and child instance into a compatible %Dynamic instance.

Unmarshalling payloads into instances
 

There's also an engine that does the inverse and that's what we're going to check out. Basically, what're going to do is to transform a %Dynamic instance into a %Persistent class. If you read the Part 1, then you have already seen something similar that was: inferring objects.

Though useful, inferring objects might not provide enough control over your object graph, since it only takes a single data assuming it as an object id. But what if we want to create or update an existing object? To do so, we need to pass a set of properties an even children to compose the object graph.

In order to demonstrate it, create a new route like that looks like this one:
 

<Route Url="/payload/person" Method="POST" Call="CreateNewPerson"></Route>

And add a method that handles the payload.
 

ClassMethod CreateNewPerson(person As %DynamicObject(UNMARSHALLTO="Sample.Person")) As %String
{
  set sc = person.%Save()
  $$$ThrowOnError(sc)
  
  return {
    "ok"(sc),
    "__id__"(person.%Save())
  }
}

This is not complicated as it sound since it's handled almost automatically. In order to unmarshall your instance, you just need to add a parameter to the receiving argument: notice that UNMARSHALLTO, this argument parameter indicates which class should the Unmarshaller use as basis to create a reflection from the receiving payload (%DynamicObject).

So now, when you execute this request we get the following feedback:

Where "ok" actually indicates that the %Save was sucessful, and it's id is 1. Notice that special __id__ format, this ensures that the id field cannot be replaced conventionally. This is useful to prevent crashing references with misleading id namings used by client applications.

Now let's create a new request and check if it really worked as expected. So create the route below:
 

<Route Url="/object/person/:person" Method="GET" Call="GetPersonById"></Route>

And a method for it.
 

ClassMethod GetPersonById(person As Sample.Person) As Sample.Person
{
  return person
}

And we get the expected result.

You'll find that the output has some few extra properties, like "DOB__x", this is the external value from the DOB (%Date) property that is automatically added. Still, you normally wouldn't return a %Persistent object because it contains properties that might not be relevant. I'd recommend wrapping it into a %DynamicObject instead and selecting which properties you want to expose.

Using the unmarshaller to EDIT an existing object

Not only you can create new objects, but edit them as well. To demonstrate we can modify our routes like to look like this:

<Route Url="/payload/person" Method="POST " Call="UpsertPerson"></Route>
<Route Url="/payload/person" Method="PUT" Call="UpsertPerson"></Route>

Now we have two routes that are calling the same method, we only have to do it in order to make the PUT request available. But the method itself was not modified.

ClassMethod UpsertPerson(person As %DynamicObject(UNMARSHALLTO="Sample.Person")) As %String
{
  set sc = person.%Save()
  $$$ThrowOnError(sc)
  
  return {
    "ok"(sc),
    "__id__"(person.%Save())
  }
}

The biggest difference is actually inside the payload that now includes the "__id__" property. Remember what I said before, that "__id__" is a special property, and here's one of the reasons: we can use it to edit our object. For this example, I added a new color in the FavoriteColors array.

If we use the GET from before to inspect the changes we can notice that FavoriteColors now has "Yellow" as well:


This is should be enough knowledge to create objects directly from payloads. Please take a note that properties not defined in the class will be ignored, so there won't have any risk of breaking the communication between the server and a legacy client that contain properties unknown to the class.

What you have learned here should make you able to create a router class with CRUD-like operations with minimal efforts.
With one exception: we didn't really covered the "R", as we aren't working with stored data efficiently yet, which means using SQL.

The Part 3 will cover how to use SQL with Frontier.

Stay close!


  • + 1
  • 0
  • 349
  • 0