UNPKG

rutile

Version:

Factory automation for Mobile Enterprise.

1,339 lines (963 loc) 49.7 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 entity<b>Id</b> in some other system. But Rutile uses entity<b>ID</b> 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: ```javascript 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: ```javascript 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. ```javascript 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. ```javascript 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: ```javascript 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: ```javascript 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. ```javascript 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. ```javascript 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. ```javascript 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. ```javascript 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. ```javascript 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. ```javascript var app = { apptag:"Segment/Entity.register", params:{ entities:[instance], bulk:true|false }, serial:1 }; ``` For example, saving a product is defined as: ```javascript 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. ```javascript 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. ```javascript 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. ```javascript 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: ```javascript var app = { apptag:"Product/Product.remove" params:{ ids:[1,2,3] }, serial:1 }; ``` The result app object contains removed IDs. ```javascript 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 <i>APP_NAME</i>Server/<i>APP_NAME</i>Config/<i>APP_NAME</i>Config.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. ```javascript // 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. ```javascript 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. ```javascript 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. ```javascript 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. ```javascript 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. ```javascript 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. ```javascript 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*. ```javascript 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: ```javascript 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 <i>APP_NAME</i>Impl 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. ```javascript 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: ```javascript 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: ```javascript // 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: ```javascript 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. ```javascript 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. ```javascript 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. ```javascript 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: ```javascript 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*. ```javascript 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. ```xml <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. ```javascript 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. ```javascript 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. ```javascript 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. ```javascript 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. ```javascript 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. ```javascript 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](#component-overriding-impl). ``` 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.