Creating Custom Connection Strings

Description

Custom drivers can be created using Xbasic, enabling you to create a connection to anything.

Overview

The Connection type defines what type of data source an AlphaDAO connection is communicating with. Alpha Anywhere ships with a variety of pre-built connections to popular database systems as well as non-database systems, such as MongoDB, Google Sheets, Sharepoint, and others. (See Connecting to SQL Databases to learn more about pre-built connections supported in Alpha Anywhere.)

The Custom Connection type can be used to build an Xbasic drive to connect to anything. This connection type has been introduced to enable you to create your own custom connections that can not only connect to data systems that Alpha Anywhere may not have a pre-built connection type, but to also extend an AlphaDAO connection to perform tasks, such as Auditing CRUD activity or building Multi-tenant applications.

Defining a Driver

A custom driver is defined as an Xbasic class that implements the following methods:

Method
Description
get_metadata as c()

Defines any properties available for configuring the connection. Properties are shown in the SQL connection string dialog.

open as L(cn as SQL::Connection)

Called when the connection string is opened (i.e. SQL::Connection Open)

query as L(statement as c, args as SQL::Arguments, er as TableQuery::ExecuteResult)

Called whenever a query is executed on the connection.

ListTables as C(includeTypes as L)

Returns a list of tables for the connection.

GetTableInfo as P(tn as SQL::TableName)

Returns table information for the specified table.

GetLastError as C()

Returns the last error.

Close as V(cn as SQL::Connection)

Called when the connection is closed (i.e. SQL::Connection Close)

ValidateQuery as L(statement as C, er as TableQuery::ExecuteResult)

(Optional) Returns whether or not the query submitted to the connection is valid.

Each method is described in more detail below.

  • get_metadata() 

    The get_metadata() method is required and is called when the SQL connection string dialog for the connection is opened. It returns a JSON object of properties that can be used to configure the custom connection. The JSON object uses the following structure:

    {
        "properties" : {
            "property_name1" : { 
                "description" : "A description of property 1, which is read only",
                "label" : "Property 1"
                "readonly" : true 
            },
            "property_name2" : {
                "description" : "A description of property 2, which is not read only"
                "label" : "Property 2"
            }
        }
    }

    Each entry in the JSON properties object can define the following properties:

    Property
    Description
    description

    The help text shown when the property is selected. It is strongly recommended to define this property.

    label

    The property name. If not specified, the name of the object is used.

    readonly

    Makes the property "read only" in the property grid.

    A property is commonly set to "read only" if a dialog (either a built-in Alpha Anywhere dialog or one created using Xdialog) is used to configure it. For example, the Connection string property for the Audit-trail Driver is populated by clicking the smart field button to select a connection string from a dialog.

    An onpopup method must be defined for a property that is set with a dialog.

    The example below is the get_metadata method from the custom Multi-tenant Driver, which defines several properties that use the .

    function get_metadata as C ()
        get_metadata = <<%json%
    {
        "properties" : {
            "ConnectionString" : {
                "description" : "Specify the connection string that points to your database.",
                "label" : "Connection string",
                "readonly" : true
            },
            "TenantIDFieldName" : {
                "description" : "Specify the fieldname of the field that contains the tenant Id. All of the tables in your database that are multi-tenant should include this field.",
                "label" : "Tenant ID field name"
            },
            
            "TenantIDFieldType" : {
                "description" : "Specify the field type for the tenant ID field. The field can be character or numeric.",
                "label" : "Tenant ID field type",
                "readonly" : true
            },
            "SessionVariableName" : {
                "description": "Specify the name of the session variable where the current tenant ID value is stored.",
                "label" : "Session variable name"
            }
        }
    }
    %json%
    end function
    images/customConnectionProperties.png
  • If your custom connection has no properties, return an empty JSON object:

    function get_metadata as C ()
        get_metadata = "{}"
    end function
    • The onpopup Method 

      For more complex settings (or settings that require validation), you can define an onpopup method that is called to set the value for the property. The onpopup method is defined as follows:

      property_onpopup as C (value as C)

      Where "property" is the name assigned to the JSON object that contains the property definition for the property. For example, consider the following get_metadata() method:

      function get_metadata as C ()
          get_metadata = <<%json%
      {
          "properties" : {
              "ConnectionString" : {
                  "description" : "Specify the connection string that points to your database.",
                  "label" : "Connection string",
                  "readonly" : true
              }
      }
      %json%
      end function

      The corresponding onpopup that defines the dialog ot display to set the "ConnectionString" property is shown below:

      function ConnectionString_onpopup as C (value as C)
          dim p as p 
          p = a5_DefineConnectionString(value,"alphadao",.f.)
          if p.lastbutton = "OK" then 
              connectionstring_onpopup = p.connstring 
          else
              connectionstring_onpopup = value
          end if 
      end function

      The onpopup method must return the value for the property.

      If an onpopup method has been defined for a property, a smart field button will be shown in the SQL connection string dialog. Clicking the smart field button calls the onpopup method.

      images/customConnectionOnPopup.png
    • The onpopup method only needs to be defined for properties that are complex and require a dialog to set them.

  • open() 

    The open method is required and is called when the connection is opened. The following parameters are passed to the method:

    Parameter
    Description
    _cn as SQL::Connection

    A SQL::Connection object used to return an error in the event the open method fails.

    The method should return a .t. or .f. value. Returning .t. indicates that the connection was successfully opened. Returning .f. indicates an error occurred.

    If an error occurs, you can provide additional details about the error using the FromText() method for the SQL::Connection's CallResult object.

    For example:

    function open as L (_cn as SQL::Connection)
        if connectionString = "" then
            _cn.callresult.FromText("No audited Connection specified")
            open = .f.
            exit function
        end if
    
        ' cn is a private SQL::Connection property for the custom Audit class
        open = cn.open(connectionString)
    
        if (open == .f.) then
            _cn.callresult.FromText("Could not open connection to database: " + cn.callResult.text)
            exit function
        end if
    
    end function
  • query() 

    The query method is required and is called when a query is made against the SQL connection. E.g. the SQL::Connection execute() method is called. The following parameters are passed to the method:

    Parameter
    Description
    statement as c

    The SQL query.

    args as SQL::Arguments

    A SQL::Arguments object that contains any arguments used in the statement.

    er as TableQuery::ExecuteResult

    A TableQuery::ExecuteResult object used to return the results of the query. This includes any SQL::ResultSets, error messages, and last inserted identity information.

    The method should return a .t. or .f. value. Returning .t. indicates the query was successful. Returning .f. indicates an error occurred.

    If an error occurs, you need to generate a SQL::CallResult object that contains information about the error. This object can be built manually or created using a variety of methods depending on how your query operation works. For example, the Mock Driver creates and uses a helper::JSONsql::Process object to perform queries on the People and Places table, which are stored internally as JSON objects. The GenerateError method can be used on the helper::JSONsql::Process object to create the SQL::CallResult and pass it back through the TableQuery::ExecuteResult object passed to the query method.

    '...
       dim jsp as helper::JSONsql::Process
        if jsp.ParseStatement(statement,args,self.schema) then
            if jsp.action = "select" then
                dim json as c
                if jsp.tablename = "People"
                    json = self.People_json
                else if jsp.tablename = "places"
                    json = self.places_json
                else
                    er.callresult = jsp.GenerateError("Table not found")
                    exit function
                end if
    '...

    The query method handles CRUD actions against the custom data source. The statement passed to the method will contain the action, which can be one of the following:

    Action
    Description
    select

    Fetch one or more records from your custom data source that match a selection criteria. At a minimum, your query method should support the select action.

    You must set the resultset or callresult of the TableQuery::ExecuteResult object for the query method.

    insert

    Add a record to your custom data source.

    You should set the hasLastInsertedIdentity and LastInsertedIdentity property of the TableQuery::ExecuteResult object if the insert succeeds. Otherwise, set callresult for the TableQuery::ExecuteResult object if the insert fails.

    update

    Update a record in your custom data source with matching primary key.

    Set callresult for the TableQuery::ExecuteResult object if the update fails.

    delete

    Delete a record from your custom data source with matching primary key.

    Set callresult for the TableQuery::ExecuteResult object if the delete fails.

    See Portable SQL to learn more about Portable SQL.

    The example below demonstrates a custom driver that implements select, insert, update, and delete against a JSON data source. This example is part of the Mock Driver example.

    function query as L (statement as C, args as SQL::Arguments, er as TableQuery::ExecuteResult)
        dim jsp as helper::JSONsql::Process
        query = .f. ' default return value to false, indicating failure
    
        if jsp.ParseStatement(statement,args,self.schema) then
    
            if jsp.action = "select" then
    
                dim json as c
                if jsp.tablename = "People"
                    json = self.People_json
                else if jsp.tablename = "places"
                    json = self.places_json
                else
                    ' Error; Generate an error
                    er.callresult = jsp.GenerateError("Table not found")
                    exit function
                end if
                dim rs as sql::ResultSet
                if jsp.JsonToResultSet(rs,json) then
                    ' Success; return the query results as a SQL::ResultSet
                    er.resultset = rs
                    query = .t.
                else
                    ' Error; return the callResult
                    er.callresult = jsp.callresult
                end if
    
            else if jsp.action = "insert"  then
    
                if jsp.tablename = "People"
                    dim name as c = jsp.GetColumn("name","")
                    if name <> "" then
                        People = People+","+name
                        People_json = extension::json::JsonFromCSV("name"+crlf()+comma_to_crlf(People))
    
                        ' Success; return the last inserted identity for people
                        query = .t.
                        er.hasLastInsertedIdentity = .t.
                        er.LastInsertedIdentity = name
                    else
                        ' Error; Generate an error
                        er.callresult = jsp.GenerateError("Name cannot be blank")
                        exit function
                    end if
                else if jsp.tablename = "places"
                    dim city as c = jsp.GetColumn("city","")
                    dim state as c = jsp.GetColumn("state","")
                    if (city+state) <> "" then
                        places = places+","+city+";"+state
                        places_json = extension::json::JsonFromCSV("city,state"+crlf()+strtran(comma_to_crlf(places),";",","))
    
    
                        ' Success; return the last inserted identity for places
                        query = .t.
                        er.hasLastInsertedIdentity = city+";"+state
                        er.LastInsertedIdentity = name
                    else
                        ' Error; Generate an error
                        er.callresult = jsp.GenerateError("City and State are empty")
                        exit function
                    end if
                else
                    ' Error; Generate an error
                    er.callresult = jsp.GenerateError("Table not found")
                    exit function
                end if
    
            else if jsp.action = "update"  then
    
                if jsp.tablename = "People"
                    dim name as c = jsp.GetColumnExact("name","")
                    dim newname as c = jsp.GetColumn("name","")
                    if newname = "" then
                        er.callresult = jsp.GenerateError("Name cannot be blank")
                    else if (","+name+",") $ (","+People+",") then
                        People = rtrim(ltrim(strtran((","+People+","),(","+name+","),(","+newname+",")),","),",")
                        People_json = extension::json::JsonFromCSV("name"+crlf()+comma_to_crlf(People))
    
                        ' Success!
                        query = .t.
                    else
                        ' Error; Generate an error
                        er.callresult = jsp.GenerateError("Name not found")
                        exit function
                    end if
                else if jsp.tablename = "places"
                    dim city as c = jsp.GetColumnExact("city","")
                    dim state as c = jsp.GetColumnExact("state","")
                    dim newcity as c = jsp.GetColumn("city",city)
                    dim newstate as c = jsp.GetColumn("state",state)
                    if (newcity+newstate) <> "" then
                        if (","+city+";"+state+",") $ (","+places+",") then
                            places = rtrim(ltrim(strtran((","+places+","),(","++city+";"+state+","),(","+newcity+";"+newstate+",")),","),",")
                            places_json = extension::json::JsonFromCSV("city,state"+crlf()+strtran(comma_to_crlf(places),";",","))
    
                            ' Success!
                            query = .t.
                        else
                            ' Error; Generate an error
                            er.callresult = jsp.GenerateError("City and State not found")
                            exit function
                        end if
                    else
                        ' Error; Generate an error
                        er.callresult = jsp.GenerateError("City and State are empty")
                        exit function
                    end if
                else
                    ' Error; Generate an error
                    er.callresult = jsp.GenerateError("Table not found")
                    exit function
                end if
    
            else if jsp.action = "delete"  then
    
                if jsp.tablename = "People"
                    dim name as c = jsp.GetColumnExact("name","")
                    if (","+name+",") $ (","+People+",") then
                        People = rtrim(ltrim(strtran((","+People+","),(","+name+","),","),","),",")
                        People_json = extension::json::JsonFromCSV("name"+crlf()+comma_to_crlf(People))
    
                        ' Success!
                        query = .t.
                    else
                        ' Nothing to delete
                        exit function
                    end if
                else if jsp.tablename = "places"
                    dim city as c = jsp.GetColumnExact("city","")
                    dim state as c = jsp.GetColumnExact("state","")
                       if (","+city+";"+state+",") $ (","+places+",") then
                        places = rtrim(ltrim(strtran((","+places+","),(","+city+";"+state+","),","),","),",")
                            places_json = extension::json::JsonFromCSV("city,state"+crlf()+strtran(comma_to_crlf(places),";",","))
    
                            ' Success!
                            query = .t.
                       else
                            ' Nothing to delete
                            exit function
                       end if
                else
                    ' Error; Generate an error
                    er.callresult = jsp.GenerateError("Table not found")
                    exit function
                end if
    
            else
    
                ' Error; Generate an error
                er.callresult = jsp.GenerateError("Only SELECT is support for this datasource")
    
            end if
    
        else
    
            ' Error; return the callresult
            er.callresult = jsp.callresult
    
        end if
    
    end function
  • ListTables() 

    The ListTables method is required and returns a list of tables for the custom driver. The method is called when the SQL::Connection ListTables() or ListTablesWithTypes() methods are called. The following parameters are passed to ListTables:

    Parameter
    Description
    includeTypes as L

    A .T. or .F. value. If .T., ListTables() should return the list of tables suffixed with (<type>). Otherwise, ListTables should return a list of tables.

    function ListTables as C (includeTypes as L)
        if (includeTypes) then
            ListTables = <<%str%
    People(Table)
    Places(Table)
    %str%
        else
            ListTables = <<%str%
    People
    Places
    %str%
        end if
    
    end function
  • GetTableInfo() 

    The GetTableInfo method is required and returns information about a specific table. The following parameters are passed to GetTableInfo:

    Parameter
    Description
    tn as SQL::TableName

    A SQL::TableName object that specifies the table to return information for.

    The GetTableInfo method should return a SQL::TableInfo object for the specified table.

    function GetTableInfo as P (tn as SQL::TableName)
        ' Look up the table info index for the table name, tn.name:
        dim index as n = schema.TableNumber(tn.name)
    
        ' If the table exists, return the TableInfo object:
        if index > 0
            GetTableInfo = schema.table[index]
        end if
    end function
  • GetLastError() 

    The GetLastError method is required and returns the error message for the last error encountered using your custom connection.

    function GetLastError as C ()
        ' Return the last error for the audited table (Audit Trail Driver)
        GetLastError = cn.CallResult.text
    end function
  • Close() 

    The Close method is required and is called when the connection is being closed. The following parameters are passed to the Close method:

    Parameters
    Description
    _cn as SQL::Connection

    A SQL::Connection object.

    If your custom connection has variables or resources that need to be cleaned up or released when the connection is closed, you can do so in the Close method. For example, the custom Audit Trail driver has a private SQL::Connection object that is a connection for the audited database. When the Audit Trail connection is closed, the connection to the audited database must also be closed:

    function Close as v(_cn as sql::Connection)
        ' Close the connection to the audited database
        cn.close()
    end function
  • ValidateQuery() 

    The ValidateQuery method is an optional method used to validate a SQL statement before it is executed. The following parameters are passed to the method:

    Parameters
    Description
    statement as C

    The SQL statement to validate.

    er as TableQuery::ExecuteResult

    A TableQuery::ExecuteResult object that contains the result of the validation.

    The following example demonstrates the ValidateQuery method for the Multi-tenant Driver example:

    function ValidateQuery as L (statement as C, er as TableQuery::ExecuteResult)
        dim qs as sql::Query
        dim flag as l = qs.Parse(statement)
        if flag then
            cn.PortableSQLEnabled = .t.
            ValidateQuery = qs.Validate(cn)
            if ValidateQuery then
                er.resultset = qs.ResultSet
            end if	
        end if
    end function

Sample Drivers

  • Simple Mock Driver
  • Multi Tenant Driver
  • Audit Trail Driver

Each of these example drivers are described below.

Simple Mock Driver 

The "Simple Mock Driver" is an example driver. It's purpose is to provide a framework from which you can build your own custom driver.

The Simple Mock Driver is an Xbasic class that demonstrates two tables: "People" and "Places". The tables are populated in the settings for the driver.

images/customMockDriver.png
You MUST define the People and Places settings in order to build any applications against the tables. If no values are defined, the tables will not be listed in the SQL Query Builder.

The properties for the Simple Mock Driver are described below:

Property
Description
People

A comma delimited list of names. This list of names will be used to populate the "People" table. E.g. Ada,Ruth,Susan

Places

A comma delimited list of City-State pairs. The City name is separated from the State name using a semicolon ";". E.g. Billings;MT,Sioux City;IA,Cheyenne;WY

Multi-tenant Driver 

images/multitenant4.jpg

The Multi-tenant driver can be used to create SaaS applications. The driver works by applying a tenant filter to all queries made to the data source. This ensures that data is always filtered on the tenant when performing CRUD operations on a data source.

To use the Sample Multi-tenant Driver as-is, your database tables must include the tenant id field in every table. The driver assumes this field uses the same name across the database.

The advantage of using the Sample Multi-tenant Driver over the MultiTenant Connection Type is that the sample driver is fully customizable and can be adapted to the needs of your SaaS application.

When you create a multi-tenant connection using the multi-tenant driver, you must specify the following properties in the connection string definition dialog:

Property
Description
Connection string

This is the connection string that points to your database.

Tenant ID field name

The name of the tenant ID field in each of the multi-tenant tables in your database (e.g. tenantid)

Session variable name

The name of the session variable that will contain the current tenant ID value (this value will typically be set at the time the tenant logs into the application). For example: session.tenantid. Session variables are deleted when a session expires. Therefore you should ensure that the user's login also expires when the session expires.

Tenant ID field type

The data type of the tenant ID field - can either be character or numeric.

You can also create a Multi-tenant connection string using the MultiTenant Connection Type.

Multi-tenant Videos 

How to Create a Multi-tenant Connection String

When you build multi-tenant SaaS applications that use a shared database, each table in the database must have a tenant id field and all of your SQL queries must include the tenant Id. When you use a multi-tenant connection string, the tenant id is automatically injected into all SQL statements before the statement is sent to the database. This makes it easier to build multi-tenant SaaS applications, or to convert an existing application to a multi-tenant application because you do not have to manually adjust all of your SQL statements.

In this video we show how a multi-tenant connection string is defined and then we show the results when a SQL SELECT and INSERT statement are executed.

2018-10-21

Audit Trail Driver 

The Audit Trail Driver automatically updates an audit table when CRUD operations are executed against a data source.

images/customAuditDriver.png

Contrast the Audit-trail driver with the built-in Audit Trail feature which is turned on by going to the Web Project Settings dialog. The built-in feature does not require a special connection string, can automatically create the audit-trail table and is therefore easier to set up. See Audit table (for SQL tables) for more information.

A sample Xbasic class that implements an audit-trail driver can be selected when you define a Custom connection string.

The Audit driver is a "pass-through" driver (i.e. SQL commands are passed through to the base SQL connection) but a new record is added to the audit table for each successfully executed CRUD statement.

When you create an audit driver you must specify these properties in the connection string definition dialog:

Property
Description
ConnectionString

The connection string to the database that contains the data that will be queried (read or updated.)

AuditTableConnectionString

The connection string to the database where the audit table is defined. This can either be the same connection string as the ConnectionString or a separate database.

AuditTableName

The name of the audit table. See Audit Table Structure below for the audit table structure.

OperationTypeFieldName

The name of the field in the audit table where the CRUD type is stored. The CRUD type is one of the following: CREATE, UPDATE, INSERT, or DELETE.

UserIdFieldName

The name of the field in the audit table where the user id of the user making the edit is stored.

DateTimeFieldName

The name of the field in the audit table where the date/time for the time when the edit was made is stored.

TableFieldName

The name of the field in the audit table where the name of the table that was updated is stored.

DataFieldName

The name of the field in the audit table where the data for the CRUD operation is stored.

OldDataFieldName

The name of the field in the audit table where the original data (for UPDATE and DELETE operations) is stored.

whereclausefieldname

The name of the field in the audit table where the WHERE clause for the CRUD statement (UPDATE and DELETE) is stored.

Audit Table Structure 

The audit table must have this structure (the actual field names can be different as the connection string builder allows you to map the actual field names):

operation char(20)
userId char(100)
dateStamp datetime
databaseTableName char(100)
data longtext
olddata longtext
whereclause longtext

Audit-trail Videos 

Audit-trail Driver

This video shows how you can create a custom AlphaDAO connection string to create an audit trail every time a CRUD operation is performed.

2018-10-28