Using the REST Library

From DataFlex Wiki
Jump to navigationJump to search

The simple example of a RESTful service set out in the article A Simple RESTful Service involved quite a bit of work to produce an API for only one table.

An alternative approach is to use the REST library I (Mike) have created, which can be downloaded from File:RESTLibrary.zip.

To use it, unzip that file into some directory, then, from the workspace you are developing in, use that as a library; in the Studio do: Tools --> Maintain Libraries... --> Add Library, then navigate to where you unzipped it and within it select the "RESTLibrary.sws" file.

That will add a new section to the Studio's Class Palette entitled "REST".

With the WebApp project selected in the Studio, do: File --> New --> Other --> DataFlex Source File, naming it, for this example, "WebOrderAPI", which will create a blank package file. Into that, from the new REST section of the class palette, drag an object of the cRESTfulService class. Rename the object oWebOrderAPI.

Then again do: File --> New --> Other --> DataFlex Source File, this time naming it ApiCustomersHandler. Into that blank file drag an object of the cRestResourceHandler class. Change the object's name to oApiCustomersHandler and set its psInterfacePath property (an empty string by default) to "customers".

Then in the DDO Explorer pane (lower-right in the Studio by default) click the "+" book icon (Add DDO: Add DD.jpg) and select the cCustomerDataDictionary.

Drag a cRESTApiObject from the class palette to below the comment "Add your cRESTApiObjects here". Rename it to "oCustomers". Set its phoDD property to oCustomer_DD, its psCollName (collection name) property to "customers" (plural) and its psInstName (instance name) property to "customer" (singular).

Uncomment one of the "Send AddListColumn" lines and change its "Table.Column" to Customer.Customer_Number, then copy that line and make the copy for Customer.Name. (You can keep adding those to your heart's content, but lists should be sparing of such detail.)

Go back to your WebOrderAPI file and under the comment line: 'Add "Use" statements for your cResourceHandler objects here:' add the line: "Use APICustomersHandler.pkg".

For development you may want to change the pbVerboseErrors setting to True, although for deployment it should probably be set back to False, to avoid confusing users with a pile of technical detail, which they neither should, nor want to, be exposed to.

You should perhaps, for future-proofing a real API, change the psPath setting from its default of "api" to "api/v1" (to provide for v2, v3 and so on in the future) - Note: this will change the URL which you use to make calls to it.

That should leave you with (some additional comments are mine):

WebOrderAPI.pkg:

Use cRESTfulService.pkg

Object oWebOrderAPI is a cRESTfulService
    Set psPath to "api/v1"       // Changed to provide for future versions
    Set pbVerboseErrors to True  // For development only, return to False for deployment
    
    // Add "Use" statements for your cResourceHandler objects here:
    Use APICustomersHandler.pkg  // You add this line
    
    Procedure APIRoot
        Handle hoResp
        
        Get CreateJsonObject to hoResp
        Send AddRegisteredCollections hoResp        
        
        Send OutputJson hoResp
    End_Procedure
    
    Procedure ProcessHttpRequest String sVerb
        String sPart0
        
        Get PathPart 0 to sPart0
        Move (Lowercase(sPart0)) to sPart0
        
        Case Begin
        
            Case ((sVerb = C_httpGet) and (sPart0 = ""))
                Send APIRoot
                Case Break
            
            Case (IsRegisteredPath(Self, sPart0))
                Send AutoProcess sPart0
                Case Break
        
            Case Else
                Send UnrecognisedOperation
                Case Break
        
        Case End
        
    End_Procedure

End_Object

APICustomersHandler.pkg:

Use cRESTResourceHandler.pkg
Use cCustomerDataDictionary.dd
Use cRESTAPIObject.pkg

Object oAPICustomersHandler is a cRESTResourceHandler
// Add your data-dictionary objects here

    // Use the DDO Explorer in the Studio to select DDOs:
    Object oCustomer_DD is a cCustomerDataDictionary
    End_Object

    Set Main_DD to oCustomer_DD

    Set psInterfacePath to "customers"  // We have to set this

// Add your cRESTApiObjects here (below the Set Main_DD line)

    // Simply drag cRESTApiObjects here from the Studio's Class Palette
    Object oCustomer is a cRESTApiObject
        Set phoDD       to oCustomer_DD  // We have to set these three
        Set psCollName  to "customers"   // properties for the object
        Set psInstName  to "customer"    // to work properly
        
        // Send AddListColumn File_Field Table.Column
        Send AddListColumn File_Field Customer.Customer_Number  // These lines determine
        Send AddListColumn File_Field Customer.Name             // the columns in the list
        
        Procedure PostProc Handle hoResp Integer iMode
            Forward Send PostProc hoResp iMode
            
            // Add dependant collections (or other links) in here, e.g.:
            // Send AddCollection hoResp "{name}" (InstURL(Self, iMode) + "/{collectionName}")
            // Send AddLink hoResp "{name}" {identifier} {URL}
        End_Procedure
    
    End_Object

End_Object

Now you can immediately see the saving: 81 lines (most of which were written for you and a couple of which are comments I have added for clarification) vs. the 574 lines in the Simple example, while most of the functionality is essentially the same (a careful examination will reveal a bit more "HATEOAS"-type information in some of the returned JSON in the former).

In addition, this approach has much more generality and extensibility: additional cRESTApiObjects, and the corresponding DDOs, can be added to the cApiCustomersHandler to provide for dependant sub-collections (since customers have orders, and orders have details), while other cRESTResourceHandler objects can be added to handle other top-level collections (orders, vendors, inventory, salespeople and so on).

The library (in the cRESTApiObjects) provides mechanisms to exclude specific fields from the interface (Send AddExcludeColumn Table.Column), or mark them as read-only through it (Send AddReadOnlyColumn Table.Column), include parent columns in collections or instances (through the AddListColumn and AddInstanceColumn procedures) and even provide calculated pseudo-columns (AddListFunctionColumn and AddInstanceFunctionColumn). Entire collections can be set as read-only (pbReadOnly), no-update (pbNoUpdate) or no-delete (pbNoDelete).

In your browser, or testing tool such as Postman or the DataFlex RESTTest view in another article in this series, you can access this one as ".../api/v1" (or just ".../api" if you didn't make the versioning change to the psPath setting).

Security

This example does not have any security built into it yet, but the cRESTfulService class does understand a bit about Basic Auth, so we can add the following short OnPreRequest procedure to the oWebOrderAPI object:

    Procedure OnPreRequest String sVerb String sPath
        tBasicAuthCredentials tCreds
        Boolean bOK
        
        Set psUsername to ""  // Reset it to blank at the start of *every* request
        Get BasicAuthCredentials to tCreds
        
        If (tCreds.sUserName <> "") Begin
            Clear WebAppUser
            Move tCreds.sUserName to WebAppUser.LoginName
            Find Eq WebAppUser by Index.1
            
            If (Found) Begin
                Move (ComparePasswords(ghoWebSessionManager, Trim(WebAppUser.Password), ;
                                                             tCreds.sPassword)) to bOK
                                                             
                If bOK ;
                    Set psUsername to tCreds.sUserName
            End
            
        End
        
    End_Procedure

As you can see, that relies on the ComparePasswords function of the WebSessionManager object, which will not be present in a Basic Web Project, so if using such, you would have to replace that, perhaps with a simple:

                Move (Trim(WebAppUser.Password) = Trim(tCreds.sPassword)) to bOK

Which assumes that you are not using encrypted passwords, however how to handle those is left as an exercise for the reader!

Then at the top of the file add "Open WebAppUser", while at the top of the oWebOrderAPI object add "Property String psUsername".

Finally, right at the beginning of the ProcessHttpRequest procedure add:

        If (psUsername(Self) = "") Begin
            Send BasicAuthRequired "Web Order API"
            Procedure_Return
        End

Fuller instructions for creating an API using the REST library can be found in my course notes from Synergy 2019, although complete documentation for the library has sadly yet to make it to the top of my ToDo pile (it will be a lot of work).

See also