A Simple RESTful Service: Difference between revisions
m Adding link |
m JSON link |
||
Line 603: | Line 603: | ||
</source> | </source> | ||
To modify (update) an existing customer you will need to use PATCH to that customer's ''instance'' (e.g.: .../customerAPI/customers/101), passing the JSON to update it with, e.g.: | To modify (update) an existing customer you will need to use PATCH to that customer's ''instance'' (e.g.: .../customerAPI/customers/101), passing the [[JSON]] to update it with, e.g.: | ||
<source lang="JSON"> | <source lang="JSON"> | ||
{ | { |
Revision as of 12:41, 13 August 2019
To create a service we will require a WebApp, and within that an object of the cWebHttpHandler class.
The code below shows how you might write a simple RESTful API for the sample Customer table (from the DataFlex Examples: WebOrder or WebOrderMobile, which will, by default, have been installed under "C:\DataFlex nn.n Examples" if you chose to install the Examples during the DataFlex installation process - it also uses the WebAppUser table from those examples).
Warning: This sample code exposes your customer table to the world (or, if using one of the security options below, to anybody who has valid credentials) - they can abuse that as they choose. It is probably not an approach you would want to take in a real-world environment without considerable enhancement! (Details on the discussion page.)
CustomerService.wo:
Use cWebHttpHandler.pkg Use cJsonObject.pkg Use cCustomerDataDictionary.dd // Used to validate users and their passwords when using BASIC authentication Open WebAppUser // Definitions of the various authentication types we will support Enum_List Define C_authTypeNone Define C_authTypeBasic Define C_authTypeBearer End_Enum_List Object oCustomerService is a cWebHttpHandler Property Integer peAuthType C_authTypeNone Property String psApiKey Property String psCachedBaseURL Property String[] pasPathParts Property Boolean pbAllowAccess False Property String psRealm "REST Demo" // The "realm" for use in BASIC Auth Set psPath to "customerAPI" // The URL *within* the WebApp's virtual directory Set psVerbs to "GET,POST,PATCH,DELETE" Set peErrorType to httpErrorJson Object oCustomer_DD is a cCustomerDataDictionary End_Object // // Utility methods // // This function will return the Base URL, so that URLs to other resources // within the API can be constructed. When first constructed it is then // cached in the psCachedBaseURL property so that we don't have to do it // each time, which is expensive on server messages and hurts performance. Function BaseURL Returns String String sHost sPort sProt sBase sURL sPath Boolean bSec Integer iPos Get psCachedBaseURL to sBase If (sBase = "") Begin Get ServerVariable "SERVER_NAME" to sHost Get ServerVariable "SERVER_PORT" to sPort Get ServerVariable "SERVER_PORT_SECURE" to bSec Get ServerVariable "URL" to sURL Get psRequestPath to sPath Move (RightPos(sPath, sURL)) to iPos Move (Left(sURL, (iPos - 1))) to sURL // Note: Case should not matter, but it seems that in Postman it // does, so stick to lowercase for the protocol: Move (If(bSec, "https", "http")) to sProt Move (sProt + "://" + sHost + ":" + ; sPort + sURL) to sBase Set psCachedBaseURL to sBase End Function_Return sBase End_Function // A simple function to find a customer row from the appropriate part // of the invoking URL using its DD Function FindCustomerFromRequest Returns Boolean String[] asParts Boolean bFound Get pasPathParts to asParts If (SizeOfArray(asParts) = 3) Begin Send Clear of oCustomer_DD Move asParts[2] to Customer.Customer_Number Send Find of oCustomer_DD EQ Index.1 Move (Found) to bFound If bFound ; // Check that you actually got the right one (should always be the case) Move (Customer.Customer_Number = asParts[2]) to bFound If not bFound ; Send SetResponseStatus 404 "Not Found" End Else ; // This shouldn't ever happen, but... Send SetResponseStatus 400 "Bad Request" Function_Return bFound End_Function // A simple function to update a customer row from the passed JSON through // its DD. However see Wil's note regarding this on the discussion page. Function UpdateCustomerFromJson Returns Boolean Handle hoReq UChar[] ucaReq Boolean bOK Integer i iMax iField iType String sField sVal Get RequestDataUChar 0 to ucaReq // Read the entire body into the UChar[] If (SizeOfArray(ucaReq) < 10) Begin // '{"x":"y"}' is the minimum that // makes sense: 9 chars Send SetResponseStatus 400 "Bad Request" Function_Return False End Get Create (RefClass(cJsonObject)) to hoReq Get ParseUtf8 of hoReq ucaReq to bOK If not bOK Begin Send SetResponseStatus 400 "Bad Request" Function_Return False End Get MemberCount of hoReq to iMax Decrement iMax Move False to Err For i from 0 to iMax Get MemberNameByIndex of hoReq i to sField Field_Map Customer.File_Number sField to iField If iField Begin Get_Attribute DF_FIELD_TYPE of Customer.File_Number iField to iType Get MemberValue of hoReq sField to sVal If (iType = DF_DATE) ; Move (ConvertFromClient(typeDate, sVal)) to sVal If (iType = DF_DATETIME) ; Move (ConvertFromClient(typeDateTime, sVal)) to sVal Set Field_Changed_Value of oCustomer_DD iField to sVal End Loop If (Err) ; Send SetResponseStatus 400 "Bad Request" Function_Return (not(Err)) End_Function // This will provide the entry-point to the API, listing all the collections // within it Procedure ApiRoot Handle hoResp hoColls hoColl UChar[] ucaResp Get Create (RefClass(cJsonObject)) to hoColls Send InitializeJsonType of hoColls jsonTypeArray // Do these six lines for each collection in the API: Get Create (RefClass(cJsonObject)) to hoColl Send InitializeJsonType of hoColl jsonTypeObject Send SetMemberValue of hoColl "name" jsonTypeString "customers" Send SetMemberValue of hoColl "href" jsonTypeString (BaseURL(Self) + "/customers") Send AddMember of hoColls hoColl Send Destroy of hoColl // Create the response object and add the collections array to it: Get Create (RefClass(cJsonObject)) to hoResp Send InitializeJsonType of hoResp jsonTypeObject Send SetMember of hoResp "collections" hoColls Send Destroy of hoColls // Return the serialized response object: Get StringifyUtf8 of hoResp to ucaResp Send Destroy of hoResp Send OutputUChar ucaResp End_Procedure // // The worker procedures to perform the GET (to the customer collection), // GET (to a specific customer instance), POST (to the customer collection) // to create a new row, PATCH (to update an existing instance) and DELETE // (to delete an instance) // Procedure CustomerList Handle hoResp hoCusts hoCust UChar[] ucaResp String sVal Get Create (RefClass(cJsonObject)) to hoCusts Send InitializeJsonType of hoCusts jsonTypeArray Send Find of oCustomer_DD FIRST_RECORD Index.1 While (Found) Get Create (RefClass(cJsonObject)) to hoCust Send InitializeJsonType of hoCust jsonTypeObject Get Field_Current_Value of oCustomer_DD Field Customer.Customer_Number to sVal Send SetMemberValue of hoCust "Customer_Number" jsonTypeInteger sVal Get Field_Current_Value of oCustomer_DD Field Customer.Name to sVal Send SetMemberValue of hoCust "Name" jsonTypeString sVal // More detail in the list can be provided by adding lines like the // two above // To be RESTful, we should provide links to our resources. The following // pair of lines provides a link to each specific customer detail from the // list - you might move this up to make it the first item for each entry // in the list, rather than the last. Get Field_Current_Value of oCustomer_DD Field Customer.Customer_Number to sVal Send SetMemberValue of hoCust "href" jsonTypeString (BaseURL(Self) + "/customers/" + sVal) Send AddMember of hoCusts hoCust Send Destroy of hoCust Send Find of oCustomer_DD NEXT_RECORD Index.1 Loop Get Create (RefClass(cJsonObject)) to hoResp Send InitializeJsonType of hoResp jsonTypeObject Send SetMember of hoResp "customers" hoCusts Send Destroy of hoCusts Get StringifyUtf8 of hoResp to ucaResp Send Destroy of hoResp Send OutputUChar ucaResp End_Procedure Procedure CustomerDetail Handle hoResp hoCust UChar[] ucaResp String sVal sField Boolean bFound Integer i iMax iType iJType iPrec If not (FindCustomerFromRequest(Self)) ; Procedure_Return Get Create (RefClass(cJsonObject)) to hoCust Send InitializeJsonType of hoCust jsonTypeObject Get_Attribute DF_FILE_NUMBER_FIELDS of Customer.File_Number to iMax For i from 1 to iMax Get_Attribute DF_FIELD_NAME of Customer.File_Number i to sField Get_Attribute DF_FIELD_TYPE of Customer.File_Number i to iType Get Field_Current_Value of oCustomer_DD i to sVal If (iType = DF_DATE) ; Move (ConvertToClient(typeDate, sVal)) to sVal If (iType = DF_DATETIME) ; Move (ConvertToClient(typeDateTime, sVal)) to sVal If ((iType = DF_ASCII) or ; (iType = DF_DATE) or ; (iType = DF_DATETIME) or ; (iType = DF_TEXT)) ; Move jsonTypeString to iJType If (iType = DF_BCD) Begin Get_Attribute DF_FIELD_PRECISION of Customer.File_Number i to iPrec If (iPrec = 0) ; Move jsonTypeInteger to iJType Else ; Move jsonTypeDouble to iJType End Send SetMemberValue of hoCust sField iJType sVal Loop Get Create (RefClass(cJsonObject)) to hoResp Send InitializeJsonType of hoResp jsonTypeObject Send SetMember of hoResp "customer" hoCust Get StringifyUtf8 of hoResp to ucaResp Send Destroy of hoResp Send OutputUChar ucaResp End_Procedure Procedure CreateCustomer Handle hoResp UChar[] ucaResp Boolean bErr Send Clear of oCustomer_DD If not (UpdateCustomerFromJson(Self)) ; Procedure_Return Get Validate_Save of oCustomer_DD to bErr If not bErr Begin Move False to Err Send Request_Save of oCustomer_DD Move (Err) to bErr End Get Create (RefClass(cJsonObject)) to hoResp Send InitializeJsonType of hoResp jsonTypeObject Send SetMemberValue of hoResp "Operation" jsonTypeString "Create Customer" Send SetMemberValue of hoResp "Result" jsonTypeString (If(bErr, "failed", "succeesed")) If not bErr ; Send SetMemberValue of hoResp "New Customer_Number" jsonTypeInteger Customer.Customer_Number Get StringifyUtf8 of hoResp to ucaResp Send Destroy of hoResp Send OutputUChar ucaResp End_Procedure Procedure UpdateCustomer Handle hoResp UChar[] ucaResp Boolean bErr If not (FindCustomerFromRequest(Self)) ; Procedure_Return If not (UpdateCustomerFromJson(Self)) ; Procedure_Return Get Validate_Save of oCustomer_DD to bErr If not bErr Begin Move False to Err Send Request_Save of oCustomer_DD Move (Err) to bErr End Get Create (RefClass(cJsonObject)) to hoResp Send InitializeJsonType of hoResp jsonTypeObject Send SetMemberValue of hoResp "Operation" jsonTypeString "Update Customer" Send SetMemberValue of hoResp "Result" jsonTypeString (If(bErr, "failed", "succeeded")) Get StringifyUtf8 of hoResp to ucaResp Send Destroy of hoResp Send OutputUChar ucaResp End_Procedure Procedure DeleteCustomer Handle hoResp UChar[] ucaResp Boolean bErr If not (FindCustomerFromRequest(Self)) ; Procedure_Return Get Validate_Delete of oCustomer_DD to bErr If not bErr Begin Move False to Err Send Request_Delete of oCustomer_DD Move (Err) to bErr End Get Create (RefClass(cJsonObject)) to hoResp Send InitializeJsonType of hoResp jsonTypeObject Send SetMemberValue of hoResp "Operation" jsonTypeString "Delete Customer" Send SetMemberValue of hoResp "Result" jsonTypeString (If(bErr, "failed", "scuceeded")) Get StringifyUtf8 of hoResp to ucaResp Send Destroy of hoResp Send OutputUChar ucaResp End_Procedure // If we are using "Basic Authentication", this function will validate that // the username and password passed with the request are valid in the // WebAppUser table Function ValidateBasic Returns Boolean String sCreds String[] asCreds Integer iLen Address pAddr Boolean bOK bValid Move False to bValid Get HttpRequestHeader "Authorization" to sCreds // Check that we have the right kind of authorization If (Left(Uppercase(sCreds), 6) = "BASIC ") Begin Move (Right(sCreds, (Length(sCreds) - 6))) to sCreds // base64 decode the credentials string: Move (Length(sCreds)) to iLen Move (Base64Decode(AddressOf(sCreds), &iLen)) to pAddr Move (Repeat(Character(0), iLen)) to sCreds Move (MemCopy(AddressOf(sCreds), pAddr, iLen)) to bOK Move (Free(pAddr)) to bOK // Split username and password: Move (StrSplitToArray(sCreds, ":")) to asCreds // Only if there are exactly two parts: If (SizeOfArray(asCreds) = 2) Begin // Is it a vaild user? Clear WebAppUser Move asCreds[0] to WebAppUser.LoginName Find eq WebAppUser by Index.1 If (Found) Begin Move (asCreds[1] = WebAppUser.Password) to bValid End End End If not bValid Begin // Deny access, but tell invoker that we are using basic auth: Send AddHttpResponseHeader "WWW-Authenticate" ; ('Basic realm="' + psRealm(Self) + '"') Send SetResponseStatus 401 "Unauthorized" End Function_Return bValid End_Function // If we are using "Bearer Authentication" (in this simle case with an // API key, issued to users), this will validate it Function ValidateBearer Returns Boolean String sKey sPassed Boolean bOK Move False to bOK Get psApiKey to sKey Get HttpRequestHeader "Authorization" to sPassed // Check that we have the right kind of authorization If (Uppercase(Left(sPassed, 6)) = "BEARER") Begin Move (Right(sPassed, (Length(sPassed) - 7))) to sPassed Move (sPassed = sKey) to bOK End If not bOK ; Send SetResponseStatus 401 "Unauthorized" Function_Return bOK End_Function // OnPreRequest is called on every request, prior to other processing. // We will use it to work out whether to allow access for a request. Procedure OnPreRequest String sVerb String sPath Integer iAuthType Set pbAllowAccess to False Get peAuthType to iAuthType If (iAuthType = C_authTypeBasic) ; Set pbAllowAccess to (ValidateBasic(Self)) Else If (iAuthType = C_authTypeBearer) ; Set pbAllowAccess to (ValidateBearer(Self)) Else ; Set pbAllowAccess to True End_Procedure // This is the main procedure that works out how to handle (or reject) requests Procedure OnHttpRequest String sVerb String sPath String sContentType String sAcceptType Integer iSize String[] asPathParts UChar[] ucaResp Integer iParts If not (pbAllowAccess(Self)) ; Procedure_Return // The return ststus will already have been set // elsewhere - just exit // All of our responses will be in JSON (if they work), so we can set // the response content type here Send AddHttpResponseHeader "Content-type" "application/json" // Need to do this to mark response as not cacheable Send AddHttpResponseHeader "Cache-Control" "no-cache" // We parse the URL into parts and store those for later use as well as // using them in the Case statement below Move (StrSplitToArray(sPath, "/")) to asPathParts Move (SizeOfArray(asPathParts)) to iParts Set pasPathParts to asPathParts Case Begin Case ((sVerb = "GET") and ; (sPath = "")) Send ApiRoot Case Break Case ((sVerb = "GET") and ; (iParts = 2) and ; (asPathParts[1] = "customers")) Send CustomerList Case Break Case ((sVerb = "GET") and ; (iParts = 3) and ; (asPathParts[1] = "customers") and ; (asPathParts[2] <> "")) Send CustomerDetail Case Break Case ((sVerb = "POST") and ; (iParts = 2) and ; (asPathParts[1] = "customers")) Send CreateCustomer Case Break Case ((sVerb = "PATCH") and ; (iParts = 3) and ; (asPathParts[1] = "customers") ; and (asPathParts[2] <> "")) Send UpdateCustomer Case Break Case ((sVerb = "DELETE") and ; (iParts = 3) and ; (asPathParts[1] = "customers") and ; (asPathParts[2] <> "")) Send DeleteCustomer Case Break Case Else Send SetResponseStatus 400 "Bad Request" Case End End_Procedure End_Object // This procedure (which is called immediately) allows us to set up the // authentication type (NONE, BASIC or BEARER) to use, and in the case // of BEARER, also set the API key, dynamically from parameters passed // to the application on the command line, rather than hard-coding the // properties involved. Procedure SetAuthDetails Handle hoCmdLine Integer iNumArgs iArg String[] asArgs Get phoCommandLine of ghoApplication to hoCmdLine Get CountOfArgs of hoCmdLine To iNumArgs For iArg from 1 to iNumArgs Move (StrSplitToArray(Argument(hoCmdLine, iArg), "=")) to asArgs If (SizeOfArray(asArgs) = 2) Begin Move (Uppercase(asArgs[0])) to asArgs[0] If (asArgs[0] = "AUTH") Begin Move (Uppercase(asArgs[1])) to asArgs[1] If (asArgs[1] = "BASIC") ; Set peAuthType of oCustomerService to C_authTypeBasic Else If (asArgs[1] = "BEARER") ; Set peAuthType of oCustomerService to C_authTypeBearer Else ; Set peAuthType of oCustomerService to C_authTypeNone End If (asArgs[0] = "APIKEY") Begin Set psApiKey of oCustomerService to asArgs[1] End End Loop End_Procedure Send SetAuthDetails
You should be able to copy and paste this code to see it in operation, as follows:
Your workspace should contain the tables Customer (perhaps copied from the WebOrder sample) and WebAppUser. (You may find that if you have only copied the Data and DDSrc directories from the samples that you need to comment out the line "Use CustomerWebLookup.wo" in the cCustomerDataDictionary.dd file in order to get things to compile.)
If your workspace does not already have a WebApp, in the Studio use File --> New --> Project --> Basic Web Project (or Desktop Web Project, if you prefer) to create one first.
Then with the WebApp as the current project (selector top-left on the Studio tool-bar by default), use File --> New --> Web Object --> WebHttpHandler, calling the object oCustomerService; then select all (CTRL-A) and paste the code above over the existing contents.
If you then compile and run the WebApp (F5), and change the URL in the web browser (Firefox is best for this, as it displays the returned JSON in the nicest format), replacing "Index.html" with "customerAPI" (the psPath setting in the code) you should see the output produced by the "ApiRoot" procedure, which will (at this point) have a link to a single collection: "customers". Clicking (in Firefox, at least) on the "href" value for that should then display a list of customers, each of which should in turn have an "href" element, clicking on which will take you to the details for that customer (the instance).
To do more than simply display (i.e. GET) data, you will have to use a tool such as Postman (click the "Get Started" button, then the "Download" button, selecting the version - 32 or 64 bit - as appropriate for your machine, saving and then running the downloaded file) or the RESTTester tool detailed in the Consuming RESTful Services in DataFlex article.
To create new customer rows you will need to use POST to the customers collection (e.g.: .../customerAPI/customers), passing the required JSON in the request body (in Postman go to the request "Body" tab and select "raw" just below that). At a minimum that should be something like:
{ "Name": "My test customer", "EMail_Address": "customer@sample.org" }
To modify (update) an existing customer you will need to use PATCH to that customer's instance (e.g.: .../customerAPI/customers/101), passing the JSON to update it with, e.g.:
{ "Address": "My house in my street", "Comments": "Where I live" }
To delete a customer (which in most cases will be prevented by the Data Dictionary if that customer has orders attached to it) you will need to use DELETE to that customer's instance (e.g.: .../customerAPI/customers/101).
Security
The above sample has two different security mechanisms built in, although by default neither of them are active.
To activate them you should pass parameters on the command line (as a nicer alternative to hard-coding them) - these will be picked up by that "SetAuthDetails" procedure at the end of the example. This can be done in the WebApp Administrator. In that, select the application in question and right-click it, choosing the "Web Application Properties" option. In the "Parameters" box on the "General" tab enter the required parameters. (You will need to restart the web application for your changes to take effect: File --> Restart Web Application.)
To set up the same thing for debug-running in the Studio, do Project --> Project Properties <WebApp.src> and in the "General" tab enter the parameters you require in the "Parameters" box.
(Note: parameters are separated from each other by spaces.)
To use an authorization type of "Basic Auth" (username and password) pass the parameter "Auth=Basic", while to use a Bearer token use "Auth=Bearer ApiKey=WhateverKeyYouWantToUse". If you use bearer tokens you will not be able to access the API in a browser any more, however all browsers understand Basic Auth and will prompt you for credentials, which they will then "remember" for you (to make them "forget" those credentials again, do Ctrl-Shift-Del in the browser and select "Active Logins" in Firefox, or "Other Site Data" in Chrome - I can't work out how to do it in IE).
Of course, either of those authorization mechanisms rely on requests being made over a secure connection (HTTPS), so for deployment they absolutely must only be used in an environment where that is enforced, otherwise the passwords or API keys being employed will be exposed to any bad guys listening in on the connection.