Personal tools

Database Connector

From OpenLaszlo

Contents

Project Information

Mission Statement

The Laszlo Database Connector is designed to make it as easy as possible for Laszlo developers to use record-based datasets, in particular SQL databases. The goal is to have a common, open protocol for communicating incremental changes to and from record-based data sources.

This spec is inspired by the ActiveRecord pattern, in particular the Ruby on Rails implementation. However, since the communication happens entirely in XML, there should be no dependency in the Database Connector that binds it to any particular technology or server on the back end.

Requirements

(In progress:)

  • The project must specify a protocal for querying, creating, reading and updating datasets but must not be dependent on any specific server-side technology.
  • The project must be secure, by default, against SQL injection attacks. (Preferrably, it will not use SQL at all, except behind the scenes on the server.)
  • The project must support a reasonable security model.

Projects

There are two projects underway that aim to meet the requirements implied by the mission statement.

  1. Laszlo on Rails will integrate Ruby on Rails on the server with OpenLaszlo on the client. This project has already made significant progress in that direction.
  2. The PHP Database Connector project is a PHP server library that will implement the requirements implied in the mission statement. This project has not actually been undertaken yet.

It is hoped that these two projects can share specifications and client-side work. The work that is common to these two projects is the subject of the Database Connector umbrella project.

In addition, RIFE/Laszlo is a ready-to-run source distribution and file structure that allows you to get started with a new OpenLaszlo application with RIFE in less then 2 minutes.

Specifications

The Database Connector could, more accurately, be described as an object broker. In response to a query for data, it returns a graph of value objects represented in XML datasets that conform to a common schema.

As data is changed on the client, the delta's will be stored and sent to the server when appropriate. See the Talk section of this page for some links to existing approaches to this problem.

If one or more errors occur when committing to the server, the client will roll back changes to the dataset to the last successful commit, thereby keeping both data models in sync.

Controller-URL mapping

Server-side controllers are accessed using a consistent URL mapping that ensures compatibility across implementations.

Controllers

Each controller is exposed at a separate path, e.g.

/contact

/address

/category

Calling Methods

Controller methods are called by listing them after the name of a controller, e.g.

/contact/list

Calls the list method in the contact controller.

Default Method Name

If a method name not provided, a default method called 'index' is called on the controller, e.g.

/contact

/contact/index

Both call the index method in /contact with no record id.

Methods are invoked my appending them to the path of a controller, e.g.:

/contact/10,20

Calls the index method in the contact controller with record ids 10 and 20.

Method arguments

Additional arguments can be passed to a controller on the query string, e.g.:

/contact/mymethod?foo=bar

Calls mymethod method on the contact controller, passing it the argument foo with the value bar

Some scaffolding methods allow ordering to be specified by adding the optional order attribute:

/contact/read?order=firstlastdesc

Reads all records from the contact controller, ordered by 'firstlastdesc'

Record IDs

Record IDs are always specified at the end of the path, e.g.

/contact/delete/52

Calls the delete method in the contact controller with record id 52.

/contact/delete/list/10,1,15

Calls the list method in the contact controller with record ids 10, 1 and 5.

ToDo: there is much more to be considered in terms of querying syntax. XPath seems like a good place to look to extend REST.

Some issues with this: Rails uses primary keys (id's) in the URI. So, for example this might actually be myapp.com/contact/1/address/category. The ugly thing about this is that an artificial single-field identifier breaks database normalization rules. For example, an enrollment in a class by a student would be identified by student_id + course_id + course_start_date. Adding a student enrollment id as a single field is unnecessary. OTOH, having the rules of data storage in a relational database dictating the structure of a REST API is inappropriate. So I would say that the contact *object* needs an identifier that can be used for purposes of the URI and that may, or may not, have meaning to the server in terms of mapping the object to its data.

What matters is that if I refer to /customer/[some key value], whether that key value is a single integer, a UUID or a concatenated string forming a composite key, the server can do the necessary update and *the Laszlo app need not have any inkling* of the actual database structure.

Return values

All methods must return valid XML.

Exceptions

Each exception must include an element named after the method called, with any arguments passed in. Exceptions are returned in the following format:

<exception> 
  <methodname argname="value" argname="value".../>    
</exception>

Exceptions can optionally include text and/or subnode values:

<exception> 
  <methodname argname="value" argname="value"...>
    User-visible message
  </methodname>    
</exception>
<exception> 
  <methodname argname="value" argname="value"...>
    User-visible message
    <contact id="2" updated_on="19239823' name="value".../>
    <contact id="1" updated_on="19239825' name="value".../>
    ...
  </methodname>    
</exception>

Method call batching

Several method calls can be combined into single batch message. This is used for doing a series of operations atomically. Batches are sent as XML via HTTP POST to the batch url:

/controllername/batch

Each method call is written as XML in the following format:

<methodname argname="value" argname="value".../>

Method calls are grouped under elements that specify which controller should be used:

<controllername>
    <methodname argname="value" argname="value".../>
    ...
</controllername>

This XML is sent via HTTP POST to the server-side controller. Each method is called in order, and responses (or exceptions) are returned in order. The server then returns XML that echos the call format, e.g.

<controllername>
    <methodname>
        User-visible message
        <result name="value" name="value".../> 
    </methodname/>
    <methodname>
        <exception> 
            <methodname argname="value" argname="value"...>
                User-visible message
                <contact id="2" updated_on="19239823' name="value".../>
                <contact id="1" updated_on="19239825' name="value".../>
                ...
        </exception/>    
    </methodname>    
</controllername>

Scaffolding

Each controller provides scaffolding methods to create, read, update and delete records in the underlying model. Each method provides two forms - a url for individual operations, and an XML tag for batch operations

Reading Records

Reads zero or more records. If no id is specified, all records are retrieved.

URL syntax

/controllername/read

Tag syntax

<read .../>

Returns

The response includes record elements with any related records listed as subnodes. Each record element is named after its controller and includes all fields as attributes. For example:

/contact/read

Calls the read method in the contact controller, which retrieves all contacts by default:

<records controller="contact" count="100" totalcount="10000" time="912991238">
  <contact first_name="Max" last_name="Foo" id="1">
    <address street="123 main st." city="San francisco" state="ca" zip="94114" id="1" updated_on="8120938"/>
    <address street="123 foo st." city="San francisco" state="ca" zip="94114" id="2" updated_on="28713098123"/>
    <company name="Foo Industries" id="1" updated_on="12739821">
      <address street="123 Across 110th st." city="San francisco" state="ca" zip="94114" id="3" updated_on="12098309182"/>
    </company>
    <category name="Friend" id="2" updated_on="128829339"/>
    <category name="Personal" id="5" updated_on="92813882103"/>
    <category name="Work" id="17" updated_on="92139823182"/>
  </contact>
  ...
</records>


Record elements must include id and updated_on fields:

Name Description
id id of the record
updated_on server epoch time when the record was last updated


Each controller returns certain attributes in the top-level <records/> element:

Name Description
controller name of the controller called
count number of records retrieved
totalcount total number of records in recordset
time current server epoch time
Exceptions

No records found:

<exception> 
  <read>       
    User-visible error message...
  </read>       
</exception>
Reading records by id

One or more ids can be specified, e.g.

/contact/read/1

Retrieves record id 1

/contact/read/1,3

Retrieves record ids 1 and 3

Exceptions

No records found:

<exception> 
  <read id="1,3...">       
    User-visible error message...
  </read>       
</exception>
Ordering records

Record ordering can be specified with the 'order' query string attribute, e.g.

/contact?order=firstlastdesc

returns contacts ordered by firstname, then lastname in descending order. The controller is responsible for determining how record ordering is determined.

Exceptions

Sorting by order failed:

<exception> 
  <read order="firstlastdesc">       
    User-visible error message
  </read>       
</exception>

Paging records

The page method allows ordered records to be retrieved by sort offset. If no order is specified, a default sort order is used.

URL syntax

/contact/page/1

Retrieves the first record in the default sort order

/contact/page/1,3?order=firstlastdesc

Retrieves the first and third record in sort order

/contact/page/1-10?order=firstlastdesc

Retrieves the first through the tenth records in sort order

/contact/page/-10?order=firstlastdesc

Retrieves the first through the tenth records in sort order

/contact/page/-10,14-17?order=firstlastdesc

Retrieves the first through the tenth and fourteenth through seventeenth records in sort order

/contact/page/11-?order=firstlastdesc

Retrieves the eleventh record onward in sort order

Tag syntax

<page id="-10,14-17" order="firstlastdesc"/>

Returns

XML in the same format as /read

Exceptions

Paging out of bounds:

<exception> 
  <page order="14-17">       
    User-visible error message
  </page>       
</exception>

Creating Records

Creates a new record.

Attributes consist of zero or more name/value pairs for the record fields to be created.

URL syntax

/controllername/create?name=value...

Tag syntax

<create name="value".../>

Returns

The id and the server epoch time the record was created, e.g.

<created id="1" created_on="128829339"/>

Exceptions

Record could not be created:

<exception> 
  <create name="value"...>       
    User-visible error message
  </create>       
</exception>

Updating Records

Updates an existing record by id.

Attributes consist of a record id and one or more name/value pairs for the record fields to be updated.

Includes the server-side epoch update time when the record was last sent to the client.

Throws an exception if the server record was updated since the last retrieval.

URL syntax

/controllername/update/id?updated_on=value&name=value...

Tag syntax

<update id="value" updated_on="value" name="value".../>

Returns

The id and server epoch time for the record updated, e.g.

<updated id="1" updated_on="128829339"/>

Exceptions

Record could not be updated:

<exception> 
  <update id="value" name="value"...>       
    User-visible error message
  </update>       
</exception>

Deleting records

Removes records from the controller.

Attributes consist of one or more record ids to be deleted

Includes the server-side epoch update time when the record was last sent to the client.

Throws an exception if the server record was updated since the last retrieval.

URL syntax

/controllername/delete/id?updated_on=value

Tag syntax

<delete id="id" updated_on="value"/>

<delete id="id,id,id,id..." updated_on="value,value,value,value¿"/>

Returns

The ids of the record(s) deleted

<deleted id="id">

<deleted id="id,id,id">

Exceptions

Record(s) could not be deleted:

<exception> 
  <delete id="id,id,id,id...">       
    User-visible error message
  </delete>       
</exception>

Committing Records

Attempts to commit all changes.

Throws an exception if the commit fails.

URL syntax

Since commits are meant to be atomic, only the tag syntax is specified.

Tag syntax

One or more commands can be included in a commit message, e.g.:

<commit>

   <delete id="id" updated_on="value"/>
   <update id="value" updated_on="value" name="value".../>
   ...

</commit>

Returns

If successful, sends commit message to client

<committed/>

<committed>
    <deleted id="id">
    <updated id="1" created_on="128829339"/>
</committed>

If the commit fails, the server rolls back to the last commit - partial commits don't succeed.

Exceptions

Send failure/rollback message to client with new record values for conflict resolution

<exception> 
  <rollback>       
    <contact id="2" updated_on="19239823' name="value".../>
  </rollback>       
</exception>

Retreiving schemas

Each controller can be asked for a schema which includes a list of fields, field types and relationship declarations, e.g.

/schema

Returns the schemas for all controllers. By default, this simple format is returned:

<controller>
  <contact>
    <field name="first_name" type="varchar"/>
    <field name="last_name" type="text"/>
    <field name="email" type="int"/>
    <field name="updated_on" type="datetime"/>
    ...
    <metadata name="has_one">
      <value>company</value>
      <attribute name="class_name">companies</attribute>
    </metadata>
    <metadata name="has_many">
      <value>address</value>
    </metadata>
    <metadata name="has_and_belongs_to_many">
      <value>category</value>
    </metadata>
  </contact>
  <address>
    <field name="street" type="varchar"/>
    <field name="city" type="text"/>
    <field name="state" type="text"/>
    <field name="zip" type="text"/>
    ...
    <metadata name="belongs_to">
      <value>contact</value>
    </metadata>
    <metadata name="belongs_to">
      <value>company</value>
    </metadata>
  </address>
  ...
</controller>

A schema can be requested for a specific controller by appending a name to the /schema path, e.g

/schema/contact

Returns the schema for the contact controller:

<controller>
  <contact>
    <field name="first_name" type="varchar"/>
    <field name="last_name" type="text"/>
    <field name="email" type="int"/>
    <field name="updated_on" type="datetime"/>
    ...
    <metadata name="has_one">
      <value>company</value>
      <attribute name="class_name">companies</attribute>
    </metadata>
    <metadata name="has_many">
      <value>address</value>
    </metadata>
    <metadata name="has_and_belongs_to_many">
      <value>category</value>
    </metadata>
  </contact>
</controller>
Optional formatting

Schemas can be returned in a specific format by specifying the optional format attribute, e.g. format=DTD. The supported formats include DTD, XSD and RelaxNG.

Security

Method permissions

Method permissions are specified for each controller as a list of method names, or * for any method. If a method name is specified, the method may be called. Any attempt to call a method the user does not have access to results in an exception.

Per-record permissions

Each model may specify whether its records have per-user read or write permissions. If so, individual records will require a specific user or group id to be able to read or write them. Any attempt to read or write to a record the user does not have access for results in an exception.

Per-record permissions may have one of the following values: r (read) w (write)

Model permissions

Read and write operations on models may be restricted using permissions. Any attempt to read or write to a model field the user does not have access to results in an exception.

Each permission declaration specifies a list of field names, or * for all fields.

Model field permission may have one of the following values:

  • (read/write)

r (read) w (write)

Groups

Each group represents a collection of method and model permissions.

Users

Each group represents a collection of method and model permissions.

Each user may be members of one or more groups.

If part of a group, they inherit the group's permissions. User permissions override group permissions.

Callbacks

After much dicussion, callbacks seem like too much rope - it's too easy to mess with data integrity. Given that this spec is about keeping data consistent on the server and the client, we'll punt on this for now.

Protecting against SQL injection attacks

Field values must be wrapped in quotes if they are string values. All Strings must be properly SQL escaped.