Dynamic Runtime

Changing your data model as your application runs.

See list of endpoints.

See the Home page for more information about this application.

To login (or register as a new user) go to Login.

Definition of Dynamic Type

Overview

This document defines the data format for the dynamic type, the basic building block of the application. In a typical schema, you define types which are then built on top of fields. In our application, we call the type definition DnType and the field definition DnField. We do this because the words type and field are so common in coding and can refer to so many different concepts that using a variant name will help make it clear that we are talking about types and fields that are the foundational elements of this application.

Primitive Types

These types have global definition, have no namespacing, and are reserved keywords that cannot be reused when naming types. The primitive types are the following:

Before we go on, we call a type simple if it has no fields and a recursive chase up the base types leads to a primitive type. A type is inline, if it is not captured into the DnSchemaStore. The data structure of the DnType is as follows:

DnType

When this type is cloned, all the top level attributes are put into a new Map and the top level attributes of the dnFields are cloned as well. Altering values interior to the values that are cloned will likely cause undesired side-effects and should be avoided. In particular, the dnTypeDef attribute of the DnField object is only cloned if it needs to be modified.

DnField

DnBuilder

During the design phase of the schema implementation, I looked at various ways to capture repeated patterns in schema definition. As an example, a traditional way to do this is to use parameterized types with the schema definition not fully defined until you give it externally supplied type definitions as parameters. I also looked at writing a generic build that would let you union the fields from various types together to build another type. This mimics the traits pattern that can be found in many programming languages.

However, neither of these patterns fits JSON data models and database table definitions as I have seen them over the years. In many cases, the patterns that needed isolating from the main code came with a lot of presumptions about the behavior of those fields. And many times, so many of the additional fields and types that I might add using parameterized types and traits were conditional on certain characteristics of the deployment and the specific nature of the usage of the schema. It just made using standard programming language constructs for reusing schema definitions clunky and produced arcane artifacts. It was also very confusing to explain in a user interface what was going on with a particular type.

In the following we describe the various DnBuilders available in the core component of this application.

Endpoint Builder

This builder, named endpoint, builds a DnType that can be used to define an endpoint.

In addition to using the name, label, and description for documentation, it adds the following custom fields to the DnType definition.

As an example of what the builder might do, let us assume the inputTypeRef named Category has two fields category and fromDate, the first is a String and the second is a Date. Assume the response type named Content has two fields contentId and contentDate with the first being a string and the second a date.

Suppose you defined the following type, using yaml style syntax.

name: CategoryRequest
label: Requests Content by Category
description: Allows requests by category starting after a particular date.
dnBuilder: endpoint
httpMethod: GET
path: /content/byCategory
function: getContent
inputTypeRef: Category
outputTypeRef: Content
isListResponse: true
hasMorePaging: true
ruleToGetContent: byCategory       

Note the extra custom parameter ruleToGetContent. This is visible to the getContent function and allows the function to alter its behavior based on the schema definition of the endpoint. One function can potentially serve many endpoints.

Then the builder would produce the following type.

name: CategoryRequest
label: Requests Content by Category
description: Allows requests by category starting after a particular date.
dnBuilder: endpoint
httpMethod: GET
path: /content/byCategory
function: getContent
inputTypeRef: Category
outputTypeRef: Content
isListResponse: true
hasMorePaging: true
ruleToGetContent: byCategory
isEndpoint: true
dnFields: 
   - name: endpointInputType
     dnTypeDef:
         baseType: Category
         dnFields:
           - name: limit
             label: Limit on Results
             description: Maximum number of items that can be returned.
             defaultValue: 100
             sortRank: 200
             dnTypeDef:
                baseType: Count
                max: 20000
   - name: endpointOutputType
     dynTypeDef:
         dnFields:
           - name: numItems
             label: Number of Items
             description: Number of items returned.
             required: true
             dnTypeRef: Count
           - name: hasMore
             label: Has More Items
             description: The results returned are not all the results available if this value is true.
             dnTypeRef: Boolean
           - name: requestUri
             label: Request URI
             description: The request URI that made this request.
             required: true
           - name: duration
             label: Duration
             description: The time taken to perform request in milliseconds.
             required: true
             dnTypeRef: Float
           - name: hasMore
             label Has More
             description: Set to true if list of items is incomplete
             required: true
             dnTypeRef: Boolean
           - name: items
             label: Items
             description: Items returned by endpoint.
             required: true
             isList: true
             dnTypeRef: Content

The reference to the dnType Count is a reference to a built-in type that extends Integer and has a min value of zero.

If you look at the above, there are some things to call out. The first is that the DnType Category is extended to have an additional limit field that can be used to limit the results. The second is that the DnType Content has been subsumed into a containing type so that the items field of the containing type has Content as of one of its objects. The values duration, requestUri, and none are all handled by generic protocol handlers and are invisible to the implementation code for the function getContent.

Table Definition Builder

The other major data model defined by a DnType is the definition of a table. This not only includes the columns, but the definition of the primary key, indexes, and any special storage options for the table. The definition is live in that it is used to automatically create tables and to update the table, if the current design of the table does not match the schema. However, there are operational issues that may limit the types of fix-ups that are performed against tables. For example, creating a new index on a table with a billion rows when the table is under heavy usage is not a good idea. In that case, the index would be applied manually by a database administrator. But the schema definition can still assist. It is easy to write a command line utility that uses the schema to create a report of what columns or indexes need to be added by an administrator before the code can be deployed.

Note: We are skipping the implementation of foreign keys for now. We will get back to them when they prove useful. In this application, rows are never deleted; they are only disabled. Rows get deleted (by not copying them) when the entire table is copied from one database to another during a data refresh process.

The builder is named table and it has the following inputs.

An Index object can either be:

If the index is supplied using the first option, it is converted into the second.

Here is an example of how this might work. First we give the inputs and again we use yaml syntax. Note that TSV stands for Two Step Verification.

name: TsvContactsTable
tableName: TsvContacts
description: Holds contact information for the user
dnBuilder: table
dnFields:
  - name: contactAddress
    label: Contact Address
    description: The contact (phone number or email) used to make contact.
    required: true
    dnTypeRef: String
  - name: contactType
    label: Type of Contact
    description: The type of contact (phone or email) used to make contact.
    required: true
    dnTypeRef: String
  - name: verified
    label: Verified
    description: Whether the contact has been verified by sending a code to it.
    dnTypeRef: Boolean
primaryKey: ['userId', 'contactAddress']   # Note the reference to userId which will be added to the table design.
indexes:
    - ['contactAddress', 'enabled']  # Allow user who forgot password to use contactAddress to reconnect.
hasRowDates: true
isUserData: true   

This will be turned into the following.

name: TsvContactsTable
tableName: TsvContacts
description: Holds contact information for the user
dnBuilder: table
isTable: true
hasRowDates: true
isUserData: true   
dnFields:
  - name: userId
    label: User ID
    description: Unique identifier for the user
    required: true
    dnTypeRef: Integer
  - name: userGroup
    label: User Group
    description: Group or organization to which the consumer belongs
    required: true
    dnTypeRef: String
  - name: contactAddress
    label: Contact Address
    description: The contact (phone number or email) used to make contact.
    required: true
    dnTypeRef: String
  - name: contactType
    label: Type of Contact
    description: The type of contact (phone or email) used to make contact.
    required: true
    dnTypeRef: String
  - name: verified
    label: Verified
    description: Whether the contact has been verified by sending a code to it.
    dnTypeRef: Boolean
  - name: enabled
    label: Enabled
    description: Whether the row is enabled. If not enabled, a row is treated as if it were deleted.
    dnTypeRef: Boolean
    required: true
  - name: createdDate
    label: Created Date
    description: Date row was created in this table
    required: true
    dnTypeRef: Date
  - name: modifiedDate
    label: Modified Date
    description: Date row was last modified in this table
primaryKey: 
    fields: ['userId', 'contactAddress']   # No *props* because there are none.
indexes:
  - name: GroupModifiedDate
    fields: ['userGroup', 'modifiedDate']
  - name: ModifiedDate
    fields: ['modifiedDate']          
  - fields: ['contactAddress', 'enabled']
  - name: UserIdModifiedDate
    fields: ['userId', 'modifiedDate']

In addition to adding protocol fields to the table, the table definitions built this way also come with associated code that will automatically generate prepared SQL statements that perform the predictable queries against the data. There is also code that automatically populates a new table with the user fields, the date fields, and the enabled field without direct intervention by the logic using the table to do TSV.

Namespacing

One of the reasons for cloning is for making a copy of a DnType so it can be put into a particular namespace, which alters the name of its type. Also, any fields referring to a base type may have that reference adjusted as well. Essentially, if the name of a DnType does not have a dot ('.') in it, then the current namespace is added a prefix separated by a dot. This allows the definition of the raw type to be done without reference to a namespace and the name spacing applied later.