open-insight/FRAMEWORKS/STPROC/HTTP_CONTACTS_SERVICES.txt
2024-03-25 15:15:48 -07:00

536 lines
29 KiB
Plaintext

Function HTTP_Contacts_Services(RemainingURL)
/***********************************************************************************************************************
This program is proprietary and is not to be used by or disclosed to others, nor is it to be copied without written
permission from SRP Computer Solutions, Inc.
Name : HTTP_Contacts_Services
Description : Handler program for the HTTP Contacts service module.
Notes : In the comments below, the term "resource" will be used. In most cases this is synonymous with a
database row, but the web (and especially REST) abstracts all information being returned simply as a
"resource". This provides developers more flexibility in their web API designs. For instance, a
resource can be a combination of various different database rows and other data (like images,
documents, etc.)
In this sample service, the "contact" resource will closely map to a sample CONTACTS database table.
This is meant to provide the OpenInsight web API developer an easy way to create a web-based CRUD
API that can also be extended as needed. Locking is performed on the resource at the database row
level using the Lock statement, but this is only done just prior to the Write statement since HTTP
is a stateless protocol. While some attempts to wait for the lock to be available could be added,
this is normally discouraged since this could cause the HTTP request to take too long to finish.
All HTTP web services should include the HTTP_SERVICE_SETUP insert. This will provide several useful
variables:
HTTPMethod - The HTTP Method (Verb) submitted by the client (e.g., GET, POST, etc.)
APIURL - The URL for the API entry point (e.g., api.mysite.com/v1).
SelfURL - The URL path representing the current service.
FullEndPointURL - The URL submitted by the client. This can be the same or longer than
the SelfURL.
NextSegment - The URL segment immediately following the SelfURL (if any). This
could contain the name of the next service or it could contain the
Item ID for the current service (aka resource).
CurrentServiceHandler - The name of this stored procedure.
Parameters :
RemainingURL [in] -- The remaining portion of the URL that follows the URL that launched this current
service. This information is used in the HTTP_SERVICE_SETUP insert to populate other
useful variables (see Notes above).
Response [out] -- Response to be sent back to the Controller (HTTP_MCP) or requesting procedure. Web API
services do not rely upon anything being returned in the response. This is what the
various services like SetResponseBody and SetResponseStatus services are for. A response
value is only helpful if the developers want to use it for debug purposes.
History : (Date, Initials, Notes)
04/17/15 dmb Original programmer. - [SRPFW-96]
03/09/16 dmb Refactor to use the updated RunHTTPService service. - [SRPFW-112]
07/01/17 dmb Refactor using Enhanced BASIC+ syntax. - [SRPFW-184]
07/07/17 dmb Add support for PUT and PATCH so this routine can serve as a more complete CRUD example.
- [SRPFW-187]
07/08/17 dmb Remove checks for query parameters from the main router and make this a function of the
GET method/URL handler. - [SRPFW-187]
07/19/18 dmb Fix minor typo in the NextSegment variable in the GetItem method. - [SRPFW-248]
***********************************************************************************************************************/
#pragma precomp SRP_PreCompiler
$insert APP_INSERTS
$insert HTTP_SERVICE_SETUP
$insert HTTP_INSERTS
// In the comments related to URL examples, words surrounded by "{" and "}" represent the names of values that
// will appear in the actual URL. Words surrounded by "<" and ">" represent variables that contain values relevant to
// the actual URL. See the Notes above for a list of the most important variables.
//
// For instance, <APIURL>/contacts/{KeyID} could look like https://api.mysite.com/v1/contacts/1000, assuming <APIURL>
// resolves to "https://api.mysite.com/v1" and {KeyID} resolves to "1000".
//
// The type of request being made needs to be determined based on the URL content. There are only a few possibilities
// that this API will support:
//
// All Resources = <APIURL>/contacts
// Specific Resource = <APIURL>/contacts/{KeyID}
// Specific Resource Property = <APIURL>/contacts/{KeyID}/{property}
//
// Also, any URL can end with query parameters like this:
//
// Resource Query = <APIURL>/contacts?{property}={value}
//
// The request will go to the same handler as if the query parameters were missing but that handler itself will
// determine if the query parameters will be used or ignored.
// Assume the current HTTP method is valid until proven otherwise.
ValidMethod = True$
// Assume the current web service is valid until provent otherwise.
ValidService = True$
// Assume no HTTP methods are valid until proven otherwise.
AllowedMethods = ''
// A list of all services able to be called from this URL.
AllowedServices = 'picture'
// Handle the HTTP request as needed.
Begin Case
Case RemainingURL _EQC ''
// This means the URL ends with /contacts, which means this is the end point. The HTTP methods roughly function
// as follows:
//
// POST - Creates a new resource. Assumes the server will generate the Key ID, which will be returned in the
// HTTP response.
// GET - The client is requesting a collection of all contacts.
AllowedMethods = 'POST,GET,OPTIONS'
Locate HTTPMethod in AllowedMethods using ',' setting MethodPos then
On MethodPos GoSub Post, Get, Options
end else
ValidMethod = False$
end
Case Count(RemainingURL, '/') EQ 0
// This means the URL ends with /contacts/{KeyID}. {KeyID} is also known as the resource ID. When a resource is
// closely mapped to a database row (as is the case with this Contacts API), this is where the basic CRUD
// functionality will be added. The HTTP methods roughly function as follows:
//
// PUT - Creates* a resource using the Key ID contained in the URL. This is equivalent to the Write
// statement in BASIC+.
// GET - Reads the resource referenced by the Key ID contained in the URL. This is equivalent to the Read
// statement in BASIC+.
// PUT - Updates* the resource referenced by the Key ID contained in the URL. This is the exact same
// feature defined above. This should make sense since the Write statement in BASIC+ is used to
// create and update database rows. Note, the PUT method assumes the entire resource is within the
// request body, not just the changes. See the PATCH method below.
// DELETE - Deletes the source referenced by the Key ID contained in the URL. This is equivalent to the Delete
// statement in BASIC+.
//
// * Many people use the POST method for creating (and updating) a resource. However, per the HTTP
// specification, POST is to be used when creating a new resource that does not yet have a resource ID
// (i.e., Key ID). The server determines the Key ID and this is returned to the client for future use.
//
// PATCH - Updates specific properties (e.g., data columns) of the resource referenced by the Key ID
// contained in the URL. This is similar in concept to the WriteV statement in BASIC+, although
// multiple changes in the resource can be updated with one PATCH method.
AllowedMethods = 'PUT,GET,DELETE,PATCH,OPTIONS'
Locate HTTPMethod in AllowedMethods using ',' setting MethodPos then
On MethodPos GoSub PutItem, GetItem, DeleteItem, PatchItem, OptionsItem
end else
ValidMethod = False$
end
Case Count(RemainingURL, '/') GE 1
// This means the URL ends with /contacts/{KeyID}/{property}. A property can be any specific data that is
// associated with the resource. It could be a column value, an image, a PDF document, etc. In this case, the
// only property supported by this web API is the contact's "picture". The developer can put add code in this
// service to update the picture or the developer can create another HTTP service to handle this. Since a
// "picture" service might be useful as a property for other types of resources, a call to a dedicated "picture"
// HTTP service will be made "as is" so it can handle the request. Calling another HTTP service is similar to
// the way one MFS calls another MFS by modifying the FS list. In this case, the NextSegment and RemainingURL
// variables will need to be modified.
Property = FullEndPointURL[-1, 'B/']
Locate Property in AllowedServices using ',' setting ServicePos then
NextSegment = Property ; // This allows the RunHTTPService to call HTTP_PICTURE_SERVICES.
RemainingURL = '' ; // This variable won't be used in the HTTP_PICTURE_SERVICES code, but to keep the
; // variables well formed, this should be cleared.
HTTP_Services('RunHTTPService', NextSegment, RemainingURL)
end else
ValidService = False$
end
Case Otherwise$
ValidService = False$
End Case
// Resolve any invalid conditions with the HTTP request.
Begin Case
Case Not(ValidService)
HTTP_Services('SetResponseStatus', 404, NextSegment : ' is not a valid service request within the ' : CurrentServiceHandler : ' module.')
Case Not(ValidMethod)
HTTP_Services('SetResponseStatus', 405, HTTPMethod : ' is not valid for this service.')
GoSub SetAllowedMethods
End Case
Return Response OR ''
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Service Parameter Options
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Options BOOLEAN = True$, False$
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Web Services
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//----------------------------------------------------------------------------------------------------------------------
// Post
//
// Attempts to create a new resource. Creating a new which is a database row follows these guidelines:
//
// - Any unexpected system errors will return a 500 status code (Internal Server Error).
// - If no errors occur then a 201 (Created) status code is returned. The Content-Location response header will be
// set to the value of the URL that will allow the client to GET the newly created resource.
// - If there is an error locking the resource then a 423 status code (Locked) is returned.
//----------------------------------------------------------------------------------------------------------------------
Post:
HTTP_Resource_Services('PostDatabaseItem', 'CONTACTS', SelfURL)
return
//----------------------------------------------------------------------------------------------------------------------
// Get
//
// Returns a collection of resources.
//
// The easiest way to return a list of resources that are mapped to a database table is to use the GetDatabaseItems
// service. This is being done in the code below. This URL also supports the passing in of query parameters, which in
// this case will be used to return those items that match the property/value query.
//
// A property can be any specific data that is associated with the resource. It could be a column value, an image, a PDF
// document, etc. In this case, only properties that match the name of database columns in the CONTACTS table will be
// supported by this web API. Note, developers can limit the properties (aka columns) to those that are indexed in order
// to avoid having a request take too long.
//----------------------------------------------------------------------------------------------------------------------
Get:
HAL = '' ; // Initialize the response.
If HTTP_Services('GetHTTPGetString') NE '' then
// This means the URL ends with /contacts?{property}={value}. The client is searching for one or more contacts
// that match the query parameters. This is equivalent to doing a filtered RLIST search.
// Get the query string passed into the URL.
GetString = HTTP_Services('GetHTTPGetString')
// Get the name of the property being queried.
Property = GetString[1, 'F=']
// Get the value being searched for.
Value = HTTP_Services('GetQueryField', Property)
// Get the database columns for the table.
ColumnNames = HTTP_Resource_Services('GetColumnNames', 'CONTACTS')
ColumnName = Property
Convert @Lower_Case to @Upper_Case in ColumnName
// Verify the property matches a valid column in the table.
Locate ColumnName in ColumnNames using @FM setting fPos then
// Use the GetDatabaseItems service to perform the search and prepare the HAL+JSON response. If a more complex
// or optimized solution is needed, then replace the following with custom code.
Filter = 'SELECT CONTACTS WITH ' : ColumnName : ' CONTAINING ' : Quote(Value)
// The GetDatabaseItems service will return all database column values unless otherwise specified. Since a query
// search might generated several results, it is sometimes best to pass in just those columns that are important
// for the query result.
ColumnNames = 'first_name' : @FM : 'last_name' : @FM : 'email'
Locate ColumnName in ColumnNames using @FM setting fPos else
// Make sure the property being searched is included in the columns being returned.
ColumnNames := @FM : Property
end
HAL = HTTP_Resource_Services('GetDatabaseItems', Filter, 'CONTACTS', SelfURL, ColumnNames)
end else
// This is not a valid property, which means the URL does not resolve. Set a 404 error. Add a description if
// desired.
HTTP_Services('SetResponseStatus', 404)
end
end else
// This means the URL ends with /contacts. The client is requesting all resources available at this URL.
// This is equivalent to performing an unfiltered SELECT statement. The ColumnNames argument for the
// GetDatabaseItems service specifies which values should be represented in the JSON response.
Filter = ''
ColumnNames = 'first_name' : @FM : 'last_name' : @FM : 'email'
HAL = HTTP_Resource_Services('GetDatabaseItems', Filter, 'CONTACTS', SelfURL, ColumnNames)
end
Response = HAL
return
//----------------------------------------------------------------------------------------------------------------------
// Options
//
// Sets the appropriate response header fields for an OPTIONS request.
//----------------------------------------------------------------------------------------------------------------------
Options:
GoSub SetCommonOptionResponseHeaders
return
//----------------------------------------------------------------------------------------------------------------------
// PutItem
//
// Attempts to update the resource. If the resource does not already exist then a new one will be created. Updating a
// resource which is a database row follows these guidelines:
//
// - Any unexpected system errors will return a 500 status code (Internal Server Error).
// - If no errors occur then a 200 (OK) status code is returned if the resource previously existed. Otherwise,
// a 201 (Created) status code is returned and the Content-Location response header will be set to the value of the
// URL that will allow the client to GET a newly created resource.
// - If there is an error locking the resource then a 423 status code (Locked) is returned.
//----------------------------------------------------------------------------------------------------------------------
PutItem:
KeyID = NextSegment
HTTP_Resource_Services('PutDatabaseItem', 'CONTACTS', SelfURL : '/' : KeyID, KeyID)
return
//----------------------------------------------------------------------------------------------------------------------
// GetItem
//
// Returns the specific resource.
//
// The easiest way to return a resource that is mapped to a database row is to use the GetDatabaseItem service. This
// is being done in the code below. However, to demonstrate how then basic functionality can be extended, there is
// additional code below that will show how to add the Contact resource's image URL to the JSON response.
//----------------------------------------------------------------------------------------------------------------------
GetItem:
KeyID = NextSegment
// Calling this service alone would be sufficient to return a HAL+JSON representation of the specified contact.
HAL = HTTP_Resource_Services('GetDatabaseItem', 'CONTACTS', SelfURL : '/' : KeyID, KeyID)
// Since the Contact resource can also have an image, the following code will generate a valid URL for this image
// in case the client wants to retrieve it. The URL will then be added to the HAL+JSON response so this comes
// back as a single representation of the resource.
If HAL NE '' then
// Make the JSON content an object so the SRP_JSON API can work with it.
ParseResponse = SRP_JSON(HALRootObj, 'PARSE', HAL)
If ParseResponse EQ '' then
// The CONTACTS table has a PICTURE data column. This stores the physical path to the image, but this is
// not useful to the HTTP client. Create a URL that will allow the client to retrieve the image.
PictureValue = SRP_JSON(HALRootObj, 'GETVALUE', 'picture', '')
If PictureValue NE '' then
If SRP_JSON(PictureObj, 'NEW', 'OBJECT') then
// Create the URL and add it to the JSON object.
ImageURL = SelfURL : '/' : KeyID : '/picture'
SRP_JSON(PictureObj, 'SETVALUE', 'href', ImageURL)
SRP_JSON(PictureObj, 'SETVALUE', 'name', 'picture-' : KeyID)
SRP_JSON(HALRootObj, 'SET', 'picture', PictureObj)
HAL = SRP_JSON(HALRootObj, 'STRINGIFY', 'STYLED')
// Set the HTTP response body with the final HAL+JSON results.
HTTP_Services('SetResponseBody', HAL, False$, 'application/hal+json')
SRP_JSON(PictureObj, 'RELEASE')
end
end
SRP_JSON(HALRootObj, 'RELEASE')
end
end
Response = HAL
return
//----------------------------------------------------------------------------------------------------------------------
// DeleteItem
//
// Attempts to delete the resource. Deleting a resource which is a database row follows these guidelines:
//
// - Any unexpected system errors will return a 500 status code (Internal Server Error).
// - If no errors occur then a 204 (No Content) status code is returned.
// - If the resource was already deleted then a 204 (No Content) status code is returned.
// - If there is an error locking the resource then a 423 status code (Locked) is returned.
//----------------------------------------------------------------------------------------------------------------------
DeleteItem:
KeyID = NextSegment
HTTP_Resource_Services('DeleteDatabaseItem', 'CONTACTS', KeyID)
return
//----------------------------------------------------------------------------------------------------------------------
// PatchItem
//
// Attempts to update the resource. Updating a resource which is a database row follows these guidelines:
//
// - Any unexpected system errors will return a 500 status code (Internal Server Error).
// - If no errors occur then a 200 (OK) status code is returned.
// - If the resource is new then a 404 (Not Found) status code is returned. PATCH only works with existing resources.
// - Only those properties (columns) which are passed in will get updated.
// - If there is an error locking the resource then a 423 status code (Locked) is returned.
//----------------------------------------------------------------------------------------------------------------------
PatchItem:
KeyID = NextSegment
HTTP_Resource_Services('PatchDatabaseItem', 'CONTACTS', SelfURL : '/' : KeyID, KeyID)
return
//----------------------------------------------------------------------------------------------------------------------
// OptionsItem
//
// Sets the appropriate response header fields for an OPTIONS request.
//----------------------------------------------------------------------------------------------------------------------
OptionsItem:
GoSub SetCommonOptionResponseHeaders
return
//----------------------------------------------------------------------------------------------------------------------
// PutItemProperty
//
// Attempts to update the property of a specific resource.
//
// A property can be any specific data that is associated with the resource. It could be a column value, an image, a PDF
// document, etc. In this case, the only property supported by this web API is the contact's "picture". The developer
// can put add code here to update the picture or the developer can create another HTTP service to handle this. Since a
// "picture" service might be useful as a property for other types of resources, a call to a dedicated "picture" HTTP
// service will be made.
//
// Calling another HTTP service is similar to the way one MFS calls another MFS by modifying the FS list. In this case,
// the NextSegment and RemainingURL variables will need to be modified. At this point in the stack the following
// API variables look like this:
//
// HTTPMethod : PUT
// SelfURL : <APIURL>/contacts
// NextSegment : {KeyID}
// FullEndPointURL : <APIURL>/contacts/{KeyID}/{property}
//
// The code will need to determine if a supported property has been passed in. If so, then the next HTTP service will
// need to be called with the appropriate modifications to the variables.
//----------------------------------------------------------------------------------------------------------------------
PutItemProperty:
// Get the name of the property by looking at the last segment in the FullEndPointURL variable. An assumption is
// being made that there are no other segments in the URL that follow the property name.
Property = FullEndPointURL[-1, 'B/']
Locate Property in AllowedServices using ',' setting ServicePos then
// A supported property has been passed in the URL. Modify the NextSegment and RemainingURL variables so the
// next HTTP service can be called correctly.
NextSegment = Property ; // This allows the RunHTTPService to call HTTP_PICTURE_SERVICES.
RemainingURL = '' ; // This variable won't be used in the HTTP_PICTURE_SERVICES code, but to keep the
; // variables well formed, this should be cleared.
HTTP_Services('RunHTTPService', NextSegment, RemainingURL)
end else
// The URL contains an unsupported property. Return a 404 error.
HTTP_Services('SetResponseStatus', 404, Property : ' is not a valid service request within the ' : CurrentServiceHandler : ' module.')
end
return
//----------------------------------------------------------------------------------------------------------------------
// GetItemProperty
//
// Returns the property of a specific resource.
//
// A property can be any specific data that is associated with the resource. It could be a column value, an image, a PDF
// document, etc. In this case, the only property supported by this web API is the contact's "picture". The developer
// can put add code here to return the picture or the developer can create another HTTP service to handle this. Since a
// "picture" service might be useful as a property for other types of resources, a call to a dedicated "picture" HTTP
// service will be made.
//
// Calling another HTTP service is similar to the way one MFS calls another MFS by modifying the FS list. In this case,
// the NextSegment and RemainingURL variables will need to be modified. At this point in the stack the following
// API variables look like this:
//
// HTTPMethod : GET
// SelfURL : <APIURL>/contacts
// NextSegment : {KeyID}
// FullEndPointURL : <APIURL>/contacts/{KeyID}/{property}
//
// The code will need to determine if a supported property has been passed in. If so, then the next HTTP service will
// need to be called with the appropriate modifications to the variables.
//----------------------------------------------------------------------------------------------------------------------
GetItemProperty:
// Get the name of the property by looking at the last segment in the FullEndPointURL variable. An assumption is
// being made that there are no other segments in the URL that follow the property name.
Property = FullEndPointURL[-1, 'B/']
If Property _EQC 'picture' then
// A supported property has been passed in the URL. Modify the NextSegment and RemainingURL variables so the
// next HTTP service can be called correctly.
NextSegment = Property ; // This allows the RunHTTPService to call HTTP_PICTURE_SERVICES.
RemainingURL = '' ; // This variable won't be used in the HTTP_PICTURE_SERVICES code, but to keep the
; // variables well formed, this should be cleared.
HTTP_Services('RunHTTPService', NextSegment, RemainingURL)
end else
// The URL contains an unsupported property. Return a 404 error.
HTTP_Services('SetResponseStatus', 404, Property : ' is not a valid service request within the ' : CurrentServiceHandler : ' module.')
end
return
//----------------------------------------------------------------------------------------------------------------------
// OptionsItemProperty
//
// Sets the appropriate response header fields for an OPTIONS request.
//----------------------------------------------------------------------------------------------------------------------
OptionsItemProperty:
GoSub SetCommonOptionResponseHeaders
return
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Internal GoSubs
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//----------------------------------------------------------------------------------------------------------------------
// SetCommonOptionResponseHeaders
//
// Sets the response headers that will be common for all OPTIONS methods.
//----------------------------------------------------------------------------------------------------------------------
SetCommonOptionResponseHeaders:
HTTP_Services('SetResponseHeaderField', 'Access-Control-Allow-Headers', 'authorization', True$)
HTTP_Services('SetResponseHeaderField', 'Access-Control-Allow-Headers', 'x-authorization', True$)
HTTP_Services('SetResponseHeaderField', 'Access-Control-Max-Age', 1728000)
GoSub SetAllowedMethods
return
//----------------------------------------------------------------------------------------------------------------------
// SetAllowedMethods
//
// Sets the Allow response header field as appropriate for the requested URL.
//----------------------------------------------------------------------------------------------------------------------
SetAllowedMethods:
If AllowedMethods NE '' then
For Each Method in AllowedMethods using ','
HTTP_Services('SetResponseHeaderField', 'Allow', Method, True$)
Next Method
end
return