Getting Started Web Services v1

Web Services v1

time to complete
15 minutes

Introduction

In this part, we’ll build out the suite of RESTful web services needed for the business to function. A well-designed system never allows business applications to operate directly on the database. Instead, we provide access via services so that we can control and monitor the actions taken.

With most databases, you build these services with any commodity framework you want — for example, Java Spring, Python Flask, or Node.js Express, — and talk to the database layer via SQL. You can certainly do that as well, but with InterSystems IRIS, you have another an easier and higher performance option: ObjectScript.

ObjectScript gives you the performance of stored procedures, but also the flexibility, power and ease-of-use of a real programming language.

The other big win with InterSystems IRIS is you don’t need to configure a web server and middleware layer when you’re using ObjectScript. It’s all built-in! Once you get the hang of it, ObjectScript is the fastest way to build the back end for web apps.

REST services

Let’s define the services needed throughout the company, from warehouse to catalog.

The roastery doesn’t need to manage any information. It takes raw beans from inventory, cooks it, then puts the product into the catalog. It needs three services:

  • listbeans: query the inventory for beans that can be roasted
  • getbeans: take a specific item and amount out of inventory
  • catalogproduct: put newly roasted product into the catalog

The storefront doesn’t need to know anything about the raw coffee. All we care about there is the roasted product. So, in the storefront, we need to know what products are available, how much we have, and how old it is. When we make a sale, we need to take that stock out of the catalog.

This will require two services:

  • getproducts: get products and be able to filter by ID and type (product code)
  • sellproduct: decrement the catalog’s stock after a sale

 

tip
Tip
All the ObjectScript files mentioned here have already been created and exist in the repository in the services/cls_Sample directory. If you get stuck, just look at the companion files there to check your work.

Connect the IDE to the database

  1. Open coffeeco/.vscode and type this into settings.json:
    "objectscript.conn": {
        "active": true,
        "label": "DEMO",
        "host": "(setting requires sandbox - click here)",
        "port": (setting requires sandbox - click here),
        "username": "tech",
        "password": "demo",
        "ns": "USER",
        "https": false,
    }
    

JSON-enable the Data Tables

It’s common practice to use JSON as the transport format for data moving between REST services. In most systems, JSON responses are completely disconnected from your data schema. That creates a situation where every developer who codes a JSON response must know exactly how all the other developers on the project have coded their responses. This is usually handled by having a specification document like Swagger or OpenAPI that defines every response format, but that introduces an extra level of complexity as well as potential errors into your code. For this reason, it would be better to describe the database-to-JSON transformation in the same place — and InterSystems IRIS allows you to do just that.

In addition to ObjectScript code, IRIS classes also include Universal Class Definition Language, which is a fancy way of saying that when you define a variable in code — which we call a property — you can also include annotations on the property that specify how to express that property as a column in a relational table, and as a key in JSON. Here’s an example:

Property vendorid As %Library.String(%JSONFIELDNAME = "vendor_id", MAXLEN = 128) [ SqlColumnNumber = 2, SqlFieldName = vendor_id ];

You can read this as saying: This class has a property named vendorid, which is a string. In ObjectScript, it is referred to as vendorid. In JSON, it will be called vendor_id, and it will also be called vendor_id in relational tables and SQL statements (the SqlFieldName annotation). Its maximum length is 128 characters, and it will be found in column 2 of the table.

The good news is that you’ll rarely have to code all of this yourself. When you created the table using the CREATE TABLE SQL statement earlier, you already defined everything but the %JSONFIELDNAME annotation.

SQL CREATE TABLE also creates ObjectScript Classes

With that conceptual background, let’s JSON-enable the two relational tables we created earlier, ICO.inventory and ICO.catalog. When the SQL CREATE TABLE statements ran, they also created corresponding IRIS classes with the same names, ICO.inventory.cls and ICO.catalog.cls. Open these now and on the first line (the class definition), change:

Extends %Persistent

to:

Extends (%Persistent, %JSON.Adaptor)

This adds another built-in class, %JSON.Adaptor, to the list of classes this one extends (multiple inheritance!). Extending %Persistent means instances of this class (actual data) are persisted in the database.

For every property whose name you want to be different when you express it as JSON, you need to specify the new name to use with the %JSONFIELDNAME annotation. In this case, we’re just adding an underscore between words, but you could make the name any valid JSON. In ICO.inventory.cls, change:

Property vendorid As %Library.String(MAXLEN = 128)…
Property vendorproductcode As %Library.String(MAXLEN = 128)…
Property quantitykg As %Library.Numeric(MAXVAL = 99999999.99…
Property datearrival As %Library.Date [ SqlColumnNumber = 5…
Property datedeparture As %Library.Date [ SqlColumnNumber = 6, …

to:

Property vendorid As %Library.String(%JSONFIELDNAME = "vendor_id", MAXLEN = 128) [ SqlColumnNumber = 2, SqlFieldName = vendor_id ];
Property vendorproductcode As %Library.String(%JSONFIELDNAME = "vendor_product_code", MAXLEN = 128) [ SqlColumnNumber = 3, SqlFieldName = vendor_product_code ];
Property quantitykg As %Library.Numeric(%JSONFIELDNAME = "quantity_kg", MAXVAL = 99999999.99, MINVAL = -99999999.99, SCALE = 2) [ SqlColumnNumber = 4, SqlFieldName = quantity_kg ];
Property datearrival As %Library.Date(%JSONFIELDNAME = "date_arrival") [ SqlColumnNumber = 5, SqlFieldName = date_arrival ];
Property datedeparture As %Library.Date(%JSONFIELDNAME = "date_departure") [ SqlColumnNumber = 6, SqlFieldName = date_departure ];

Note that if you want the JSON name to be the same as the ObjectScript name, don’t do anything. You don’t even need a %JSONFIELDNAME annotation. Also, note the SqlFieldName annotation at the end of each property. This annotation works the same way. For now we’re making them the same, but we could have used different names for ObjectScript, SQL, and JSON if necessary. InterSystems IRIS always gives you maximum flexibility.

Now annotate cls/ICO/catalog.cls where needed. If you get confused, remember to refer to the companion cls_Sample/ICO/catalog.cls file for the exact syntax.

warning
Warning
insert some SQL queries to play with the data

Your first RESTful service

With the basic JSON support set up, we can build a service. Create a new file cls/ICO/Handler.cls, and write the following:

Class ICO.Handler Extends %CSP.REST
{
Parameter HandleCorsRequest = 1;
Parameter CONTENTTYPE = "application/json";
XData UrlMap [ XMLNamespace = "http://www.intersystems.com/urlmap" ]
{

    

}
}

Since this is probably your first introduction to ObjectScript, let’s step through this code slowly.

First, we specify this is a class named Handler in the namespace ICO. It extends %CSP.REST, which is a built-in class that makes this a special class that knows how to process HTTP requests and responses. The HandleCorsRequest parameter is set to 1 to say that we’ll allow Cross-Origin Resource Sharing (CORS). The CONTENTTYPE parameter says that our default response type will be JSON.

Create routes

Now look at the XData section. This is how you route requests coming in from the web to methods in our code. All REST frameworks have a way to do this, and InterSystems IRIS is no different. In this case, we route requests by defining what’s called an XData section that looks a bit like XML.

We have a <Routes> element, which has any number of <Route> elements. Each <Route> has three attributes: a URL path, an HTTP Method, and a Call. Call is the name of the function to call when this route is hit. Method is the HTTP method (such as GET, POST, PUT, DELETE) to listen for. And URL is the path to listen on. The full URL to access this endpoint will be a combination of your server address, a base endpoint we’ll define later, and this URL path.

Here we have a single route. When a client invokes an HTTP GET request on /inventory/listbeans, the method ListRawBeans is called.

Write route-handling methods

Now we’ll write the method. Before the XData section, type (or copy and paste) the code below:

ClassMethod ListRawBeans() As %Status
{
    try {
        set sqlquery = "SELECT ID, vendor_product_code, date_arrival, SUM(quantity_kg) AS quantity_kg FROM ICO.inventory GROUP BY vendor_product_code ORDER BY date_arrival"
        set rs = ##class(%SQL.Statement).%ExecDirect(,sqlquery)
        set itemsarray = []

        while rs.%Next()
        {
            do itemsarray.%Push(
                {
                    "id": (rs.%Get("ID")),
                    "vendor_product_code": (rs.%Get("vendor_product_code")), 
                    "date_arrival": (rs.%Get("date_arrival")), 
                    "quantity_kg": (rs.%Get("quantity_kg"))
                }
            )
        }
        set itemsobj = {}
        set itemsobj."rowcount" = rs.%ROWCOUNT
        set itemsobj."items" = itemsarray
        write itemsobj.%ToJSON()
    } catch (oException) {
        set expobj = {}
        set expobj."exception" = oException
        write expobj.%ToJSON()
    }
    Quit $$$OK
}

The first line is the function definition. Functions in ObjectScript are called ClassMethod (or Method in some cases), and this one is ListRawBeans. It takes no arguments, and it will return a status code to the caller in the %Status return argument. This is separate from whatever it returns in the HTTP response.

Next, we have a try-catch block, so that we can catch any exceptions and handle them gracefully. In this case, we just convert the exception into JSON and return it to the client, but you could get fancier.

Here’s the interesting part: This method returns a listing of the inventory of available coffee beans, and we query the database to get that information. ObjectScript has many ways to query the database, but here we’ll simply use SQL. We define a valid SQL query in a string variable called sqlquery. Then we execute the query and store a pointer to the results, which is a ResultSet class, in a variable called rs using this one-liner:

set rs = ##class(%SQL.Statement).%ExecDirect(,sqlquery)
tip
Tip
There are more sophisticated ways to run a SQL query that give you more control over status and error handling, but this is the simplest.

We then specify an array, itemsarray, to hold our JSON response, and iterate over the ResultSet using a while loop: while rs.%Next(). Calling %Next() on rs makes rs point to the next record in the SQL result set, so we can push an object onto our JSON array using rs.%Get() commands to get the values of individual items in the record.

After putting all of the records into the array, we assign the array to an “items” key in a new JSON object called itemsobj. Finally, we write the itemsobj to JSON and send it back to the client using the built-in function, %ToJSON(). On the last line, we return a success status code to the calling command with the statement Quit $$$OK.

tip
Tip
Most functions that handle GET requests will work much the same as above:

  1. Query the database using SQL.
  2. Iterate over the response and transform it to JSON.
  3. Return the result.

The final step in making the REST service work is to expose it to the web. You might recall that we need to define a base endpoint for this. Let’s return to the Management Portal and hook this IRIS class into the web.

Go to (setting requires sandbox - click here) and navigate to System Administration > Security > Applications > Web Applications. Then follow these steps:

  1. Click the Create New Web Application button.
  2. Under Name, enter /api/coffeeco (the base endpoint).
  3. Under Namespace, select USER.
  4. Make sure Enabled Application is checked.
  5. Under Enable, select REST.
  6. For the Dispatch Class, type ICO.Handler (the IRIS Class all URLs starting with /api/coffeeco will be dispatched to).
  7. Under Security Settings, check Unauthenticated and Password for Allowed Authentication Methods.
  8. Click Save next to the title Edit Web Application.

Now you will see new tabs, one of which is Application Roles. Click that tab, then %ALL under the Available list, then click the right arrow to copy it to the Selected list. Click Assign.

Test the service

Now we can hit this REST endpoint. The full URL is:

server host and portApplication nameRoute
https://(setting requires sandbox - click here):(setting requires sandbox - click here)/api/coffeeco/inventory/listbeans

Here’s what the whole thing should look like:

https://(setting requires sandbox - click here)/api/coffeeco/inventory/listbeans

Put this URL into a new browser window to make the REST call.

It should return this:

{
    "rowcount": 5,
    "items": [{
        "id": "1",
        "vendor_product_code": "ETHIOPA32",
        "date_arrival": "65541",
        "quantity_kg": 400
    }, {
        "id": "2",
        "vendor_product_code": "BRAZILPREM",
        "date_arrival": "65541",
        "quantity_kg": 200
    }, {
        "id": "3",
        "vendor_product_code": "GUATEMALAALT30",
        "date_arrival": "65559",
        "quantity_kg": 200
    }, {
        "id": "4",
        "vendor_product_code": "SUMATRA2",
        "date_arrival": "65559",
        "quantity_kg": 200
    }, {
        "id": "5",
        "vendor_product_code": "SUMATRA3",
        "date_arrival": "65559",
        "quantity_kg": 400
    }]
}

Move beans to the roastery

Now that you know the basics, things will move much faster.

Create routes

We will need a service for taking beans out of inventory so that they can be roasted. Add this route to <Routes>:

    

The new elements here are the variables id and quantity in the URL. Any word in the URL prefixed with a colon (:) is a variable that will get passed to the Call method. Also note we’re using a POST request instead of GET.

note
Note:
Even though this request has no data in the body, it’s good practice to use POST when you are making a request that is not idempotent. In other words, multiple identical requests will not have the same effect as a single request. In this case, GetRawBeans requests will deplete the stock of inventory, so eventually you will make a call that can’t be fulfilled.

Write route handling methods

Here’s the code for GetRawBeans. Type or paste this above ListRawBeans.

ClassMethod GetRawBeans(id As %String, quantity As %Numeric) As %Status
{
    try {
        if (1 '= ##class(ICO.inventory).%ExistsId(id))
        {
            set err = {}
            set err."error" = "ID "_id_" does NOT exist!"
            write err.%ToJSON()
        }
        else 
        {
            set item = ##class(ICO.inventory).%OpenId(id)
            if (quantity > item.quantitykg)
            {
                set err = {}
                set err."error" = "You tried to get "_quantity_", but we only have "_item.quantitykg_" kilograms available."
                write err.%ToJSON()
            }
            else 
            {
                set item.quantitykg = (item.quantitykg - quantity)
                set sc = item.%Save()
                do item.%JSONExportToString(.outstring)
                write outstring
            }
        }
    } catch (oException) {
        set expobj = {}
        set expobj."exception" = oException.%AsSystemError()
        write expobj.%ToJSON()
    }
    Quit $$$OK
}

The first thing you should notice is this method has input arguments, id and quantity. They came straight from the URL. The client figured out what product IDs were available and how much was available in inventory by calling listbeans. Hopefully they then constructed a valid request, but since we’re good programmers, most of this method’s code is error checking to make sure.

Besides error checking, we’re looking up a record in the database by ID, subtracting the requested amount from the inventory on hand, and returning the requested amount.

Earlier, we queried the database using SQL. In cases where you only need to get a single record (object) from the database and you know its unique ID, ObjectScript makes things even simpler with a convenient set of built-in functions for accessing the database in this way. You can call the %ExistsId function to check if an object with that ID exists and use the %OpenId function to get it.

Notice that in the code, we do everything in a try block so we can elegantly handle errors, returning an exception if something goes wrong. In the try, we check if the ID exists in the ICO.inventory table ( ‘= is ObjectScript syntax for not equal to). If it doesn’t, we send an error back to the client. If it does, we use %OpenId to get it.

Next, we check if we have enough quantity on hand to fulfill the request. If not, another error message is returned. If the quantity is enough, decrement the inventory and save the new value to the database with a final, convenient function that avoids writing SQL, %Save().

For a response, we’ll return the new amount on hand, in case the client wants to request more. We do this by exporting the record to a JSON string using the built-in function %JSONExportToString and write the JSON out to the client.

Use the services

This request can’t be tested by pasting the URL into a browser because you can’t send POST requests that way. Open a command line terminal and type:

curl -X POST https://(setting requires sandbox - click here)/api/coffeeco/inventory/getbeans/1/2.4

Put roasted beans into the sale catalog

The roasting process is tricky, and a lot can happen in that step. Coffee beans can be bad. The roasting can be done wrong, and more. The only thing we know is the roasting process dries the beans, so we get less volume than went in. It’s a process involving real artisanal skill, so we won’t try to capture it here. Let’s just assume bags of freshly roasted coffee come out of the roastery and need to be put into the catalog so the online store can sell them.

Create routes

We’ll need a new route to advertise, so add this to the <Routes> section:

    

Write route handling methods

Below are the listing for the CatalogProduct method and the GetJSONFromeRequest method we’ll need to process the POST body:

ClassMethod GetJSONFromRequest(Output obj As %DynamicObject) As %Boolean
{
    set ok = 1
    try {
        set obj = ##class(%DynamicObject).%FromJSON(%request.Content)
    } catch ex {
        set ok = 0
    }
    Quit ok
}

ClassMethod CatalogProduct() As %Status
{
    if '..GetJSONFromRequest(.obj) {
        set %response.Status = ..#HTTP400BADREQUEST
        set error = {"error": "No JSON body in request"}
        write error.%ToJSON()
        Quit $$$OK
    }

    try {
        set catobj = ##class(ICO.catalog).%New()
        set catobj.productcode = obj."product_code"
        set catobj.quantity = obj."quantity"
        set catobj.price = obj."price"
        set catobj.timeroasted = obj."time_roasted"
        set catobj.roastingnotes = obj."roasting_notes"
        set catobj.img = obj."img"
        set sc = catobj.%Save()
        set resp = {}
        set resp."success" = 1
        write resp.%ToJSON()
    } catch (oException) {
        set expobj = {}
        set expobj."exception" = oException.%AsSystemError()
        set %response.Status = ..#HTTP400BADREQUEST
        write expobj.%ToJSON()
    }
    Quit $$$OK
}

Like GetRawBeans, we’re only accepting POST requests, but this time we look at the body of the POST for the first time. Two interesting things are happening in the first line of this method:

if '..GetJSONFromRequest(.obj)

We’re calling GetJSONFromRequest with two dots before it, which simply means: find this method in this class. We pass obj with a single dot before it, which is syntax for passing a variable by reference. Instead of passing a copy of this variable, putting a dot in front of it means the called method gets a reference to the same, so any changes it makes will be appear back in the calling method.

In GetJSONFromRequest, we could do more error checking, but here we simply translate the JSON body from the POST request — %request.Content — into a built-in type that ObjectScript can work with, a %DynamicObject class.

Where did %request come from? Remember that this class extends %CSP.REST, the built-in class that takes care of a lot of the REST implementation details. That comes into play here, as %CSP.REST automatically provides the ICO.Handler class with a variable called %request which, among other things, holds the content of the POST body in its content property.

Once we get that back as a %DynamicObject, we could use SQL to save it to the database, but it is easier to do it the object-oriented way. We create a new ICO.catalog object, fill it with data from the POST request, and save it to the database using the built-in %Save method available for all persistent classes.

Use the services

Let’s use this service to add some product to the catalog. In the services/samples directory, you’ll find a number of JSON files pre-populated with sample data. You can change anything you want here such as quantities or prices, or write some descriptive roasting notes. For the online store to work properly, you must change the “time_roasted” values to be between one and ten days earlier than today (the day you’re doing this exercise).

After fixing the dates, open a terminal and go to the services/samples directory and run the loadproducts.sh script to run a curl command that iterates through every JSON file in the directory and uses the web service you just wrote to load the data into ICO.catalog. Make sure to fix the server and point so that they point to your instance of InterSystems IRIS.

If you’re not comfortable running a script you didn’t write yourself (which is smart from a security perspective), you can manually run your own curl commands to load the data. Here’s an example:

curl -d "@product_brazil_dark.json" -H "Content-Type: application/json" -X POST https://(setting requires sandbox - click here)/api/coffeeco/catalog/catalogproduct

At this point, we’ve taken shipments of raw coffee beans, put them into inventory, taken some out to roast, packaged them, and put them in the ICO.catalog table so they can be sold.

Services for the online store

Now we will begin building the services needed for the front-end online storefront. The web developers will need a set of services available to them for:

  1. Getting freshly roasted bags of coffee available for sale
  2. Getting older bags of coffee that should be sold at a discounted rate
  3. Recording sales of bags of coffee

The first two are easy. They are read-only services, so we can simply use GET requests. We can even use the same method for both, and just have an input argument that tells us whether to return fresh or older stock.

Here are the routes to add to the <Routes> section. The first returns all fresh coffee. The second is the same except an extra parameter is added at the end to signal that we want older coffee (fresh should equal 0). The third is for the client to record the sale of a number of bags of a specific product.

     
    
    

Now let’s write the class method. Here’s the code listing to type or copy and paste into the ICO.Handler class:

Parameter MAXAGE = 5;

ClassMethod GetProducts(fresh As %Boolean = 1) As %Status
{
    try {
        set sqlquery = "SELECT catalog_id, product_code, quantity, price, time_roasted, roasting_notes, img FROM ICO.catalog"
        if fresh = 1 {
            set sqlquery = sqlquery_" WHERE time_roasted > DATEADD('day',-?,CURRENT_DATE)"
        } else {
            set sqlquery = sqlquery_" WHERE time_roasted <= DATEADD('day',-?,CURRENT_DATE)"
        }
        set rs = ##class(%SQL.Statement).%ExecDirect(,sqlquery, ..#MAXAGE)
        set itemsarray = []

        while rs.%Next()
        {
            do itemsarray.%Push(
                {
                    "catalog_id": (rs.%Get("catalog_id")),
                    "product_code": (rs.%Get("product_code")), 
                    "quantity": (rs.%Get("quantity")),
                    "time_roasted": (rs.%Get("time_roasted")),
                    "roasting_notes": (rs.%Get("roasting_notes")),
                    "img": (rs.%Get("img")),
                    "price": (rs.%Get("price"))
                }
            )
        }
        set itemsobj = {}
        set itemsobj."rowcount" = rs.%ROWCOUNT
        set itemsobj."products" = itemsarray
        write itemsobj.%ToJSON()
    } catch (oException) {
        set expobj = {}
        set expobj."exception" = oException
        write expobj.%ToJSON()
    }
    Quit $$$OK
}

Notice two new concepts here. The first is a constant value. Before the class method, we define the constant Parameter MAXAGE = 5. Then, in the class method we have an input argument, fresh. For the first time, we define a default value if the argument does not exist. If fresh is 1 we query for fresh coffee. If not, we query for old coffee bags.

The rest of the code you’ve seen before in the ListRawBeans method. We query using SQL, iterate through the returned records, and write each to an array. Finally, we put the array into a JSON object and return it to the caller.

Make a sales transaction

Our final service records coffee sales, SellProduct. It takes as input the product ID and the quantity of bags being sold. This is extremely simplified, as we won’t do any error checking or special payment handling or shipping. We’ll just decrement the catalog’s quantity of coffee bags, assuming everything else is taken care of on the front end. We also will assume if the customer bought multiple products, the client will send a SellProduct request for each. Just like we did in the GetRawBeans method, we’ll take advantage of ObjectScript’s convenience methods for querying records when you know their ID: %ExistsId, %OpenId, and %Save. Since this method is so similar to GetRawBeans, there’s nothing new to explain. Here’s the code:

ClassMethod SellProduct(id As %String, quantity As %Numeric) As %Status
{
    try {
        if (1 '= ##class(ICO.catalog).%ExistsId(id))
        {
            set err = {}
            set err."error" = "Catalog ID "_id_" does NOT exist!"
            write err.%ToJSON()
        }
        else 
        {
            set item = ##class(ICO.catalog).%OpenId(id)
            if (quantity > item.quantity)
            {
                set err = {}
                set err."error" = "You tried to get "_quantity_" bags, but we only have "_item.quantity_" available."
                write err.%ToJSON()
            }
            else 
            {
                set item.quantity = (item.quantity - quantity)
                do item.%JSONExportToString(.outstring)
                write outstring
                set sc = item.%Save()
            }
        }
    } catch (oException) {
        set expobj = {}
        set expobj."exception" = oException.%AsSystemError()
        write expobj.%ToJSON()
    }
    Quit $$$OK
}

Use the services

Query for fresh products:

curl https://(setting requires sandbox - click here)/api/coffeeco/catalog/getproducts

Query for stale:

curl https://(setting requires sandbox - click here)/api/coffeeco/catalog/getproducts/0

Try selling products:

Curl statement here…

Build the Storefront with Vue.js
Exercise
time to complete
10 minutes
Be a front-end developer and build the coffee selling storefront with a simple Vue.js single-page app.