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
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.
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.
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
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.
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.
Audit Trail Driver
The Audit Trail Driver automatically updates an audit table when CRUD operations are executed against a data source.
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.