Permalink
Switch branches/tags
Nothing to show
Find file
Fetching contributors…
Cannot retrieve contributors at this time
1339 lines (963 sloc) 48.5 KB

Specification

Here is some architectural knowledge to generate application by Rutile, and to use generated code for your concrete application.

Schema

Rutile requires data world composed of Entity and Collection.

This presupposition provides all of the code generation for both server and client. If you are familiar with EJB, Enterprise Java Beans, you have an experience this kind of database design.

This design manner simplify data and application at all points and it make hight level abstraction.

Entity

# Entity (Entity Name)
sequence:entitySeq(num)

	field*		type*	name*			search*		valid*		tags
	-----------+-------+---------------+-----------+-----------+---------------------
	entityID	int4	EntityID		SEARCH		VALID
	field1		TYPE	Field1 Name		SEARCH		VALID		TAGS
	field2		TYPE	Field2 Name		SEARCH		VALID		TAGS
	foreignID	int4	Foreign Name	SEARCH,join	VALID		helper:Segment/Entity
	  :			  :		  :				  :			  :			  :

The entityID is a sequential number generated by entitySeq defined at just after the Entity definition. This is sometimes called as surrogate key or pseudo key. Composite key is not allowed.

Its name should be a entity name starting with lower case character and with trailing ID. The primary key might be written in entityId in some other system. But Rutile uses entityID to explicitly define it is an ID.

The foreignID means a primary key of foreign entity. This should be literally its primary key.

The foreign key usually have join and helper option in schema definition. This definition does not constraint your physical database, just a logical constraint. So that you can define external database entity.

Rutile generates sql files to set up your database according to this definition. But those sql files does not have any Forign Key Constraint. Therefore, you can design your physical database as you like in this point.

(You have to add join property for search section, and helper tag for tags section at the same time. This is historical reason.)

Collection

+-----------+                     +-----------+
| Collector |◆---------+----------| Collected |
+-----------+          |          +-----------+
               +-------+--------+
               | Junction table |
               +----------------+


* CollectorCollected (Collector-Collected)

	collector/collected*					type*
	---------------------------------------+------
	CollectorSegment/Collector.collectorID	int4
	CollectedSegment/Collected.collectedID	int4

Collection means Collector has Collected entities, in other word, many-many relationship. This is represented by junction table.

All of business entities might be defined by this kind of design pattern.

Client Server Protocol

+--------+                       +--------+
| Client |<---{app} WebSocket--->| Server |
+--------+                       +--------+

To use generated server function, client have to connect to the server with WebSocket over ssl, and request a small object encapsulating application message. This is named as app.

Rutile generates SCRUD server functions as a method for each entity. Those are named as search, launch, get, register and remove.

Therefore, all operations for the specific entity can be described as "Segment/Entity.method". This is a key of app, named as apptag.

The app should have apptag for the identifier of the server side application function. Client makes a request to call the function with parameter for this identifier like this:

var app = { apptag:"Segment/Entity.method", params:{parameters}, serial:1 };

For example, to get instances of "Product/Product" its productID is 1,2 and 3 will be composed as:

var app = { apptag:"Product/Product.get", params:{ ids:[1,2,3] }, serial:1 };

The last element serial is a unique number in the client instance, to determine callback for the app.

In the actual request of websocket, the app is contained in a context object> That is bidirectional message object between client and server.

var context = { request:[ app1, app2, ... ], serial:1 };

To make request to the server, you have to make an object composed of request with array of apps, and serial with unique number in you application instance.

All apps are executed in the order you defined in the request array. After all, server returns a context object having a property name composed of "request" and serial number of you request.

var context = { 'response,1':[ app1, app2, ... ] };

Those responses are also having apptag and serial in it. And the actual result of request is in result property instead of params. Client can pick up callback for each response by them.

Following is general description of request and response object for each method.

search

The search method accepts an object having keys of constraint, logic, orderby and expand.

The constraint keyword is a main keyword for search query. This is an object containing search target and constraint values.

For example, searching instances of Product/Product, its name(Product/Product.name) like "Apple" or "Orange", and its price is between 100 and 200, can be defined as:

var app = {
	apptag: "Product/Product.search",
	params: {
		constraint: {
			"Product/Product.name(like)" : { values:["Apple","Orange"], logic:"OR" },
			"Product/Product.price(num)" : { min:100, max:200 }
		},
		logic : "AND",
		orderby: { price : "desc" },
		expand : 2
	}
};

The constraint keyword is composed of target segment, entity, field and search type in the brackets. The expand keyword is depth of result instantiation, that means how many times recursively instantiate foreign keys.

Acceptable constraint format for search type is following:

type format note
key { values:[V], logic:AND/OR } V: string or number
like { values:[V],logic:AND/OR } V: string or number
num { min:N, max:N } N: number, one or both
date { min:D, max:D } D: string represent date, one or both
timestamp { min:T, max:T } T: string represent timestamp, one or both
nearby { values:[A], logic:AND/OR } A: {centroid:'POINT(LON LAT)',distance:meter}
area { values:[A], logic:AND/OR } A: {area:'POLYGON((LON LAT,...))'}

If you define orderby for the field, you can add a orderby keyword for your app request with sort target field as its key and desc or asc for its value.

The keyword logic can be defined for each search element, and also for whole search params.

And if you define join for your field, searching foreign field can be available. For example:

var app = {
	apptag : "Order/OrderItem.search",
	params : {
		constraint: {
			"Order/OrderItem.name(like)" : { values:["Apple","Orange"] },
			"Product/Product.price(num)" : { min:100, max:200 }
		}
	}
};

If the constarint contains external segment, dabase query will be generated as dblink query.

var instances = context['response,serial'][i].result;

The result set of search method is an array of instances. You can get this array by app.result.

NOTE: No search implementation is generated for the Collection.

NOTE: If you want to cap search result, you can add limit property for constraint element or search parameter.

var app = {
	apptag : "Order/OrderItem.search",
	params : {
		constraint: {
			"Order/OrderItem.name(like)" : { values:["Apple","Orange"], limit:80 },
			"Product/Product.price(num)" : { min:100, max:200, limit:80 }
		},
		limit : 100,
	}
};


### launch

The launch method does not require any kind of parameters, just call it with empty object.

```javascript
var app = { apptag:"Segment/Entity.launch", params:{}, serial:1 };

The result app has a single object in the result property. This ia a instace of the requested Entity.

var instance = context['response,1'][i].result;

launch method can be available for both Entity and Collection.

get

The get method accepts a array of IDs you want to get instance.

var app = { apptag:"Segment/Entity.get"  params:{ ids:[1,2,3], expand:1 }, serial:1 };

You can define depth of instantiation as expand option. The value expand:1 means do not load foreign entity, 2 means instantiate foreign entity linked by foreign key defeined in the instantiate target entity itself. 3 means, and so on.

The result app is also having a array of instances.

var instances = context['response,1'][i].result;

get method can be available for both Entity and Collection.

register

The register method accepts an object having keys of entities and bulk.

The keyword entities is an array of object literal that represent instance.

var app = { apptag:"Segment/Entity.register", params:{ entities:[instance], bulk:true|false }, serial:1 };

For example, saving a product is defined as:

var product = { productID:1, name:"Apple", price:100 } ;

var app = { apptag:"Product/Product.register", params:{ entities:[product], bulk:true  }, serial:1 }

The bulk flag means a selection for saving entire entity or individual field value. If the bulk flag is false, you can save individual field value. If it is true, you are required to save entire fields. And if missing some field, server logic will fail. Even if the flag is true, you have to define its primary key with using register method.

The result app object includes all requested IDs. If there is an error in saving the ID, you can find some error info in exception property.

var app = context['response,1'][i];

var id = app.result[j].target;
if( app.result[j].exception ){
	console.log(id+"not saved");
}

Above described description is same for the Collection. But the bulk option is not affect in the Collection.

var productProductImage = { productID:1, collection:[1,2,3] };

var app = { 
	apptag:"Product/ProductProductImage.register",
	params:{ entities:[productProductImage] },
	serial:1
};

Indeed, the internal semantics of register method for Collection is different from Entity's. The method, at first, removes all entries in the junction table, then saves new collection.

But this behaviour is hidden by Model class. You can use rigster and also remove method for the Collection as same as Entity.

remove

The remove method accepts a array of IDs you want to delete instance.

var app = { apptag:"Segment/Entity.remove"  params:{ ids:[IDs] }, serial:1 };

This method does not remove foreign entities recursively. Therefore, if your entity has foreign entity only stand with the entity, you have to remove those entities independently.

For example, removing products its ID is 1, 2, and 3 can be defiend as:

var app = { apptag:"Product/Product.remove"  params:{ ids:[1,2,3] }, serial:1 };

The result app object contains removed IDs.

var ids = context['response,1'][i].result;

Server

Rutile's server side application is based on traditional container.

The container provides data persistent and object cacheing under the database transaction. This is synchronous.

Container

				           data persistent (PostgreSQL/PostGIS)

       +-----------+           +-----+
       | Container +-----+-----+ DB1 |
       +-----+-----+     |     +-----+
             |           |   
object cache |           |     +-----+
             |           +-----+ DB2 |
         +---+---+       :     +-----+
         | Redis |       :
         +-------+

The container provides object cacheing, data persistent and transaction.

You can get an instance of the container from ContainerFactory defined in your generated package. And connection information can be found in the file generated as in APP_NAMEServer/APP_NAMEConfig/APP_NAMEConfig.js. There is some default information defined, edit it for your env.

You can get container object for each database segment by specifying segment name. The other way, you can bind multi-segment or bind all of your segments.

// specific segment
var container = ContainerFactory.getContainer('Segment1');

// multi-segment
var container = ContainerFactory.getContainer('Segment1','Segment2',...);

// all segments
var container = ContainerFactory.getContainer();

container.connect();

To use container, you must call init method to clean up cached instances that was used by previous session.

container.init();

Transaction is managed by transaction object made by container. If your container is binding multiple segments, its transaction is also bound.

To start transaction, just call begin method. The default is auto-commit mode.

You can set transaction isolation level and auto-commit mode individually. And usual method is available.

You have to commit and close your transaction after your work. Un-commited transaction will be aborted at the end of the session.

var tx = container.getTransaction();
tx.begin();

tx.setAutoCommit();
tx.isolationLebelSerializable();

// your works here

tx.commit();
tx.rollback();
tx close();

Object cache is provided by Redis. It minimizes database access, and makes instances identical for the same model for the same id in your session. But it does not provide any transaction.

var instance1 = Model.instance(1);
var instance2 = Model.instance(1);

instance1 === instance2; // true

Logic

+----------+
| Frontend | (server.js)
+---+------+
    |
	| find a logic for the app
	| 
    |    +---------+
	+--->|  Logic  |
         +---------+

The entry point of server application is server.js, the frontend trigger for your app.

The server looks for appropriate application logic for requested apptag from LogicFactory that is generated in you server package. The logic found by server is a function object that implements SCRUD.

var method = LogicFactory.getMethod(apptag);
method(context,app);

The function object accepts context object and responsible app object. The concrete implementation is provided by Model.

Model

Model class is preliminary generated according to your schema definition. To manage your Entity or Collection, at first you have to get a model class from your generated ModelFactory. And then, instantiate it by ID.

var ModelFactory = APP_NAME.getModelFactory();
var EntityModel = ModelFactory.getModel("Segment/Entity");

var instance = EntityModel.instance(id);

Collection instance can be get in the same manner.

All of the model instances are managed by the container. Therefore, as described above, the instance is always same object in the session.

Model class implements concrete SCRUD functions for the Entity/Collection. Functions SCR, search, create and read are defined as static method. The rest UD, update and delete are defined as instance method.

The life cycle of data persistent is like following.

var ids = Model.search(query);     // S:search
var id = Model.publishID();        // C:create
var instance = Model.instance(id); // R:read
instance.save();                   // U:update
instance.remove();                 // D:delete

Sanitizing phase

The model implementation automatically sanitizes you input value. This logic is automatically generated by your data type.

instance.field = value;

For example, if you tyr to set a string for a field being numeric type, instance does not accept you input.

Indeed, this function simply sanitize the value, so no exception has been occurred. In this case, the field will becomes 0.

type sanitizing default
int evaluate as number 0
int2 evaluate as number 0
int4 evaluate as number 0
text evaluate as string ''
date evaluate as Date object, then stringify null
timestamp evaluate as Date object, then stringify null
geography evaluate as string ''

Type geography is simply evaluated as string, not checked as POINT format.

Validation phase

You can check whether your instance is having valid field value as you defined in schema. This is provided by instance method valid.

instance.valid();

instance.valid('fieldName');

Calling valid method without argument checks all fields. Otherwise, define a field name you want to check. Both returns true or false.

valid validation
notNull true if some data in there
positiveValue true if the value is positive
negativeValue true if the value is negative
timestampString true if the value is formatted as timestamp style
dateString true if the value is formatted as date style
emailString true if the value is formatted as email style
geographyPoint true if the value is formatted as PostGIS point style
(helper) true if the foreign entity exists

If you define helper tag for your field, validation process try to find your foreign object. And if the object exists correctly, returns true.

This behaviour seems to make trouble when you saving multiple entities depending each other by foreign key. But don't worry. Container returns same instance in the same session.

For example, "Product/Product" has "Product/Product.productImageID" field, and its helper foreign entity is "Product/ProductImage", and both are freshly created instaces:

var productID = Product.publishID();
var productImageID = ProductImage.publishID();

var product = Product.instance(productID);
var productImage = ProductImage.instance(productImageID);

product.price = 100;
product.productImageID = productImageID;
productImage.image = blob.toString();

product.save();
productImage.save();

The method call product.save() will look up the instance for productImageID in its internal validation phase, but the container returns same instance already having your blob string just set above. Hence no error.

This is the same thing whenever you call via websocket, while you are using app and batchtag.

(Those are described in the section of Client.)

Constraint

Constraint is a internal representation of search query.

Rutile generates query compiler for each Entity named SQLMaker. It gets the request object created by UI, described in above, and makes intermediate constraint objects, then makes sql expression for them.

All of query elements are preliminary generated as a class definition for each.

For example, searching "Product/Product" entity, its productClassName likes some value, under the condition of "Product/Product.productClassID" is defined with helper "Product/ProductClass", is generated as a file:

Constraint/Product/Product/SelectbyProductClassProductClassNameLike.js

The format is:

Constraint/<Traget Segment>/<Target Entity>/Selectby<Search Entity><Search field><Search type>.js

This is why you have to define unique entity name across all segments.

(Of course the file name can be generated as including Search Segment, but not nowadays.)

Component overriding (Impl)

Rutile generates symmetric package for your main APP_NAME package named APP_NAMEImpl under the same directory of the former. The package has symmetric directory structure for the main package.

Automatical implementation such as authentication and permission management will be generated as override module in there. And also, you can override modules by putting your modules into the appropriate location and modify Factory being there.

For example, following module overrides ProductImage model definition of DemoShop schema.

var DemoShop = require('DemoShop');
var ModelFactory = DemoShop.getModelFactory();
var ProductImageModel = ModelFactory.getModel('Product/ProductImage');
var ParentConstructor = ProductImageModel.getClass;

function ModelConstructor(){
	var instance = new ParentConstructor(arguments);
	
	// wrapping the save method
	var orig_save = instance.save;
	instance.save = function(){
		console.log("I am save method, wrapped by implementation!");
		orig_save();
	};
	
	return instance;
}

module.exports = {
	getClass      : ModelConstructor,                   // override
	publishID     : ProductImageModel.publishID,        // delegate to the parent
	instance      : ProductImageModel.instantiate,      // delegate to the parent
	search        : ProductImageModel.search,           // delegate to the parent
	ids           : ProductImageModel.ids,              // delegate to the parent
	fieldManifest : ProductImageModel.getFieldManifest, // delegate to the parent
};

Put this file as in DemoShopImpl/Model/Product/Product.js, and modify DemoShopImpl/Model/ModelImpleFactory.js to return a instance of this module something like this:

models.__defineGetter__('Product/ProductImage', function(){
	if( module_caches['Product/ProductImage'] ){
		return module_caches['Product/ProductImage'];
	}
	module_caches['Product/ProductImage'] = require('./Product/ProductImage');
	return module_caches['Product/ProductImage'];
});

Client

index (list of entities)
      |
      |
	  V
+------------+         +------------+
|    List    |<------->|  EditForm  | 
+------------+         +------------+
      ^ 
      | 
      v
+------------+
| SearchForm | 
+------------+

Rutile generates KitchenSink application as a combination of three basic functions, List, EditForm and SearchForm at the end.

The index is a list of all your Entities. Selecting one of them shows the list of instances in the Entity. And selecting one of them shows the detail of values the instance having.

The list has a button to bring up SearchForm having possible search patterns according to your schema definition. And also, this list has a function to delete some instances.

Model

Client side model is a facade object for its data management and UI interaction. This is not Alloy's model.

Model also provides SCRUD method. Functions SCRD are provided as static method. The rest U is provided as instance method. Those methods are bit different to the interface implemented in server side model. This is for making it simplify to mangae callbacks. Data interaction in client side components are implemented as async structure.

The life cycle of data persistent is following:

// S:search
Model.search({
	query    : { valid query described in the section of Client Server protocol },
	batchtag : 'apptag binder',
	callback : function(){ 'you can get search result here!'; },
});

// CR:create and read
Model.instantiate({
	primaryKeys : [array of IDs you want to get instance],
	expand      : depth of instantiation,
	batchtag    : 'apptag binder',
	callback    : function(){ 'you can get instances here!'; }
});

// U:update
instance.save({
	batchtag : 'apptag binder',
	callback : function(){ 'you can get saved instance here!'; }
});

// D:delete
Model.remove({
	ids      : [array of IDs you wan to remove],
	batchtag : 'apptag binder',
	callback : function(){ 'you can get removed ids here!'; }
});

To modify your field value, you can use usaul method.

PostgreSQL treats your field name as case insensitive, unless you define them in double quotation. But generated Model class defines your field as is. (case sensitive) So you can use your instance like this:

instance.fieldName = value;

You see several batchtag properties in the above gist. This is a binder to serialize multiple apps.

The app is a small package of application. Keyword of batchtag makes a percel to the server call, so that execute those fragments together. Bound apps will be executed in the same context, in other word in the same session. Those are also executed in the order you called methods with the same batchtag.

For example, saving a fresh Product instance having foreign key productImageID, and its linked entity ProductImage is also fresh, you have to save them with same batchtag.

var Dispatch = require('CentralDispatch'); // singleton

productImage.save({
	batchtag : 'saving product',
	function : function(){ console.log('image saved'); },
});

product.save({
	batchtag : 'saving product',
	function : function(){ console.log('product saved'); },
});

Dispatch.sync('saving product'); // actual invocation of save

The CentralDispatch is a framework provided by Rutile as a singleton object. Calling a save method generates an app representing its operation, and push it into the queue of CentralDispatch. Therefore, your methods call anywhere in your application with same batchtag will be invoked in the same context when the sync was called. And those apps are executed by its order you pushed.

(In above sample snippet, the first line is just for demonstration, you don't have to call it before save, but just before Dispatch.sync in the last.)

(Client side model should have sanitizing and validation phase like server side model. But not yet implemented.)

CentralDispatch

As mentioned above, CentralDispatch is a framework provided by Rutile.

This module encapsulates application call for the server, and manages series of apps by batchtag.

In Rutile generated UI application, application function is fragmented in small package of app. So usually tracking serial number and managing callbacks makes code unreadable.

CentralDispatch encapsulates these things. You can make app request object that having only your business logic.

var Dispatch = require('CentralDispatch');

var work1 = function(instances){
	instances.map(function(instance){ console.log(instance); });
};

var app1 = {
	apptag   : "Product/Product.get",
	params   : { ids:[10,20,30] },
	callback : work1
};

Dispatch.sync(app1);

The sync method of CentralDispatch immediately execute your app.

var tag = 'my series';

var work2 = function(instances){
	instances.map(function(instance){ console.log(instance); });
};

var app2 = {
	apptag   : "Product/TopSales.get",
	params   : { ids:[1,2,3] },
	callback : work2
};

Dispatch.push(tag,app1);
Dispatch.push(tag,app2);
Dispatch.sync(tag);

On the other hand, using push method with tag stacks your works in its queue, and then executing them with calling sync method.

This is useful in the case of some works having dependencies. Bound apps will be executed in the same context(session) in sever. This is indispensable functionality for saving instance having fresh foreign object.

In client side, stacked callbacks will be also executed in the order you push. But this does not guarantee that those callbacks are serialized. Callback follows standard JavaScript manner.

If your callbacks depend on each other in your data oriented application, it might be a sign that you can get more better schema and UI design.

FYI, the implementation of CentralDispatch that calls back is following:

socket.once(response_event,function(context){
	var responses = context.response;
	for( var i=0; i<responses.length; i++ ){
		var response = responses[i];
		var apptag = response.apptag;
		var result = response.result;
		var serial = response.serial
		var callback = callbacks[serial];
		callback(result);
		delete callbacks[serial];
	}
});

(The name CentralDispatch is just for fun)

NotificationCenter

NotificationCenter is an event propagation module provided by Rutile client framework.

You know this kind of module everywhere in complex JavaScript UI application.

NotificationCenter has usual methods notify, listen, once and remove.

var Notifier = require('NotificationCenter');

Notifier.notify(EVENT_NAME,object);

Notifier.listen(EVENT_NAME,callback);
Notifier.once(EVENT_NAME,callback);

Notifier.remove(EVENT_NAME,callback);

If you forget to remove your registration for NotificationCenter, your callback will leak.

Navigation

Rutile provides generic navigation controller like in iOS objective-c application, just named as NavigationGroup.

To use this navigation system, you have to add it to your index.xml at first.

<Alloy>
	<Window>
		<Require src="Framework/NavigationGroup" id="NavigationGroup"/>
	</Window>
</Alloy>

And in the controller of this view, the index.js, load your first Alloy controller. Then open it.

var navi = $.NavigationGroup;
navi.setRootWindow($.index);
navi.enableBackButton();

var entityList = Alloy.createController("/KitchenSink/EntityList");
navi.open(entityList);

This is the part of actual code to be generated for your schema.

Controllers you opened by this protocol can enjoy the benefit of several iOS like callbacks at some timing of its view life cycle.

exports.viewDidLoad = function(){
	navi = Alloy.Globals.navigationControllerStack[0];
	updateView();
};

exports.viewWillAppear = function(){
	var rootWin = navi.getRootWindow();
	rootWin.add(infoPanel.getView());
	infoPanel.restorePosition();
};

exports.viewWillDisappear = function(){
	var rootWin = navi.getRootWindow();
	rootWin.remove(infoPanel.getView());
	infoPanel.resumePosition();
};

Method viewDidLoad will be called at the view has been loaded by NavigationGroup. In this phase, getting the navigation instance is a recommended way to manage your navigation. And also, you have to setup your controller's view component here.

The component in you view will be loaded at the timing of the controller is instantiated by Alloy framework. Method viewDidLoad is the almost same timing as this.

Method viewWillAppear will be called at the view will be actually shown in the view rect of your application. This is useful to activate functionality that should be stopped while the view is not shown for user.

Last method viewWillDisappear will be called at the timing the view will be actually invisible from your view rect. In the opposite direction of viewWillAppear, this method is useful for the the functionality that should be inactivated while the view is not visible.

NavigationGroup provides several utility for your view, here is abstruct.

method arguments description
showSubMenu navbar can have main and sub,
hideSubMenu you can change between them by show/hide
setTitleView Ti.UI.View title view for main navbar
setSubTitleView Ti.UI.View for sub navbar
addLeftButton kind of Framework/Navi*Button buttons in left side of main navbar
setLeftButton kind of Framework/Navi*Button ditto
addSubLeftButton kind of Framework/Navi*Button buttons in left side of sub navbar
setSubLeftButton kind of Framework/Navi*Button ditto
addRightButton kind of Framework/Navi*Button buttons in right side of main navbar
setRightButton kind of Framework/Navi*Button ditto
addSubRightButton kind of Framework/Navi*Button buttons in right side of sub navbar
setSubRightButton kind of Framework/Navi*Button ditto
setRootWindow Ti.UI.View root window, all controller shown on this
getRootWindow get root the window object
enableBackButton automatically show back button
back do back
close close current navigation group
open instance of Alloy controller open new controller

Additionally, you can use ModalWindow to open new controller under modal. This modal method creates new NavigtionGroup. And push it to the global accesible array as defined Alloy.Global.navigationStack. When the modal been closed, it removes itself from the stack.

For more detail, see Framework/NavigationGroup.js and Framework/ModalWindow.js in your generated client package.

Component made by Framework

The final output of Rutile is a KitchenSink TiApp that covers all of possible basic UI comes from your schema design.

As described in the README.md, generated application has following component stack.

+-----------------+
|   KitchenSink   | Generated App covering all Components
+-----------------+
| Component/Model | Generated UI components and Models
+-----------------+
|    Framework    | Rutile client framework
+-----------------+
|      Alloy      |
+-----------------+

Rutile generates UI components to show, edit and search your data from abstract UI component implemented in client Framework. Show and edit are made from component group named as EditFormElement* implemented in the Framework. EditFormElement beeing in un-editable mode represents show functionality of it.

Search is made from component group named as SearchFormElement* also in the Framework.

The final product is an application to show, edit and search your entire entity relation.

If your entity has a foreign key, its editor should have selector for the linked entity. If your entity has a image type field, its editor should have image selector accessing to your local album. If your entity has a aggregation(Collection), its editor should have selector to pick up collecting entity.

Those are same for searching. If your entity has a foregin key, its search form should have a form to define field value for all fields you want to constraint search query. Bla bla bla.

Each edit form and search form is very basic functionality, that can be defined by your schema definition. For instace, number type value will be edited by text field UI, and will be searched by less than, more than or in range. But integrating them as a single application that having ratinal page transaction is little bit complex.

Rutile actualizes this kind of work by generating intermediate component made from abstruct element, EditFormElement* and SearchFormElement*.

Those components are generated in the Component directory in your generaged client package. You are able to use them for your own implementation as a part of it.

And combination of those Component with Model, NavigationGroup, CentralDispatch for remote access and NotificationCenter for local event propagation realizes application itself.

EditFormElement

type Base Framework module note
primary key EditFormElementPrimaryKey if the key is primary key
int,int2,int4 EditFormElementInt
text EditFormElementTextArea if having tag editor:textArea
text EditFormElementTextField if having tag editor:textField (default)
date EditFormElementDate
timestamp EditFormElementTimestamp
image EditFormElementImage
geography EditFormElementLocation
extkey EditFormElementExtkey if having tag helper:ENTITY
extentity EditFormElementExtentity if this is a collection item

The implementation of Alloy controller to edit your field is generated according to your field type into the location of controllers/Component/EditForm/Segment/Entity/Field.js. Its file name is defined as field name starting with upper case charater. Corresponding styles and views are also generated in the same manner.

For example, editor for "Product/Product.price" should be defined as controller/Component/EditForm/Product/Product/Price.js. The generated editor implements collaboration with Model. The Price.js editor listen and notify data change to the instance of Product Model.

To enable this form and model binding, you have to use EditFormGroup described below.

If your entity has collection, editor for the collection is also generated in controllers/Component/EditForm/Segment/Entity/Collection/CollectedEntityName.js.

For example, if the entity "Product/Product" has collection that collecting "Product/ProductImage", controllers/Component/EditFormSegment/Product/Product/Collection/ProductImage.js should be generated. This collection edit form provides ProductImage picker and show the list of collected instances of ProductImage.

SearchFormElement

type Base Framework module
key SearchFormElementKey
like SearchFormElementLike
num SearchFormElementNum
date SearchFormElementDate
timestamp SearchFormElementTimestamp
nearby SearchFormElementNearby
area SearchFormElementArea

The implementation of Alloy controller to search your data is also generated according to your field search type definitions into the location of controllers/Component/SearchForm/Segment/Entity/SelectbyTargetentityTargetfieldType.js.

Its file name is defined as composite of constraint entity name, its field name and search type with prefix 'Selectby'. Correspoinding styles and views also there.

For example, the search form for searching "Product/Product" by match full of "Product/ProductClass.productClassName" linked by "Product/Product.productClassID" foreign key definition should be defined as contollers/Component/SearchForm/Product/Product/SelectbyProductClassProductClassNameKey.js.

Your search query should be generated by all SearchForm impelmentation on your concrete application. This is provied by SearchFormGroup you can get at Rutile client Framework described below.

EditFormGroup (Form Model binding)

To make link between your edit form and its corresponding model instance, you have to put your Alloy view elements representing implementation of EditFormElement and instances of Model into the EditFormGroup. The linkage will be automatically established accordint to its name.

var EditFormGroup = require('EditFormGroup');
var group = EditFormGroup.makeGroup(UNIQUE_NAME);

group.setForms([$.FORM,$.FORM2]);
group.setModels([instance]);

group.activate();

EditFormGroup requires unique name across your application, so that identify appropriate event.

After you make instance of EditFormGroup by makeGroup() method, just add your list of forms and instances by setForms and setModels. You can add forms and instances being for different Entities. EditFormGroup will identify those aspect and automatically bind appropriately.

Calling activate actually start form and model binding. You can also manually synchronize at the specific timing by calling syncEntityToForm or syncFormToEntity. The former applies entity's field value to bound form, and the latter applies form value to the bound model.

group.syncEntityToForm();
group.syncFormToEntity();

SearchFormGroup

The implementation of SearchFormElement supports you to get a constraint object for the specific field. And SearchFormGroup supports you to get the combination of those implementations.

Your actual search application may have several elements to input field data, to select search logic, and to select orderby field. And the params for app should be created as a combination of them. To get query object for your search app, just add your forms to the SearchFormGruop with definition of the search target entity.

var SearchFormGroup = require('SearchFormGroup');
var group = SearchFormGroup.makeGroup({
	'name'    : "Segment/Entity",
	'segment' : "Segment",
	'entity'  : "Entity",
});

group.addElements([$.FORM,$.FORM2]);

group.setLogicSelector($.Logics);
group.setOrderbys($.Orderbys);

The group should be initialized by unique name, and the name of Segment and Entity to be selected. The unique name is typically defined as its full segment name.

Logic selector and orderby selector also generated in your client package.

group.setSubmitAction({
	'form'    : $.Submit,
	'handler' : function(){
		Notifier.notify('Segment/Entity.searchQueryChanged',{
			'query'           : group.getQuery(),
			'constraintTexts' : group.getTextExpressionOfConstraint(),
			'logicText'       : group.getTextExpressionOfLogic()
		});
		navi.close();
	}
});

To execute your search, set your handler to the group using setSubmitAction. In the above sample code, clicking $.Submit notifies query to the List page that listening(waiting) for your query.

KitchenSink made by Component

Finally, TiApp is generated as a combination of all above components, as for your KitchenSink.

It has three main pages, List, Editor and SearchForm. Those are generated for two version. One is for application main path, and one is for reuse.

List(Reusable)

+----------------------+     +----------------------+
| <  EntityList (S)(+) |     | <  EntityList (S)(+) |
+----------------------|     +----------------------|
| □ instance         > |     | instance           > |
| -------------------- |     | -------------------- |
| □ instance         > |     | instance           > |
| -------------------- |     | -------------------- |
| □ instance         > |     | instance           > |
| -------------------- |     | -------------------- |
| □ instance         > |     | instance           > |
| -------------------- |     | -------------------- |
| □ instance         > |     | instance           > |
| +------------------+ |     | -------------------- |
| | Query            | |     | instance           > |
| |    details...    | |     | -------------------- |
+----------------------+     +----------------------+

* with gadget                * simple reusable

The index of your TiApp is a list of all entities. Tapping an entity navigates you to the List page for the selected entity. At first, the List simply select all instances being defined for this entity. Select all will be limited by Config.txt definition.

Rutile generates two version of List controller. One is some gadget and the other simply list instances.

The former controller is just for application main path, to create and delete instances and navigate several different searching. The latter is for reuse. When you select a foreign key in EditForm, UI brings up a selector to pick up an instance with a selection of create new one, select from list or search. Reusable List controller is for this, that don't need gadget.

The List for main path is generated as KitchenSink/Segment/Entity/List.js, and reusable version is as ListReusable.js in the same location.

EditForm(Reusable)

+----------------------+     +----------------------+
| <     Editor     (E) | ==> | X      Editor      V |
+----------------------|     +----------------------|
| Entity               |     | Entity               |
| +------------------+ |     | +------------------+ |
| |FLD : value       | |     | |FLD : value       | |
| +------------------+ |     | +------------------+ |
| |FLD : value       | |     | |FLD : value       | |
| +------------------+ |     | +------------------+ |
| |FLD : value       | |     | |FLD : value       | |
| +------------------+ |     | +------------------+ |
| Collection           |     | Collection           |
| +------------------+ |     | +------------------+ |
| |Collected         | |     | |Collected         | |
| +------------------+ |     | +------------------+ |
+----------------------+     +----------------------+

* click (E) to editable mode
* reusable version has same visual, but different internal

In the main path, selecting an instance navigates you to the detail view of it. Un-editable mode of EditForm is for viewing. When you tap edit button on it, it alters its mode to be editable.

Main path version and reusable version has a same visual, but different internal function. The former returns to the List page with a query, the latter will be only called from selector brought up by EditForm to pick up target instance for a foreign key.

In the same manner, EditForm will be generated as KitchenSink/Segment/Entity/EditForm.js, and reusable version is as EditFormReusable.js in the same location.

SearchForm(Reusable)

+----------------------+     +----------------------+
| X    SearchForm      | ==> | <  ResultList (S)(+) |
+----------------------|     +----------------------|
| Field(type)          |     | instance           > |
| AND|OR           (+) |     | -------------------- |
| +------------------+ |     | instance           > |
| | value            | |     | -------------------- |
| +------------------+ |     | instance           > |
| Field(type)          |     | -------------------- |
| AND|OR           (+) |     | instance           > |
| +------------------+ |     | -------------------- |
| | from ~ to        | |     | instance           > |
| +------------------+ |     | -------------------- |
|                      |     | instance           > |
|       [Search]       |     | -------------------- |
+----------------------+     +----------------------+

* [Search] to show List
* reusable version has same visual, but different internal

Both main path version and reusable version has same visual. That contains all elements generated as SearchFormElemnt for the target entity.

The different between those two version is action for the submit. SearchForm for the main path returns back to the List page with gadget, and the rest is notify for its listener.

SearchForm will be also generated as KitchenSink/Segment/Entity/SearchForm.js, and reusable version is as SearchFormReusable.js.

Auto Implementation

The above auto generated client/server framework and application based on them are very core of application structure. That implements minimum implementation.

Rutile can generate several auto implementation for your more practical application.

Authentication and Authorization

With defining AuthPassword keyword in your config file, Rutile automatically implements authentication and authorization logic.

The authorization is implemented as a normal logic that can be requested as an app. For example, if you select id and password auth logic, app will be described as:

var app = { apptag:'AuthPassword.authorize', params:{ id:ID, pass:PASS }, callback:authenticated };

If succeeded to authorize, the callback will be called. The callback never called if the request was failed. In this case, client application have to catch the exception message notified by NotificationCenter.

var Notifier = require('NotificationCenter');

Notifier.listen('Error.AuthPassword.authorize',function(Error){
	// authorize failed
});
Notifier.listen('Error.AuthPassword.authenticate',function(Error){
	// authentication error
});

Authentication of app will be implemented as override components in the implementation package.

var Auth = require('../AuthPassword');

var auth_required_search = function(context,app){
	var authenticate = Auth.getMethod('authenticate');
	if( !authenticate(context) ){
		return; // message will be sent with context.Error.AuthPassword.authenticate
	}
	var method = ProductImageLogic.getMethod('search');
	method(context,app);
};

If your authorization request was successfully accepted by the server, it returns a JSON Web Token signed by certificate defined in <APP_NAME>Config package.

After getting token, client sends apps request with the token in context object, then server checks the token to authenticate apps like above logic implementation.

Certificate file to sign the token will be automatically generated in the <APP_NAME>Config directory. This is just for test purpose. You should put your development or production certificate file in there.

Permission management

coming soon.

Image thumbnail service

coming soon.

Graph for timeline data

coming soon.