Wakanda Concepts
Wakanda Concepts
Wakanda Concepts
This
Wakanda Overview Wakanda is a development and deployment environment for browser-based data applications. Wakanda is composed of three components: Wakanda Server, a data/HTTP/REST/web server, Wakanda Application Framework (WAF), a browser-based JavaScript framework used to simplify access to Wakanda Server, and Wakanda Studio, the development environment used to create both front- and back-end portions of an application. The native programming language for Wakanda is JavaScript. When designing a Wakanda application, you use JavaScript under Wakanda Server and also when designing web pages using WAF. In order to make development as easy as possible, WAF programming is similar to programming under Wakanda Server. For example, the WAF provides access to datastore classes on the browser side in a manner substantially similar to accessing these same classes under Wakanda Server. This document however explores Wakanda Server concepts and code. Unless specified, programming examples in this document refer to server-side Wakanda programming. Wakanda Datastore Model Built into Wakanda is a data management system that lets you easily manipulate information. The Wakanda datastore model uses a datastore class paradigm rather than a relational database methodology. Instead of representing information as tables, records, and fields, Wakanda uses a new approach that more accurately maps data to real world items and concepts. Relational Databases, Normalization vs. Denormalization Relational databases are an efficient way to store and retrieve information. They achieve efficiency by storing information in lists called tables and linking tables together through values in specific fields. The highest level of efficiency is achieved by following a discipline called normalization in which information only appears in one location but is referenced wherever needed. This approach removes the need to update data in multiple places whenever a value is changed. However, normalization can add complexity to the development process and often results in a relational structure that obscures the real world concepts of the information it contains. Take for example the following partial relational database structure:
Fig 1
This structure describes Invoices, each of which can have many InvoiceItems. Each InvoiceItem refers to one Part that holds the name and individual cost. An Invoice total might be calculated by summing the product of InvoiceItems.Quantity and Parts.Cost for all InvoiceItems for a given Invoice. Of course, each time the Invoice total is calculated, the relationships between all three
Wakanda Server Concepts Page 1 of 61
tables must be understood and traversed correctly. This approach adds complexity to the development process. Often database designers will avoid this kind of complexity by incorporating some degree of denormalization in their datas structure. For example, in the above structure one might be temped to add a Cost field to InvoiceItems copied from the corresponding part or an InvoiceTotal field to the Invoices table where the sum of all its InvoiceItems is stored. Adding these two fields would certainly be more convenient and in some cases increase performance to one portion of the development process, but increase complexity in another. With a Cost field in InvoiceItems or an InvoiceTotal field in Invoices, developers must take care to recalculate and store these fields whenever a cost or quantity is changed. This perpetual battle between the need for efficiency via normalization and the need for convenience via denormalization is one of the issues that the Wakanda datastore model addresses. SQL Queries Return Row Sets not Records Another struggle in relational databases is the difference between the records stored in tables and the rows returned by a query. Take for example the following query used with the relational structure above:
SELECT i.Post_Date, p.Name, p.Cost, ii.Item_Quantity, p.Cost * ii.Item_Quantity as Extended From InvoiceItems ii Join Parts p on p.ID = ii.Part_ID Join Invoices i on i.ID = ii.Invoice_ID Where i.Post_Date between 1/1/10 and 12/31/10
The result of this query would be a list of post dates, part names, costs, quantities and extended costs for all invoice items on all invoices in the year 2010. Notice that the resulting rows are a collection of columns from all three tables and include a calculation. Each row in the resulting row set is neither an invoice, nor a part, nor an invoice item. In addition, each row is independent from the data where it is stored and does not remember to which record(s) it belongs. Enter the Datastore Model Paradigm Imagine the ability to denormalize a relational structure yet not affect efficiency. Imagine describing everything about the business objects in your application in such a way that using the data becomes simple and straightforward and removes the need for a complete understanding of the relational structure. Imagine that the result of queries is an object that knows where it is stored and its relationship to other objects. These are some of the goals of the Wakanda datastore model. In the Wakanda datastore model, a single datastore class can incorporate all the elements that make up a traditional relational database table but can also include calculated values, values from related parent entities, and direct references to related entities and entity collections. When a Wakanda datastore model is complete, a query returns a list of entities called an Entity Collection, which fulfills the role of a SQL querys row set. The difference is that each entity knows where it belongs in the data model and understands its relationships to all other entities. This means that a developer does not need to explain in a query how to relate the various pieces of information nor in an update how to write modified values back to the relational structure.
Page 2 of 61
Fig 2
Notice that each datastore class (Invoice, InvoiceItem, and Part) has been filled out with every logical property and that each one reflects a complete picture of the entity and all its related entities. Invoice now contains an attribute (invoiceItems) that represents all the related InvoiceItems. InvoiceItem now contains an attribute (itemPart) that represents the corresponding part as well as all the attributes from the parent Part entity along with a calculated attribute (extended) that determines the extended cost. Singular and Plural In a traditional database, a table has only one name. Many times developers name a table in the plural. For example, a typical name for a table might be Cities or Companies. Later during development, it often becomes necessary to refer to a single record in a table, particularly in association with relationships between tables. Sometimes developers name tables in the singular. For example, City or Company but then later it becomes necessary to refer to multiple records by some name. Wakanda addresses this need by providing a singular name for each datastore class and a plural name for entity collections of that class. For example, a class might be named City or Company while a collection of entities in those classes would be Cities or Companies. This duality helps clarify the relationships between datastore classes and provides a more natural set of expressions when referencing an entity and an entity collection. In the above diagram (Fig 2) note the itemInvoice attribute in the InvoiceItem class. This attribute is of type Invoice (note the singular) and makes it clear that it references a single entity of the Invoice class. Wakanda Terminology Throughout the rest of this document we will refer to Wakanda items differently than relational database items so as not to confuse the two. Here is a brief list of the terminology used in Wakanda and the closest relational database equivalent.
Wakanda RDBMS Notes
Page 3 of 61
Solution
Datastore Class
Table
Datastore Entity
Record or Row
Entity Collection
Attributes
Fields or Columns
Storage Attribute
Field
Calculated Attribute
Calculated Field
Relationship
Dependent
A Wakanda solution is a collection of projects, each of which contains a single datastore model. A solution can contain any number of projects, all of which can be accessible at the same time. A Wakanda project is a design time concept that houses a single datastore model along with the other files that make up a single Wakanda application. A Wakanda application is the runtime entity of a single Wakanda project. A complete set of all datastore classes for a single Wakanda project is referred to as a datastore model and is roughly equivalent to the relational concept of a single database structure. Datastore model is the top-level term for a Wakanda applications datastore and incorporates all the datastore classes for the project/application. The datastore model is sometimes referred to as the model. The Wakanda datastore class is similar to a database table except a datastore class can be inherited and can contain many other types of properties (attributes & methods). In this document and elsewhere, datastore classes are sometimes referred to as classes. In traditional relational databases a record is the logical unit of storage within a table that holds a value for each field in the table. In a SQL database, a row is one of the results of a query, which may not correspond directly to a record. Since a Wakanda datastore entity can represent any level of denormalization without its accompanying shortcomings, it fulfills both of these roles. In this document and elsewhere, datastore entities are often referred to as entities. These two concepts are only roughly similar. Whereas a row set is a list of rows, each of which contains values in columns an entity collection is a bundle of entities (objects), each of which contains a complete picture of the objects situation in the data. The 4D concept Selection is more similar to an entity collection except denormalization has its typical drawbacks on a relation database. A field is the smallest storage cell in a relational database. It is sometimes referred to as a column. However, the term column is more often associated with the results of a query where it may or may not be the value of a field. In Wakanda the collective term attribute is used to describe fields where information is stored as well as several other items that can return a value. A storage attribute (sometimes referred to as a scalar attribute) is the most basic type of attribute in a Wakanda datastore class and most directly corresponds to a field in a relational database. A storage attribute holds a single value for each entity in the class. A calculated attribute doesnt actually store information. Instead, it determines its value based on other values from the same entity or from other entities, attributes or methods. When a calculated attribute is referenced, the underlying calculation is evaluated to determine the value. Calculated attributes may even be assigned values where user-defined code determines what to do during the assignment. A scalar attribute is a collective term to denote any attribute that returns a single value. All storage and most calculated attributes are scalar. Relation attribute is a collective term that exposes a relationship between two datastore classes and can be either N->1 or 1->N. Since they are attributes, they can be named and therefore become available as properties of the corresponding class. A relation attribute can result in an entity or an entity collection. A primary relation attribute is not dependent upon another relation attribute. Primary relation attributes are created by adding an attribute to a datastore class that refers to another class and either creates a new relationship between the classes or reverses the path of an existing N->1 primary attribute. Only primary relation attributes can be designated as a composition relationship. A dependent relation attribute is one that is reliant on another relation attribute
Page 4 of 61
in the same datastore class. Denormalized Field An alias attribute builds upon a relation attribute. Once an N->1 relation attribute is defined, any of the attributes within the parent class can be directly referenced as attributes within the child class. The result is what appears to be denormalized data without the overhead of duplicating information. Alias attributes can reference any available attributes further up the relational tree. A general term for either an attribute or a method associated with a datastore class. Two datastore classes are associated via a relationship, which is an artifact of a datastore model. A relationships existence results in either one or two relation attributes, one in each of the related classes. Relation attributes are named and become a property of their respective classes. A special relationship that allows for bundling child entities with parents. A collective term for a discreet portion of programming in Wakanda. Calculated attribute methods, class methods and event methods are all examples. In addition to attributes, each datastore class may also include methods. There are three different scopes to datastore class methods: Class, Collection, and Entity. A method that substitutes programming behavior in place of standard behavior for an attribute. There are four aspects to calculated attribute methods: On Get, On Set, On Sort and On Query. A method triggered by either a datastore class event or an attribute event. These methods are sometimes referred to as simply events. Any of several built-in trigger points in a Wakanda datastore model. Events are available for both attributes and datastore classes. Wakanda supports five different events: On Init, On Load, On Set, On Validate, On Save, On Remove. Similar to the like-named concept found in object oriented programming, a datastore class can inherit from another. The newly defined (derived) class may add additional properties or hide inherited properties. Datastore class inheritance may be defined by a query stored in a projects datastore model. A query in a traditional database typically returns a row set, which can be thought of as a grid of values (rows and columns). A Wakanda query returns an entity collection, which is a bundle of entities, each of which remembers where it exists in the data. The Wakanda term datastore is used to reference a single applications set of data.
Property Relationship
Composition Relationship Method Datastore Class Method Calculated Attribute Method Event Method Events
Inheritance
Query
Query
Datastore
Database
Datastore Classes In Wakanda a datastore class represents several things: it is the blueprint or template used when defining entities of its type, it is a named object with several attributes and methods, it is a visual construct used in the Wakanda Datastore Model Designer, and it is the object to which you add events, methods, and other properties. Each class has one attribute that is unique and represents its key. Commonly, the key attribute for a datastore class is set to generate an auto sequence value so that each entity has a unique key as it comes into being. Developer Preview Note: There will be a datastore class setting called scope that controls whether the class is available to the WAF or is only available in code running under Wakanda server. Developer Preview Note: Future versions of Wakanda will support multi-attribute keys.
Page 5 of 61
Entities An entity is a reference to a single instance defined by a particular datastore class. Entities are the basic unit in a Wakanda application. When an entity reference is obtained by means of an entity collection, it retains information about the entity collection. Entity Collection An entity collection is a group of entity references of the same type that may or may not be sorted into a particular order. Normally, an entity reference only appears once in an entity collection. However, if an entity collection is sorted, it is possible that a single entitys reference be a member of the entity collection more than once. Storage Attribute A storage attribute is equivalent to a field in a relational database. Values assigned to a storage attribute are stored as part of the entity when it is saved. When a storage attribute is accessed, its value comes directly from the datastore. Storage attributes are the most basic building block of an entity and are defined by name and data type. There are several different data types in Wakanda including BLOB, bool, byte, date, duration, image, long, long64, number, string, uuid, and word. In addition, all datastore classes become a data type in a Wakanda application. Attribute Indexes Wakanda allows you to index storage attributes. In Wakanda there are four different choices for index types: B-tree, cluster, keywords, and auto. B-tree is a general-purpose index that works well in most circumstances and is the most commonly chosen type. Cluster is an index that is best used on attributes with low cardinality, that is, when there are fewer unique values for an attribute. The keyword index type is used for large quantities of text and provides quick searches for individual words in the text. If you choose Auto, Wakanda will choose the index type (either B-tree or Cluster) based upon the data at the time the attribute is indexed. For classes with no saved entities, Auto will choose a B-tree index for all attribute types accept Boolean, which will be set to Cluster.
Custom Data Types Any of the existing storage attributes can become the basis for new data types that encapsulate length controls, pattern matching and default formats. Once created, a custom data type is available for any attribute. Developer Preview Note: Custom data types are not yet available in Wakanda.
Page 6 of 61
Calculated Attribute A calculated attribute is a named property with a data type that masks a calculation. At the very minimum, a calculated attribute requires an On Get method that describes how its value will be calculated. When an On Get method is supplied for an attribute, Wakanda does not create the underlying storage space in the datastore but instead substitutes the methods code each time the attribute is accessed. If the attribute is not accessed, the code never executes. A calculated attribute can also implement an On Set method, which executes whenever a value is assigned to the attribute. The On Set method describes what to do with the assigned value usually redirecting it to one or more storage attributes or in some cases other entities. Just like storage attributes, calculated attributes may be included in queries. Normally, when a calculated attribute is used in a Wakanda query, the attribute is calculated once per entity examined. In many cases this is sufficient. However, calculated attributes can implement an On Query method that substitutes other attributes during the query. This allows calculated attributes to be queried quickly by redirecting searches to other attributes, including storage attributes that may already be indexed. Similarly, calculated attributes can be included in sorts. When a calculated attribute is used in a Wakanda sort, the attribute is calculated once per entity examined. Just like in queries, this is sufficient in many cases. However, calculated attributes can implement an On Sort method that substitutes other attributes during the sort, thus increasing performance.
Fig 3.1
ID, firstName and lastName are standard storage attributes. fullName is a calculated attribute with an onGet method as shown below:
onGet:function() { return this.firstName + ' ' + this.lastName; }
Page 7 of 61
Whenever the fullName attribute is accessed, Wakanda evaluates the expression and returns the resulting value. Since this code becomes part of the attributes definition, it need not be referenced again in the application. An implementation of the On Set calculated attribute method for fullName might be:
onSet:function(value) { var names = value.split(' '); this.firstName = names[0]; this.lastName = names[1]; }
In this code, value is the parameter that receives the value assigned to the fullName attribute and is subsequently portioned out and assigned to the storage attributes firstName and lastName. On Sort and On Query calculated attribute methods behave differently. Instead of simply performing an action, they return a string that is substituted during the operation. For example, the On Sort method for fullName might be:
onSort:function(ascending) { if (ascending) return 'lastName, firstName'; else return 'lastName desc, firstName desc'; }
The On Sort method receives a single Boolean value that describes whether the sort is ascending or descending. Using that parameter, the sort can be replaced with a string representation of other attributes. Notice that in the example above, lastName is sorted before firstName: the fullName attribute is calculated using firstName + ' ' + lastName. This illustrates that calculated attributes do not need to sort the same as the values they return. Similar to the On Sort method, the On Query method returns a string that represents the redirected query. The On Query method receives two string parameters, the comparison operator (e.g. >, >=, etc.) and the compared value. It is the job of the On Query method to use these two values and construct a string that represents a substituted query fragment. Although more sophisticated than the other calculated attribute methods, the On Query usually follows a pattern that responds to the various kinds of comparison operators. Consider the following method:
Page 8 of 61
onQuery:function(compOp, valueToCompare) { var result = null; var pieces = valueToCompare.split(' '); //break into array var fname = pieces[0]; var lname = ''; //not sure they provided a full name if (pieces.length > 1) //so check lname = pieces[1]; //2nd piece provided if (lname == '') { //only one piece was supplied if (compOp == '=') { //we'll take to mean special case //indicating very broad query result = '(firstName = "' + fname + '"'; result += ' or lastName = "' + fname + '")'; } //if Else //we'll take this to mean comparison to lastName result = 'lastname ' + compOp + '"' + fname + '"'; } else { //two pieces were supplied switch (compOp) { case '=': //since no 'break' runs next case case '==': result = 'firstName ' + compOp + '"'+ fname + '"'; result += ' and lastName ' + compOp +"'"+lname + '"'; break; case '!=': //since no 'break' runs next case case '!==': /* could use this but not as fast result = "(firstName "+ compOp +"'"+fname+"'"; result += "and lastName "+ compOp +"'"+lname+"')"; instead use the code below */ result = 'not ('; result += 'firstName '+compOp.substr(1)+ '"'+fname+'"'; result += 'and lastName '+compOp.substr(1)+ '"'+lname+'")'; break; case '>': //all 4 handled in the case below case '>=': case '<': case '<=': var compOper2 = compOp[0]; // get the first char result = '(lastName = "' + lname + '" and firstName ' result += compOp + '"' + fname + '")'; result += 'or (lastName ' + compOper2 + '"' + lname+ '")'; break; } //switch } //else return result; }
Notice that in all cases this function returns the string variable result. Although this method may seem somewhat daunting, this code is a realistic example of how you might handle the fullName calculated attributes On Query method and it does quite a lot. This example handles queries on fullName where only one name part is supplied (e.g. Smith) as well as full names (e.g. John Smith). In a query with a single part, it provides for a search on both lastName and firstName when the = operator is supplied. When using a not equals operator (!= or !==) it substitutes a faster query that respects the operator used (see the comments). When a full name is
Page 9 of 61
supplied it expects the values to be in first name, last name order. It would be fairly easy to augment this code such that it handled full names in reverse order (e.g. Smith, John). Primary Relation Attribute A relation attribute provides access to other entities. Relation attributes can result in either a single entity (or no entity) or an entity collection (0 to N entities). It is important when discussing relationships between datastore classes that we indicate which end of the relationship represents the many (or N) and which represents the one (1). Sometimes developers refer to this as a parent/child relationship where there is one parent entity and multiple child entities. In other cases the relationships between datastore classes is described using terms belongs to and has many. Regardless of how we choose to discuss relationships, the point of view used in the discussion is important. For example, consider the following partial Wakanda structure:
Fig 4
According to the single relationship in this model, there is at most one Company for each Project. Conversely, for each Company there is any number of related Projects. From a single projects point of view, there may or may not be one Company entity, considered the client. From a single Company, there can be any number of Projects, including none. When you define a relation attribute in Wakanda, you usually start by creating an attribute in the many or child class, naming it and choosing as its type another datastore class. Doing so creates a relationship between the two classes. In the above diagram the theClient attribute in the Project class was added first. When this attribute was created, Wakanda automatically created the reciprocal attribute in Company and gave it a default name. In the above example, the reciprocal relation attribute was renamed companyProjects. It is possible to remove the 1->N attribute from the parent class if desired. The resulting relation attribute (theClient in the above example) in the child datastore class references a single entity in the parent. The reciprocal relation attribute (companyProjects in the example above) in the parent class references an entity collection in the child. Typically you name each attribute in a way that describes it logically from the containing class's point of view. So, in the example above we named the relation attribute in the Project class theClient in order to indicate that the related company is the client for this project. In the Company class, the relation attribute is named companyProjects (notice the use of the plural) and when accessed results in an entity collection of related projects. In many cases, naming the attributes in a useful way is the only challenging task in relating datastore classes.
Page 10 of 61
Both theClient and companyProjects in the above diagram are primary relation attributes and represent a direct relationship between the two entities. Below you will see how to build on these attributes using dependent relation attributes. Alias Attributes Once a relationship exists between datastore classes, it becomes simple to add new attributes to child classes that refer to parent or even ancestral class attributes. Referred to as alias attributes, these properties store no data, but provide the convenience found in a denormalized relational structure. Consider the following partial Wakanda model:
Fig 5
Notice that the InvoiceItem class has two attributes, partCost and partName, each of which refers to a storage attribute in the related class Part via the relation attribute itemPart. Because Wakanda traverses a specifically named relation attribute to determine each alias attributes value, there is no ambiguity when it is accessed. Any number of alias attributes can be included this way. The result is an InvoiceItem datastore class that incorporates all the pieces of information needed by the application.
Page 11 of 61
Dependent Relation Attributes and a More Sophisticated Example Wakandas relational abilities dont end with primary relation attributes. Wakanda also allows relation attributes that are dependent on other relation attributes. Called dependent relation attributes, they refer to other datastore classes via a path that always starts with another relation attribute in the same datastore class. So, consider the following Wakanda model:
In the diagram above we show a variety of different kinds of attributes and have added color for clarity. The attributes are color-coded as follows: - Storage attributes are white - Calculated attributes are blue - Primary relation attributes are pink - Alias attributes are green - Dependent relation attributes are yellow When a dependent relation attribute is created, instead of a data type, you indicate a path. The path is composed of a list of other relation attributes separated by periods. Just like primary relation attributes, a dependent relation attribute can result in either a single entity or an entity collection. Consider the client attribute in Invoice. This dependent relation attribute uses as its type the path of invoiceProject.theClient. It references the projects parent company and in use would return a single entity. Notice that the path starts with a reference to another attribute in the same entity (invoiceProject) and the path describes a chain of N->1 relation attributes. When a dependent relation attributes path is composed of only N->1 relation attributes, the result is a single entity.
Page 12 of 61
Consider the allProjectInvoices attribute in Company. This dependent relation attribute is derived from the Company companyProjects attribute (which alone would result in an entity collection of related projects) combined with the Project relation attribute projectInvoices. When the allProjectInvoices attribute is accessed it will result in an entity collection of all invoices for all projects for a given Company entity. When a dependent relation attributes path includes at least one 1->N relation attribute, the result is an entity collection. Consider the invoiceParts attribute in Invoice. This relation attribute uses the Invoice invoiceItems relation attribute combined with the InvoiceItem itemPart relation attribute. The path then includes both a 1->N and an N->1 relation attribute. In use, the invoiceParts relation attribute results in an entity collection of all parts used on an invoice. The resulting entity collection would contain each part once, regardless of whether the part was used on more than one invoice item. Consider the purchasedBy relation attribute in Part. This attribute uses the Part invoiceItems attribute, combined with the InvoiceItem itemInvoice attribute combined with the Invoice client attribute. Following the logic through you will see that the purchasedBy attribute results in all companies that have any invoices that sold the part. Conversely, the allPartsPurchased attribute in Company will result in all parts on all invoices for the company. The incorporation of sophisticated relation attributes at the datastore class level allows Wakanda applications to function without the need for explicit joins. Notice that Invoice has an alias attribute named clientDiscount that results in the discount storage attribute in Company. clientDiscount uses the client dependent relation attribute in Invoice which references the invoiceProject attribute which references the related Project. Thus, alias attributes can reference values using any valid N->1 relation attribute in the same entity including dependent relation attributes. There are a number of ways to achieve the same results in a Wakanda datastore model. For example, the clientDiscount attribute in Invoice could have been implemented using invoiceProject.theClient.discount. Similarly, the allPartsPurchased attribute in Company could have been implemented using companyProjects.projectInvoices.invoiceItems.itemPart. Although the entire path would be clear, the length of the path might become unmanageable. It is often easier to use intermediary relation attributes for convenience. Regardless of how you implement alias or relation attributes, if they traverse the same relational path, they are equivalent in efficiency. The main difference is in the readability of the datastore model. The Wakanda model in Fig 6 also incorporates the calculated attributes invoiceTotal in Invoice and extended in InvoiceItem. The invoiceTotal onGet method could be as simple as:
onGet:function(){ return this.invoiceItems.sum('extended'); }
Page 13 of 61
Entity Collections and Attributes So far we have seen how to incorporate very sophisticated relationships between datastore classes and then use relation attributes to traverse the relational web of the data. But in Wakanda, the use of attributes is not limited to individual entities. Attributes are also used in conjunction with entity collections. When a scalar attribute is used with an entity collection, the result is an array of values each of which is the type of the scalar attribute. When a relation attribute is used in conjunction with an entity collection, the result is a new entity collection made up of all related entities. For example, consider the partial Wakanda model in Fig 5. Given an entity collection of invoice items, the attribute itemPart will project a collection of all parts for all invoice items in the original entity collection. This ability allows you to traverse relationships on multiple entities at a time (more on this later). Inheritance Although common in object-oriented programming, inheritance is a concept not usually found in the data management world. However, there are some data management situations that could benefit from inheritance. Consider the following partial relational database structure:
Fig 7
This normalized structure describes People (inside and outside of the organization), Employees (only those inside the organization) and Salespeople (a specific subset of employees). Although shown in the above diagram using N->1 relationships, our business rules dictate that each salesperson represents exactly one employee, and each employee represents exactly one person. A salesperson cannot exist without a reference to an employee and an employee cannot exist without a reference to a person. Furthermore, employee records derive their key value from the corresponding person record and salespeople derive their key from the corresponding employee. In less formal terms, we can say that a salesperson is a kind of employee, and that an employee is a kind of person. Later when the data management system needs to address salespeople it can do so by operating on the Salespeople table but of course must join with the other tables to access a complete picture of the data.
Page 14 of 61
Alternately, we could denormalize the data and combine all fields into one table, but then we would have to add extra fields that categorize the various record types of employees and salespeople and manage these types. Furthermore, if we combine all the fields into one table, each query that needs to operate on say Salespeople would have to include a means to identify only records of type salesperson. This places a greater burden on down stream business logic to identify the different types of people and their corresponding significant fields. This situation is an example that is better addressed through inheritance. In Wakanda you do this by extending a datastore class to create a new class. So, in Wakanda the model can become:
Fig 8
Developer Preview Note: In this version of Wakanda, you cannot yet add storage attributes to derived classes and are restricted to adding relation attributes. In the above Wakanda model, the Salesperson class inherits from the Employee class, which inherits from Person. Later during runtime each employee entity is composed of all properties from both Employee and Person and each salesperson entity is composed of properties from all three datastore classes. This includes all attributes and all methods associated with the extended classes. When a Wakanda application needs to work with just employees or just salespeople, it doesnt need to explicitly refine each query to filter out unneeded entities. Furthermore, when a derived class is created, Wakanda manages the underlying data and generates the inherited entities when needed. For example, if a new Salesperson entity is created and saved, Wakanda automatically manages the creation of the corresponding new Employee and Person. Furthermore, if a new attribute is added to the Person class, it becomes available in all derived datastore classes. Public, Protected and Private Attributes in Wakanda can have a scope of public, protected, or private. If an attribute is private, it can only be accessed by code attached to its own class. A private attribute is only available within the corresponding classs methods, the classs events or a method associated with calculated attributes of the class. If a private attribute is accessed outside of these three contexts, including derived classes, Wakanda will generate an error. The protected scope is similar to
Page 15 of 61
private but allows access to the attribute in derived classes. The public scope is the default and allows access to the attribute from anywhere. Developer Preview Note: There will be another scope that specifies that the attribute is only available in server-side code. Hiding Inherited Attributes In some instances, inherited attributes are not appropriate for specific derived datastore classes. Consider the model in Fig 8. Lets assume that in our organization Employee and Salesperson dont have individual work phone numbers and that we dont want to include workPhone as an attribute in either Employee or Salesperson. Wakanda allows inherited attributes to be hidden in derived classes. In the Wakanda Datastore Model Designer, you do this by clicking the attribute in the derived class. The removed attribute appears crossed out in the panel shown below and is not available in derived entities.
Fig 8.1
Restricting Queries and Inheritance Normally Wakanda manages inheritance classification. That is, if a derived entity is created, Wakanda assures that it qualifies as a derived type. But this behavior can be overridden using a restricting query. When a restricting query is provided for a derived datastore class, the query becomes the basis for the inheritance classification. This allows the developer direct access to the entitys classification and provides a way to promote and demote entities up and down the inheritance chain.
Page 16 of 61
Fig 9
Typical of many data applications, this model denotes datastore classes used in support of user interface pick-list mechanisms. Choice is the base class for the other four. Keyword, Status, City, and State are all derived classes. Each of the derived classes includes a restricting query in the form of:
Notice that the category attribute is compared to the derived classs name. In use, each of the derived classes can be used without the need to include a filtering query at runtime. Should we need to add specific attributes to a derived class, we can do so without affecting the other types. However, if we add new attributes to the base class Choice, for example sortOrder or abbreviation, these attributes become available in all derived datastore classes. We can automate the classification of derived entities by adding a method for the On Init event of the Choice class as follows:
onInit:function() { var myType = this.getDataClass().getName(); this.category = myType; }
The On Init event executes when an entity is created (more on this later). The above code determines the classs name and assigns it to category, thus auto categorizing the entity. Since all derived datastore classes inherit this behavior, the category is set when any of the derived types are created. If we create an entity in the base type Choice, it will be given a category of Choice
Page 17 of 61
but this could be changed before saving the entity. Also, notice that the derived classes State and City have a relationship between them. Derived classes can be related in ways that their corresponding base classes are not. Of course, the inheritance scheme above implies that a single entity can be only one type such as Keyword, Status, City, State, or Choice. But Wakanda inheritance allows a single entity to be more than one inherited type. Consider the following model:
Fig 9.1
In this model, both Student and Teacher are derived from Person. Student is dependent upon the following restricting query isStudent = True while Teacher is dependent upon the restricting query isTeacher = True. Since these two values are independent, an entity in the Person class may be both a Student and a Teacher. Other Uses of Restricting Queries Restricting queries can also be used apart from inheritance and can be added to any datastore class. Three system-wide values are available in restricting queries that represent the current user, the current date, and the current time. Using these values datastore classes can include a filter at the model level, removing the need to respect these values in downstream logic. For example, a Sale class can have as its restricting query logic that restricts sales to the current user. The advantage of doing so will ensure that no user can access sales of another user, regardless of the manner in which the information is retrieved. Datastore Class Methods In addition to attributes, datastore classes may also include another type of user-defined property called methods. Methods are discreet portions of programming logic that execute when called. Datastore class methods can have one of three scopes: Entity, Collection, or Class. Each methods scope is defined in the Wakanda model. At runtime, class methods with a scope of Entity become a property of all entities of the class. In this scope, when the keyword this is used in the method it refers to the entity. Entity methods with a scope of Collection become properties of entity collections for the corresponding class. In this scope, when the keyword this is used it
Page 18 of 61
refers to the entity collection. The scope of Collection allows developers to build methods that operate over an entire entity collection. Datastore class methods with a scope of Class are essentially static methods and need no instance of an entity or entity collection to operate. In this scope, when the keyword this is used it refers to the class itself. When you define a datastore class method, you can also indicate the return type using a popup in the Wakanda Datastore Model Designer. Choosing a type from the popup provides information to any mechanism that may call the method. Wakanda does not type check the results of the method against the type chosen in the popup. Scope Example: Entity For an example of a method with the scope of Entity, consider the expanded model from Fig 9.1.
Fig 10
This model describes Student and Teacher, both derived from Person. It includes Course, the entities of which represent a specific subject and semester. Finally, it includes Attendee, each of which indicates that a particular student attended a course and received a specific grade. In this example, a method has been added to the Student class as shown. The method is named getGPA and its scope is Entity. The method is as follows:
Page 19 of 61
This method accepts the parameter year and uses it to query the students attendance for courses taken that year. It then averages the grades of the student. Note that using the keyword this refers to the student entity in which this method is called. The following code fragment shows how the getGPA() method might be called.
var theStudent = Student(321); //returns a single student entity var theGPA = theStudent.getGPA(2011);
Scope Example: Collection For an example of a method with a scope of Collection, we could create a new method that has code identical to the previous example. If we did so, the keyword this would refer to the entity collection and since relation attributes are available on entity collections as well as entities, this.studentAttendance would be just as valid. Instead, lets do something slightly more sophisticated. The following datastore class adds another method to Student called listGPAs and is given the scope of Collection:
Fig 11
Page 20 of 61
}) return results; };
This method starts by declaring results as an empty array. It then uses the built-in Wakanda entity collection iterator forEach to cycle through all entities in the collection. For each entity, it defines an object with two attributes, name and GPA and then appends the object to the array. It fills the GPA for each student by calling the previously defined getGPA function. The result is an array of objects with two attributes each. The following code fragment shows the listGPAs method in action:
var stds2011 = Student.query('studentAttendance.year = 2011'); stds2011.listGPAs(2011);
When executed in a JavaScript file on Wakanda Server, the results look like figure 11.1.
Fig 11.1
Scope Example: Class When a method is given the scope of Class, it can be executed using a reference to the datastore class itself and requires neither an entity nor an entity collection. One good example of this is code that imports data into an empty Wakanda datastore. Consider the following Wakanda model and notice the Company method importCompanies.
Fig 12
Page 21 of 61
This method takes a file object as its parameter. It then opens a text stream to read from the file and reads the first line (/n is a line feed) where it expects to find column titles. If the column titles match, it reads one line at a time from the file and breaks the line into an array of values (\t is a tab). It then creates a new company entity and assigns values to the storage attributes. An example of this method in action might be the following:
var theFile = File('c:/Projects/companies.txt'); Company.importCompanies(theFile);
The importProjects method of the Project class is similar, but must include a mechanism to find and designate the related company entity. So importProjects might be something like this:
importProjects:function(file) { var importStream = TextStream(file, 'read'); var record = importStream.read('\n'); if (record == 'Project ID\tProject\tProjClientID') { while (!importStream.end()) { record = importStream.read('\n'); if (record != '') { var fields = record.split('\t'); if (fields.length >0) { var myClient = Company(fields[2]); var newProject = new Project({ ID : fields[0], name : fields[1], theClient : myClient
Page 22 of 61
This method is similar to the importProjects method except that it looks up the matching company and assigns it to the theClient attribute for each project. This establishes the relationship between the project and company. Notice that in order for this to work, the companies must be imported first so that they can be found during the projects import. But what if we have two datastore classes that are dependent on one another? For example, notice the new attribute in Company named defaultProject.
In a relational database the defaultProject would typically be represented by a foreign key related to the Project ID field. Likewise theClient in Project would be a foreign key to the Company ID field. In this example, if we import companies first, there are no projects to be found. If we import projects first there are no companies to be found. How can we resolve this issue? Wakanda solves this issue by allowing you to assign a key value in place of an entity. Instead of looking up an entity in another class, you can assign the imported foreign key directly to the N->1 relation attribute. For example, the importCompanies method becomes (note the imported text file has an additional column representing the default projects key):
importCompanies:function(file) { var importStream = TextStream(file, 'read'); var record = importStream.read('\n'); if (record == 'ID\tClient\tDiscount\tDefProjID') { while (!importStream.end()) { record = importStream.read('\n'); if (record != '') { var fields = record.split('\t');
Page 23 of 61
if (fields.length > 0) { var newCompany = new Company({ ID : fields[0], name : fields[1], discount : fields[2], defaultProject : fields[3], }); newCompany.save(); } //if } //if } //while } //if importStream.close(); }
Notice that the value in fields[3] is assigned directly to the defaultProject relation attribute. Wakanda converts this to an entity reference and stores the reference as part of the company. Wakanda allows this even though at the time of the assignment the corresponding entity does not exist in the datastore. The importProjects method can be modified to use a similar approach.
importProjects:function(file) { var importStream = TextStream(file, 'read'); var record = importStream.read('\n'); if (record == 'Project ID\tProject\tProjClientID') { while (!importStream.end()) { record = importStream.read('\n'); if (record != '') { var fields = record.split('\t'); if (fields.length >0) { var newProject = new Project({ ID : fields[0], name : fields[1], theClient : fields[2] }); newProject.save(); } //if } //if } //while } //if importStream.close(); }
The only issue this technique introduces is that we may create entity references where no entity exists. If we are unsure that every foreign key value has a matching record in our import file, we may want to locate orphaned entities and set their relation attributes to null. Regardless of how we create entities in Wakanda, if we are assigning values directly to the key attribute of an entity, the auto sequence mechanism is not automatically informed of the largest key value. This implies that we need to reset the auto sequence mechanism so that the next value it produces is not a duplicate value of an existing entity. See the example in Programming in Wakanda Server section for a way to address this.
Page 24 of 61
Class Events and Attribute Events In addition to class methods that are executed when called, Wakanda provides a series of events on both entities and attributes that can trigger specific code to execute. Not to be confused with the On Get, On Set, On Sort, and On Query methods of a calculated attributed, events are available on all attributes and datastore classes.
Class events operate on whole entities and are triggered by particular conditions. The following are datastore class level events:
Description Executes when a new entity comes into being after all auto sequence values are assigned. Executes just after an entity is accessed from the datastore but before it is delivered to the calling routine. Executes when an entity is saved but before the On Save event and can fail the validation if needed. Executes before an entity is saved and can stop the save if needed. Executes before an entity is deleted and can stop the deletion if needed.
Like class events, attribute events run as the result of specific datastore actions. For each attribute event, Wakanda automatically passes a parameter, which is the name of the attribute affected. The following are attribute level events: Event On Init On Load On Set Description Executes when a new entity comes into being but after the entitys On Init event. Executes the first time the attribute is accessed after the entity is loaded. If the attribute is not accessed, this event is not executed. Executes after an attributes value is set. In server-side code, this executes when the value is set. When an attribute is set in client-side code, this event executes when the entity is saved or the method serverRefresh is called, but only for each attribute that was modified.
Page 25 of 61
Executes just before the entitys On Validate event executes and can fail the validation if needed. Executes just before the entitys On Save event executes and can stop the save if needed. Executes just before the entitys On Remove event and can stop the deletion if needed.
Regardless of whether an event method is executing for a datastore class or an attribute, the keyword this refers to the entity. On Init The On Init event executes when an entity is created. In the following code fragment, we are creating a new InvoiceItem. The On Init event attached to InvoiceItem executes before the assignment to the variable theItem.
var theItem = new InvoiceItem(); //create a new item
However, when the On Init event executes any auto sequence assignment has already been made. In our example below, we attach an On Init event to a datastore class where the ID attribute is set to auto sequence:
onInit:function() { this.name = 'my ID = ' + this.ID; }
When an entity is created the name will include the ID since the auto sequence mechanism executes before the On Init event of the entity. The On Init event is the obvious place to initialize an entity. Typically you would assign default values to attributes or in the case of derived classes you may test for which type of entity is being created (see the inheritance restricting query example). The On Init event for attributes is very similar and executes just after the entitys On Init. The main difference is that the event is provided a string parameter that is the name of the attribute. This allows developers to write modular code that can operate on a variety of attributes. Using a combination of the keyword this and the parameter attributeName you can indirectly reference the attribute. The syntax used to reference an entitys attribute by its name is shown below:
onInit:function(attributeName) { this[attributeName] = ''; }
Page 26 of 61
On Load The On Load event executes when an entity is loaded from the datastore. This provides a control point where the entity attributes values can be modified before the entity is delivered to the requesting mechanism. For example, consider the following datastore class:
Fig 14
In this example from Figure 6, a new attribute of type Image has been added to the Part entity. This attribute is intended to display a picture of the part, but in our example application not all parts have photos. The On Load method might look like this:
onLoad:function() { if (this.photo.size == 0) this.photo.setPath('c:/Projects/Docs/WebFolder/NoPhoto.jpg'); }
In this code, the NoPhoto.jpg image file is used for parts that have no photo. The setPath() method is a Wakanda image property. Since this code runs before the entity is delivered, parts without photos will have the default photo when accessed. For attributes the On Load event executes when the attribute is first accessed. To see this in action, consider the following Wakanda Server code fragment:
var var var var myParts = Part.query('ID < 10'); myEntity = myParts[0]; //onLoad for entity executes myName = myEntity.name; //onLoad for attribute executes myName2 = myEntity.name; //onLoad does NOT execute again
The first line of code assigns an entity collection of parts to myParts. Since no particular entity was accessed, the On Load entity event for Part does not execute. The second line of code assigns a specific part to myEntity. Since a specific entity was accessed, the On Load event for the entity is executed but no On Load events occur for the entitys attributes. The third line of code assigns the value in the entitys name attribute to myName. Since a specific attribute was accessed the On Load event for that attribute executes. The fourth line of code will not cause the On Load event for name to execute again because the attribute has already been loaded for the entity.
Page 27 of 61
Note: Even assignments to attributes will cause the On Load event to execute on that attribute. For example:
myEntity.name = 'Part 1';
This code will cause the name On Load event to execute before its On Set event. On Set The On Set event is only available for attributes. For an example of an attribute event consider the following classes from Figure 15.
Fig 17
Notice that the alias attributes partCost and partName in the InvoiceItem class have been replaced with the storage attributes cost and name. In our example application, this is not an instance of denormalization because our business rules state that an invoice item acquires the part name and cost at the time the part is specified and thereafter is independent from the part. The rules indicate that future invoice items can reference the same part with a different name or cost. This is a common application situation where values from a parent entity need to be copied into a related entity. This can be accomplished using the On Set event for itemPart in InvoiceItem. The method might look something like this:
onSet:function(attributeName) { this.name = this.itemPart.name; this.cost = this.itemPart.cost; }
Later, when a part is assigned to itemPart, the values will be copied from the part into the invoice item. For example, consider the following code fragment:
var theItem = new InvoiceItem(); //create a new item var thePart = Part(324); //find part with a key of 324 theItem.itemPart = thePart; //entity is assigned so On Set runs //at this point the new item has copied the name and cost theItem.save();
Page 28 of 61
In an attributes event method, the keyword this refers to the entity, not the attribute. In the case of the On Set event, the method runs after the attributes value has been assigned therefore the attribute itemPart is a valid entity reference during the On Set event code. All entity and attribute events are processed on Wakanda Server. This means that assignments made in code running on the browser will not cause On Set events until the entity is returned to the server by either save() or serverRefresh(). When this happens, only the attributes that have had values assigned will receive On Set events. Attribute events execute even when values are assigned inside of other attribute events. In the above On Set example, if there is an On Set method for either the invoice items name or cost, the events for those attributes are executed. The same is true for datastore class level events; code in one event may cause another event to execute. For example, if the class On Init event contains the following code:
onInit:function() { this.postDate = Date(); }
In this code the assignment to postDate will cause other events to execute, namely the postDate attributes On Load and On Set events. These additional events will complete before the original entitys On Init event. It is not hard to envision a situation where an events code ends up causing the same events code to run again. Wakanda Server manages this by ensuring that each chain of event calls never executes the same event twice for the same entity or attribute. On Validate The On Validate event occurs when an entity is saved but before the On Save event. The purpose of On Validate is to provide a mechanism to test whether an entity is complete and qualified to be saved before proceeding. This can be particularly useful when your On Save event contains code that accesses external services or external files. Without this event, the On Save event may proceed and complete part of its intended function before running into a validation issue, leaving the data in an inconsistent state. It is good practice to put all business validation logic in the On Validate and reserve the On Save for actions on the entity being saved, writing to other entities or external files, calling external services and reporting on the most egregious errors. For example, consider the following structure:
Fig 15
Page 29 of 61
Lets assume that our business rules dictate that a Document entity cannot be saved without a name and content, but when it is saved the content is written to an external file and not stored in the datastore. Our On Validate might be something like this:
onValidate:function() { if (this.name == null) return {error: 3, errorMessage: ' no name specified'}; else if (this.content.length == 0) return {error: 4, errorMessage: ' document contains no content'}; else { var modelPath = getModelFolder().path; var docFolder = Folder(modelPath + 'Documents'); if (!docFolder.exists) try { docFolder.create(); } catch (e) { return {error: 5, errorMessage: ' could not create doc folder'}; } if (docFolder.exists) { if (docFolder.getFreeSpace()/1024 < 100) return {error: 6, errorMessage: ' drive space is low'}; } } }
This code tests if the entity meets some simple business rules (name must be supplied and document must have something in it) and then checks to make sure the folder where documents are to be written exists and that there is a minimum amount of hard disk space. If anything in the validation goes wrong, the validation fails. This event would typically be paired with an On Save event where the document is written to the Documents folder, the path to the document is stored in the path attribute, and the content attribute is cleared and therefore not stored in the datastore. The code to save a document entity might look something like this:
var newDoc = new Document() //some code try { newDoc.save(); //try to save the document } catch (e) { //may be either a validation or save error }
Keep in mind that the save() method may produce errors from either the On Validate event or the On Save event so your code must handle exceptions from both.
Page 30 of 61
An attributes On Validate event executes before the entitys On Validate event and is supplied the attributes name as a string parameter. For example:
onValidate:function(attributeName) { return hasSomeText(this, attributeName); }
Note that returning an object where the error attribute equals zero is the same as returning no error. On Save The On Save event functions similar to the On Validate but only executes if the entity passes validation. If an entity has both On Validate and On Save, then the On Validate executes first. If an entity has no On Validate method then the On Save will execute whenever the save() method is called on an entity. However, the On Save event can still reject the save by returning an error object just like the On Validate. Designers should typically reserve the On Save for work needed in tandem with saving an entity. Consider this datastore model:
Fig 16
The checkForUpdate() method has the scope of Class and the following code:
checkForUpdate:function(theEntity) { if (!theEntity.isNew()) {
Page 31 of 61
theClass = theEntity.getDataClass(); formerEntity = theClass(theEntity.getKey()); changedFrom = ''; (var e in theClass.attributes){ if (theEntity[e] != formerEntity[e]) changedFrom += e + ' : ' + formerEntity[e] + '\r';
} if(changedFrom.length > 0) { new Update({ className: theClass.getName(), entityKey: theEntity.getKey(), changes: changedFrom }).save(); } } }
This class method is a generic routine for recording all changes to an entity. It checks to see if the entity being saved is new. If not, it looks up the same entity in the datastore, which returns a copy of the entity before it was modified. It then cycles through all the entitys attributes testing each one to see if it has been changed and writing the changes to a variable. If there are any changes, it creates a new Update entity to record the modifications along with the class name and key. Incorporating it into the Persons On Save event is as simple as:
onSave:function() { Update.checkForUpdate(this); }
We can even add a new method to the Person class with a scope of entity that returns the corresponding updates. For example, here is the same class with a new method named getUpdates().
Notice that the new method has the scope of Entity. In addition, we have set this methods return type to be Updates, which is an entity collection. The getUpdates() method has the following code:
getUpdates:function() { var className = this.getDataClass().getName();
Page 32 of 61
var qString = 'entityKey = :1 AND className :2 order by ID desc'; var theUpdates = Update.query(qString, this.getKey(), className); return theUpdates; }
To use this new method you would call it as a property of an entity. The result would be an entity collection of updates that show the modifications in reverse order of creation so that the latest modification comes first. For example:
var somePeople = Person.query('lastName = "Brown"'); var onePerson = somePeople[0]; var theUpdates = onePerson.getUpdates(); //returns sorted collection
Of course this code assumes that we have at least one person with a last name of Brown. On Remove The On Remove event executes just before an entity is to be deleted. It can be used for a variety of purposes including cleaning up related entities and validating the deletion. To reject the deletion, you pass a specific object as the result of the function. Take the Part class from Fig 14 and the following On Remove event method.
onRemove:function() { if (this.invoiceItems.length != 0) return {error: 1, errorMessage: ' Part in use by invoice items'} }
In this example, our business rules dictate that we cannot delete a part that is still in use. This method rejects the deletion of a part that is still being referenced by an invoice item. You reject the deletion by returning an object with at least one property named error. The error property should be a number greater than zero. If it is greater than zero, then the deletion will not occur. The number you choose is up to you. Optionally you may include the errorMessage property, which is a string to describe the error. But what if we wanted to just delete the corresponding invoice items when the part is deleted (unlikely, but lets use it for an example)? Our On Remove code might look something like this:
onRemove:function() { if (this.invoiceItems.length != 0){ try { ds.startTransaction(); this.invoiceItems.remove(); ds.commit(); } catch (e){ var theError = { error: 2, errorMessage: ' Related invoice items could not be deleted' }; ds.rollBack;
Page 33 of 61
return theError; } } }
Notice the use of the trycatch error-trapping block to attempt to delete the related invoice items. In this example we start a transaction before we attempt to delete the related entities. If deleting the invoice items generates an error, then the line ds.commit()will be skipped and control will pass directly to catch where we roll back the transaction. In this way, the invoice items deletion is an all or nothing mechanism. If there is an error our parts deletion generates an error as well. Composition Relations A composition relation is one where related child entities are expressed as an array of entities and bundled with their parent. A composition relationship is described through a 1->N primary relation attribute. A checkbox in the relation attributes properties tab allows you to specify it to be a composition relationship between datastore classes. Only 1->N primary relation attributes qualify to be marked as composition. Programming in Wakanda Server As you may have noticed, Wakanda Server uses JavaScript as its programming language. This allows developers to utilize a single language for both browser-based and server-based code. The Wakanda Server JavaScript framework provides a variety of services to manipulate datastore classes, entity collections, entities, and many other server-side constructs. When code runs on Wakanda Server it does so in the context of a Wakanda application. In this context, Application is a global object so all of its properties are available with no prefix. One of the application objects properties is ds, which represents the datastore of the application. When programming in Wakanda server, many references begin with ds. Datastore Classes All datastore classes are available as a property of ds. For example,
var personClass = ds.Person;
This code assigns to personClass a reference to the Person datastore class. In most cases when referencing a datastore class, you need not include the ds. For example,
var personClass = Person;
Page 34 of 61
A list of all datastore classes is also available as a property of ds. For example,
function resetAllAutoSequences(){ for (var e in ds.dataClasses){ var theClass = ds.dataClasses[e]; var maxID = theClass.all().max('ID'); class.setAutoSequenceNumber(maxID + 1); } }
This code resets all auto sequence values for all datastore classes, provided the key for each class is left at the default name of ID. The method setAutoSequenceNumber is a standard method of datastore classes. Datastore Class Attributes Datastore class attributes are available as properties of their respective classes. For example:
var lastNameAttribute = Person.lastName; var firstNameAttribute = Person['firstName'];
This code assigns to lastNameAttribute and firstNameAttribute references to the lastName and firstName attribute of the Person class. This syntax does not return values held inside of the attribute, but instead returns references to the attributes themselves. Note the two different ways to reference class attributes. You can cycle through all the attributes of a datastore class like this:
var var var for personClass = Person; //reference to class, not a collection allPeople = Person.all(); //reference to entity collection attribCount = {}; //empty object (var e in personClass.attributes){ var attribute = personClass.attributes[e]; attribCount[attribute.getName()] = allPeople.count(attribute);
This code first gets a reference to the Person class and then a reference to an entity collection of all people. It then creates an empty object that will house the non-null counts of each attribute in people. It then cycles through all the attributes in the Person class and uses the name of the attribute to implicitly add a new property for each attribute that stores the value of the count. Note that the count() function can take either a reference to an attribute or a string name of an attribute.
Page 35 of 61
If we were to run this directly on Wakanda Server and display the attribCount object, we might see the following result:
Attribute Names and Attribute References Many Wakanda methods accept string references as attribute names and can also accept attribute references. For example, consider the following:
var localPeople = Person.query('zipCode = 95113'); var lastNames = localPeople.toArray('lastNames');
This code defines an entity collection of people in the 95113 zip code. It then produces an array of last names. In place of a string value representing an attribute, you may also use an attribute reference like this:
var lastNameAttrib = Person.lastName; //returns reference to attribute var localPeople = Person.query('zipCode = 95113'); var lastNames = localPeople.toArray(lastNameAttrib);//uses attribute ref
Creating Entities New entities can be created using the datastore class name. For example:
var newPerson = new Person();
This code creates a new entity of the Person datastore class. Alternatively, the same entity could be created using the following syntax:
var newPerson = Person.createEntity();
Page 36 of 61
Of course, the entity is still to be saved. A more complete example might look like:
var newPerson = new Person(); newPerson.first = 'Fred'; newPerson.last = 'Williams'; newPerson.save();
You need not even assign the entity to a variable if all you want to do is create a new one:
new Person({first : 'Fred', last : 'Williams'}).save();
Or similarly:
Person.createEntity({first : 'Fred', last : 'Williams'}).save();
Entity Collections Entity collections are usually created using a query or returned from a relation attribute. For example:
var brokers = Person.query('personType = broker');
This code returns into brokers all the people of type broker. To access an entity of the collection, use syntax similar to accessing an element in an array. For example:
var theBroker = brokers[0]; //Entity Collections are 0 based
The entity collection method orderBy() returns a new entity collection according to the supplied sort criteria. For example:
brokers = brokers.orderBy('name'); // returns a sorted collection
This code returns into brokers the same collection of person entities but sorted by name. Alternatively, you can use a relation attribute to return an entity collection. For example:
var brokers = Person.query('personType = broker'); var brokerCompanies = brokers.myCompany;
Page 37 of 61
This code assigns to brokerCompanies all related companies of the people in the entity collection brokers assuming the relation attribute myCompany. Using relation attributes on entity collections is a powerful and easy way to navigate up and down the chain of related entities. Queries and Finds A Wakanda query returns an entity collection. An entity collection may have no entities. A Wakanda find returns a single entity, the first one found if more than one matches the criteria or null if no entities match the query. Both use the same syntax and both can be very simple. For example:
var lowParts = Part.query('ID < 100');
This query returns into lowParts an entity collection of all parts with ID less than 100. Notice that query() is a method of the Part datastore class. When a query is performed on a datastore class, all entities of that class are included in the query. But consider the following code:
var lowParts = Part.query('ID < 100'); var lowWidgetParts = lowParts.query('Name = Widget');
The first line returns an entity collection that is queried further in the second line. A query performed on an entity collection only considers entities that are included in the collection. Thus, lowWidgetParts would be an entity collection of all parts named Widget with an ID of less than 100. Notice that the value Widget is not in quotes in the query string. This is allowed provided the value contains no spaces or other characters that would cause ambiguity. For example, we would need to write the following if the parts name is Large Widget:
var lowWidgetParts = lowParts.query('Name = "Large Widget"');
Of course, queries can be more complex than this. But since datastore classes can include alias and relation attributes, even complex queries are easy to write and understand. For example, using the Wakanda model from Fig 6 consider the following query examples:
Project.query('ID < 1000 and clientName = "Brown"');
This finds all projects with an ID less than 1000 where the clients name is Brown. Notice the use of the alias attribute clientName, which behaves as any other attribute in Project. Wakanda manages the relation between the two classes. Similarly, we can use relation attributes in queries. For example:
Invoice.query('client.discount < 30');
This query returns all invoices where the clients discount is less than 30. The relation attribute client represents the parent projects parent company. Notice how simple this query is to its SQL equivalent where we would have to incorporate two joins in our statement.
Page 38 of 61
Even more interesting are queries using 1->N relation attributes. For example,
Company.query('name = "B*" and companyProjects.name = "Green"');
This query returns all companies that have a name that starts with B and have at least one related project with the name of Green. The * character is the Wakanda wildcard character. When you include more than one reference to a 1->N relation attribute in a query then the conjunctions OR and AND control whether the criteria is applied to the same related entity or independently. For example:
var qString = 'companyProjects.name = "Green"'; qString += ' AND companyProjects.ID < 100'; Company.query(qString);
This query returns all companies that have at least one project with the name of Green and an ID below 100. One single related entity must meet both criteria for the company to be included in the resulting collection. If we replace the AND with OR then the query finds all companies that have at least one project with the name of Green or another project with an ID less than 100. Wakanda queries also allow for keyword searching. For example:
var myDocs = Document.query('docText %% "Project"');
This query will return into myDocs an entity collection of documents where the docText attribute contains the individual word Project. Note the keyword comparison operator %%. This type of query will work correctly on the contents attribute regardless of whether it was indexed or not. But if we add a Keywords index to the contents attribute, the query will operate much faster. Using keyword queries is different from using a simple contains query. For example, consider this similar query:
var myDocs = Document.query('docText = "*Project*"');
This query will not only find documents with the word Project, but also those with the word Projection. In fact, it will locate all documents where the string of characters is found anywhere in docText. Wakanda interprets query strings from left to right. This means that neither ANDs nor ORs take precedence. Use parenthesis in queries to control the order of execution. For example:
var qString = '(cost > 50 and cost < 100) or name = Widget' var myParts = Part.query(qString);
The above code might return a different entity collection than the following:
var qString = 'cost > 50 and (cost < 100 or name = Widget)'
Page 39 of 61
Developer Preview Note: The Wakanda wildcard is currently @ not * and will be changed before release. Placeholder Query Syntax As an alternative, queries can be written with placeholder syntax. For example,
Company.query('name = :1', name);
Note :1 in the query string. Each placeholder is included as a colon followed by a digit. Up to nine placeholders can be included in the query string and then supplied as parameters to the query method. There are several advantages to this syntax: 1) You dont have to build a complex query string that includes values from other variables or objects. 2) You dont have to put quotes around string values that have spaces or other characters. 3) This syntax is necessary if you wish to use an entity as a value in the query (as opposed to one of its attributes). For an example of using an entity as a value in a query consider the following:
var myPart = Part(100); var myPartItems = InvoiceItem.query('itemPart = :1', myPart);
Note how myPart (an entity) is used as the value in the query. After the query, myPartItems would be an entity collection of all invoice items related to myPart through the itemPart relation attribute. You may also query using 1->N relation attributes. For example:
var myPart = Part(100); var invoices = Invoice.query('invoiceItems.itemPart = :1', myPart);
This query will return an entity collection into invoices containing all invoices that have at least one invoice item whose itemPart is myPart. The find() method uses an identical syntax to the query() but differs in what it returns. For example,
var Fred = Person.find('name = "fred"'); //Returns one entity var Freds = Person.query('name = "fred"'); //Returns a collection var aFred = Freds[0] //Returns an entity, but remembers the collection
The first line returns only one entity. If there are more than one fred in the example above then find()returns the first one. You should not depend on which entity will be returned by find() when more than one entity matches the criteria.
Page 40 of 61
The second line returns an entity collection of all people named fred. It then assigns to the variable aFred a reference to the first entity in the collection. Note that functionally the variable Fred and aFred are different. Fred has no underlying reference to an entity collection while aFred is a reference to a specific entity in the Freds entity collection. This allows the entity aFred to iterate through the Freds entity collection using the method next(). Developer Preview Note: Using an entity reference as a value in a query is not completed. Locating Entities by Key Similar to the find() method a single entity may be looked up by key value. For example,
var thePerson = Person(100);
This code assigns to thePerson an entity that has the key of 100. The converse of this is the getKey() function for entities. For example,
var mike = Person.find('name = "mike"'); //Returns one entity var mikeKey = mike.getKey() //Returns the entity key
You can also locate an entity by supplying an object as a value. For example:
var aPerson = ds.Person({firstName: 'Brad', lastName: 'Wilson');
This code assigns to aPerson a person whose first name is Brad and whose last name is Wilson. This is functionally identical to
var aPerson = ds.Person.find('firstName="Brad" & lastName= "Wilson");
Just like find(), if more than one entity matches the criteria then the first one is returned. Developer Preview Note: Wakanda will support multi-attribute keys in the future. When getKey() is called on an entity with a multi-attribute key, it will return an array of values that make up the key. To locate an entity with a multi-attribute key, you will provide either an array of values or a series of parameters. Using Nulls in Queries As you would expect the keyword Null plays an important role in Wakanda queries. For example:
var badInvoices = Invoice.query('invoiceItems = Null');
This query returns a collection of all invoices that have no invoice items. Similarly:
var orphanItems = InvoiceItem.query('itemPart = Null');
This query returns all invoice items that have no related itemPart. This includes items that have never been assigned a related part as well as items whose part has been deleted.
Page 41 of 61
Conjunctions Wakanda lets you build queries using the following conjunctions.
Conjunction AND OR NOT EXCEPT Aliases & or && | or || ! ^ Notes Logical AND Logical OR Logical negation. Operates on next criteria or group of criteria if inside of parenthesis Equivalent to AND NOT
Comparison Operators Wakanda supports the following comparison operators. Generally, all Wakanda queries are case insensitive.
Operator = != == !== > >= < <= %% =% !=% Aliases eq or like # is or eqeq nene, isnot, or ## gt gteq or gte lt lteq or lte matches or %* !%* Notes Allows for wildcard support using * Not like, allows for wildcard support Exactly equal, no wildcard support Not equal, no wildcard support Greater than Greater than or equal to Less than Less than or equal to Contains keyword, may use a keyword index Matches a JavaScript regex expression Does not match regex expression
Using JavaScript in Queries Most queries can be handled by the syntaxes above. But in some cases a query is needed that doesnt follow the attribute/comparison/value methodology. What if we want to find all entities with an even numbered ID? Or what if we want to find all the people who have a last name with a length of 10? To do this, we can incorporate JavaScript expressions that return a Boolean value in our queries. For example:
var evenIDs = ds.Part.query('$((this.ID % 2)= 0)'); var lastName10 = ds.Person.query('$(this.lastName.length = 10)');
To inform Wakanda that a portion of a query is to be treated as a JavaScript expression, you prefix it with a dollar sign ($) and enclose it in parenthesis. In order to reference any attributes of the querys datastore class you need to use the keyword this so that Wakanda will correctly interpret identifiers. If you dont use the keyword this, then Wakanda interprets the identifier as a JavaScript variable. For example:
var divisor = 2; var someParts = ds.Part.query('$((this.ID % divisor)= 0)');
Page 42 of 61
Notice that the variable divisor is assigned before the query and is then used as part of the JavaScript expression. Relation Attributes and Entity Collections In addition to the variety of ways you can query, you can also use relation attributes as properties of entity collections to return new entity collections. For example:
var myParts = Part.query('ID < 100'); var myInvoices = myParts.invoiceItems.itemInvoice;
The last line will return in myInvoices an entity collection of all invoices that have at least one invoice item related to a part in the entity collection myParts. When a relation attribute is used as a property of an entity collection, the result is always another entity collection, even if only one entity is returned. When a relation attribute is used as a property of an entity collection and no entities are returned, the result is an empty entity collection, not null. Scalar Attributes and Entity Collections All scalar attributes are available as properties of entity collections as well as entities. When used in conjunction with an entity collection a scalar attribute returns an array of scalar values. For example,
var locals = Person.query('city = "San Jose"'); var localEmails = locals.emailAddress;
Page 43 of 61
Entity Collections and toArray() All entity collections include a function called toArray(). This flexible function provides a way to return an array of objects of varying complexity. For example, assume the following structure:
This model indicates that a course entity may have many classes (courseClasses) and that a class entity may have many attendees (classAttendees). If we query for classes and apply the toArray() function to the resulting collection without specifying a parameter then we receive back an array of objects, one object for each entity in the collection. The structure of each object is the default for the datastore class and follows these rules: - Storage, calculated, and alias attributes are returned as properties on each object. - N->1 relation attributes are returned as a property, which itself has one property called __KEY made up of two properties: ID and __Stamp. In future versions of Wakanda, __KEY may have more properties. - 1->N relation attributes are returned as a property having one property named __Count. These three rules result in a visual representation like this:
Page 44 of 61
First thing to note is that the datastore class named Class is prefixed with ds. Most datastore classes can be referenced without a prefix but in the case of one named Class you might need to reference it as a property of ds since Class is a reserved word. This example illustrates the default values returned by toArray(). In most cases you will specify a parameter or parameters for the toArray() method and this will affect the objects returned. So using the same structure lets look at the following code:
var var var var var myClasses = ds.Class.query('ID < 5'); classesArray = myClasses.toArray("ID, courseCode"); oneClass = classesArray[0]; //one class object (not an entity) theID = oneClass.ID; //scalar value of one object theCourseCode = oneClass.courseCode; //value from alias attribute
Here the toArray() function is provided with two scalar attribute names: ID and courseCode. Even though courseCode is referenced from another entity, it is treated as any other scalar attribute in Class. A visual representation of the array might be:
But we are not limited to attributes of the entity collection. For example:
var myClasses = ds.Class.query('ID < 5'); var classesArray = myClasses.toArray("ID, theCourse.name, theCourse.code"); var oneClass = classesArray[0]; //one class object (not an entity) var theID = oneClass.ID; //scalar value var myCourse = oneClass.theCourse.name; //attributes from the same entity var myCode = oneClass.theCourse.code; //are grouped into one object
Page 45 of 61
Here we are requesting a scalar attribute from the collection myClasses and two scalar attributes from the related course. When multiple attributes from the same related entity are included in the toArray(), they are grouped together. A visual representation might be:
If we reference a relation attribute that returns an entity collection, an array is returned. For example:
var var var var var var var var var myClasses = ds.Class.query('ID < 5'); a = "ID, classAttendees.grade, classAttendees.studentName"; classesArray = myClasses.toArray(a); oneClass = classesArray[0]; //one class object (not an entity) theID = oneClass.ID; //scalar attribute allAttendees = oneClass.classAttendees; //classAttendees is an array oneAttendee = allAttendees[0]; //get one of the array's elements oneGrade = oneAttendee.grade; //get the attendees grade oneName = oneAttendee.studentName; //get the attendees name
Page 46 of 61
Wakanda Transactions A transaction is a way to bundle several data operations (saves, updates, deletes) into an all or nothing package. Wakanda implements transactions as a method of the datastore called startTransaction(). Once this method is called, all subsequent data operations are included in the transaction. When all the data operations are complete, a matching commit() or rollBack() method is called. If you roll back a Wakanda transaction, all the data operations are reversed. Code that is executing between the startTransaction() and either the commit() or rollBack()is said to be in transaction. Code running inside of a transaction sees all the data operations as completed until and unless there is a roll back. Wakanda supports nested transactions. A nested transaction occurs when you start a transaction while already in a transaction. Like a set of matching parenthesis, the first occurrence of either a commit() or rollBack() applies to the most recently created transaction and all the data operations that were performed while it was in place. Entity Locking Record or row locking is a methodology used in relational databases to avoid inconsistent updates to data. The concept is to either lock a record upon read so that no other process can update it or alternately to check when saving a record to verify that some other process hasnt modified it since it was read. The former is sometimes referred to as pessimistic record locking and it ensures that a modified record can be written at the expense of locking records to other users. The latter is sometimes referred to as optimistic record locking and it trades the guarantee of write privileges to the record for the flexibility of deciding write privileges only if the record needs to be updated. In pessimistic record locking, the record is locked even if there is no need to update it. In optimistic record locking, the validity of a records modification is decided at update
Page 47 of 61
time. Wakanda uses optimistic locking in that when Wakanda attempts to save an entity, it checks to see if the entity has been modified by another mechanism since it was loaded. Each Wakanda entity has an additional attribute not shown in the Wakanda Datastore Model Designer. This additional attribute is changed at each save of the entity. When Wakanda attempts to save an entity, it checks to make sure that the value in memory matches the one stored in the datastore. If it doesnt, then the save is not completed and an error is thrown. For example, consider the following:
var x = Person(1); var y = Person(1); //Separate reference to same entity x.firstName = 'Bill'; x.save(); y.firstName = 'William'; y.save(); //Throws error
In this example, we assign to x a reference to the person entity with a key of 1. Then, we assign another reference of the same entity to variable y. Using x, we change the first name of the person and save the entity. When we attempt to do the same thing with y, Wakanda checks to make sure the entity on disk is the same as when the reference in y was first assigned. Since it isn't the same, Wakanda reports an error and doesnt save the second modification. If we want to handle the error ourselves, we can add a trycatch block to the code like this:
var x = Person(1); var y = Person(1); //Separate reference to same entity x.firstName = 'Bill'; x.save(); y.firstName = 'William'; try { y.save(); //Throws error } catch (e){ //do something here }
One exception to Wakandas optimistic entity locking method happens if an entity is modified and saved while inside of a transaction that is still open. When this occurs the entity is locked to all other processes until the transaction is completed. Attempting to save an entity in this state will result in an error. Testing Code An easy way to test out server-side code is to create a JavaScript file in your project and run it individually using Wakanda Studio. The scope of the file will be the Application where the file resides and the last line of code that returns a value will be echoed back. This allows you to work through and debug server-side code independent of the front-end browser. This technique was used in several examples above to show the results of functions. Wakanda Server Global Namespace and Objects The above examples have introduced Wakanda Server objects such as entities, entity collections, and datastore classes. Each of these types of objects has predefined methods and attributes that
Page 48 of 61
are available in Wakanda server-side programming. Below is a brief description of some of the other Wakanda Server objects. In code running under Wakanda Server, the default object is the Application. The application object represents one Wakanda project and it has several attributes and methods. All the application objects attributes and methods can be accessed without a qualifier in code running under Wakanda Server. For example:
Application.console.log('Processed %d items', numItems);
Some of the more commonly used application attributes are sessionStorage, an object that provides a general-purpose storage mechanism for session based information and ds an object that provides access to the applications model and a variety of other services. For example, say our application needs to track user-specific information. We define a class scope method named setSessionInfo with the following code:
setSessionInfo:function(name, value) { sessionStorage.lock(); sessionStorage[name] = JSON.stringify(value); sessionStorage.unlock(); }
This method takes two parameters: the name of what is to be stored and the value. It then attempts to lock the sessionStorage object. Each session has a unique sessionStorage object but since the browser-side code can run multiple asynchronous threads, we must ensure that two threads dont update the session storage object at the same time. This is easily done using the lock() method, which will wait for the sessionStorage object to be unlocked before the code proceeds. You should keep the code between lock and unlock as streamlined as possible. The sessionStorage object only allows string values to be assigned to properties. In the example above, we use the JSON function stringify() to convert the parameter value into a string for storage. The parameter value can be any type of scalar or object except Wakanda entities or entity collections. With this one method, browser-side code can identify name/value pairs that can be passed to the server for safekeeping. Each browsing session has a default timeout of 30 minutes, which is refreshed upon each use. The thirty-minute timeout can be adjusted as needed. To retrieve the stored value, we create the following method:
getSessionInfo:function(name) { return JSON.parse(sessionStorage[name]);
Page 49 of 61
This method can retrieve any saved session value. This includes complex JavaScript objects. Note: The two methods above are for illustration purposes only. It is unlikely that you would have a session storage mechanism with no control under what conditions an item is stored. More likely, you would attach items to sessionStorage using server side constructs driven by controlled input from the client. One of the most common properties of the Application object is the attribute ds. This attribute provides access to the applications model and datastore. All datastore classes can be accessed as a property of ds. Some examples are:
var dClass = ds.Invoice;
Assigns to dClass the datastore class named Invoice. Alternately, we can refer to classes indirectly by name such as:
var className = 'Invoice'; var dClass = ds[className];
As you may have noticed in other examples, if the class name is not a reserved word, it can be accessed without the ds prefix. For example,
var dClass = Invoice; //Usually don't need ds for class names
If we need to cycle through all datastore classes, we use the ds.dataClasses attribute like this:
for (var i = 0; i < ds.dataClasses.length; i++){ var theClass = ds.dataClasses[i]; //Do something with the class here }
Also available for datastore classes is the setAutoSequenceNumber() method. This method takes one integer parameter, which sets the value for the classs sequence number. This method is particularly useful after assigning values directly to the key attribute of an entity. Resetting a datastore classs auto sequence number will ensure that no new entity will repeat a key value. The ds object also provides methods to access transactions (see the On Remove event example), project specific folders (getModelFolder(), getDataFolder()), and to manipulate the servers data cache for the application (getCacheSize(), setCacheSize()).
Page 50 of 61
Once we have a reference to a datastore class, a variety of attributes and methods become available. Consider the following code:
var dClass = Invoice; var endDt = new Date(); var startDt = new Date(); startDate.setMonth(startDate.getMonth()-1); var eCol = dClass.query('postDate >=:1 AND postDate<=:2', startDt, endDt); var eCount = dClass.length; var message = eCol.length + ' invoices of ' + eCount + ' in last month';
This code fragment starts by assigning the Invoices class to the dClass variable. It then creates two date variables for use in the query. When a query is performed on a datastore class, all entities of the class are considered. The next line counts the number of entities in the datastore class. When the attribute length is applied to a datastore class, it returns the total number of entities in the class. If we want to retrieve a collection of all the entities, we use the datastore class method all() like this:
var allInvoices = Invoice.all();
This brings us to one of the most important objects in Wakanda: entity collections. An entity collection represents a list of entities and provides many different methods for manipulating data. For example, lets break down the following code fragment:
var brokers = Person.query('personType = :1', 'broker'); var brokerCnt = brokers.count('rank'); //Count brokers with a rank var westBrokers = brokers.query('region = "West"'); //West brokers var rankedWestBrokers = westBrokers.orderBy('rank'); //Sorted west brokers var topWestBroker = null; if (rankedWestBrokers.length > 0){ topWestBroker = rankedWestBrokers[0]; //Highest ranked west broker } var topWestBrokers = ds.Person.createEntityCollection();//Empty collection rankedWestBrokers.forEach(function(theBroker){ if (theBroker.rank == topWestBroker.rank) topWestBrokers.add(theBroker); }); var topWestCompanies = topWestBrokers.myCompany ();
This code uses a variety of entity collection abilities. It performs a query on a datastore class and assigns the resulting collection to brokers. It then counts the number of entities in the collection that have a value in the rank field, excluding nulls. Note that count() is a method and not an attribute. Using the entity collection now residing in brokers, it creates a new collection by further filtering the people to those in the West region. A query on an entity collection only considers entities that are already members of the collection. It then sorts the entities in westBrokers in order of the attribute rank and produces a new, sorted entity collection assigned to rankedWestBrokers. Next, it tests to see if there are any entities in rankedWestBrokers by using the attribute length. If there are, it assigns the first person to topWestBroker. To access a specific entity in an entity collection use [e] where e is an integer from 0 to the total number of entities in the collection minus one. It then uses the Person class to create a valid, but empty
Page 51 of 61
entity collection in topWestBrokers that will eventually hold all other brokers whose rank ties with the rank of the topWestBroker. Next, it cycles through all entities in rankedWestBrokers using the entity collection iterator forEach(), which takes as its first parameter a function to be applied to each entity in the collection. In our example, the function is defined in-line but you may also reference a JavaScript function elsewhere in your code. The function that is specified must accept as its first parameter a reference to the correct entity type. In our example, this parameter is named theBroker, but you may choose any name for this argument. For each entity in the collection, we test to see if the rank equals the topWestBroker. If it does, we add the entity to topWestBrokers using the entity collection method add(). When an entity collection is unsorted, add() will not add the same entity twice. If an entity collection is sorted, then add() will append the entity reference to the collection, even if the entity is already elsewhere in the collection. The method add() can add either a single entity or an entire entity collection. When the forEach() is finished, the topWestBrokers collection contains the top west broker and all those with the same rank. Of course, we could have performed a query in place of the forEach(), but this code illustrates one of the ways to traverse an entity collection. Lastly, we use the myCompany relation attribute of the Person class to return an entity collection of the companies related to the topWestBrokers. Lets look at some other methods of entity collections. For example,
var peopleTypes = Person.all().distinctValues('personType'); var results = {}; for (var e in peopleTypes){ results[e] = Person.query('personType = :1', e).average('salary'); }
This code fragment starts by using the entity collection method distinctValues() to retrieve an array of unique values in the personType attribute from all entities in the Person class. It then cycles through each element of peopleTypes and assigns to an implicitly defined attribute of results the average salary of all people with that specific person type. When this code fragment is done, results will have one attribute per person type and the value of each attribute will be the average salary of all people of that type. Another entity collection method is remove(). For example,
var orphanPeople = Person.query('myCompany = null'); orphanPeople.remove();
This code will delete all people that have no related companies, assuming there is a relation attribute myCompany. If the Person class has an On Remove event method, the event will execute once per person being deleted. If there is no On Remove method and we are deleting all entities in the class, Wakanda will truncate the datastore classs data, which is faster than deleting individual entities.
Page 52 of 61
Individual entities also have methods in addition to the user-defined ones. For example:
onSave:function() { if (this.isNew()){ this.creationDate = Date(); } }
This On Save event tests to see if the entity in question is new (never been saved) and if so it writes the creation date into the entity. A similar function isModified() is also available for entities. It returns True when an attribute of an entity has been changed, but the entity has yet to be saved. Once the entity is saved, isModified() becomes False. If we wish to test whether an entity will pass validation, we could use the following:
var newPerson = new Person({ firstName: 'Fred', lastName: 'Williams' }); try { newPerson.save(); //try to save the item } catch (e) { //may be either a validation or a save error }
Developer Preview Note: In a derived entity there will be an attribute that provides access to the corresponding extended entity. This item is not available in this version. Users, Groups, and Privileges Wakanda provides a flexible access control system based on users and groups. Users may be stored as data in datastore classes or can be referenced using an external service such as LDAP. Whether internal or external, users are managed via their membership in groups. Groups are defined as part of a datastore model and can be attached directly to model items, such as classes and attributes. Individual groups can be members of other groups. A Wakanda model can have groups assigned to methods as well as read, modify, and delete events for datastore classes. A default group can be defined at the class level controlling who can access the class. In addition, a default group can be assigned to the entire datastore model. If a different group is assigned as the default for the model or datastore class, it will take affect at all lower levels that have not been specified independently. Developer Preview Note: The access section for Wakanda is not yet available.
Page 53 of 61
Wakanda Server File Management Included in Wakanda Server are many utility methods, including file management methods and objects accessible at the global level. For example:
var projectFolder = ds.getModelFolder(); var importFolder = Folder(projectFolder.path + 'ImportData/'); if (importFolder.exists()){ importFolder.forEachFile(function(file){ var filename = file.nameNoExt; var theClass = ds.dataClasses[filename]; if (theClass!= undefined) theClass.import(file); }); }
This code starts by using getModelFolder, which is a method of ds. This method returns the folder containing the Wakanda project in which the code is executed. It then defines a new variable named importFolder to hold a reference to a subfolder named ImportData. Next, it checks to see if the importFolder exists and if it does, it cycles through all the files it contains. In our example, the code is expecting to find text files in this folder that have the same names as datastore classes in our model. For each file, the code determines the file name and uses it to locate a reference to the corresponding datastore class. On the off chance that a file in the folder is named something other than a datastore class, we check to see if the reference theClass is undefined. If not, the code expects a method named import to exist for each class and passes it the file reference. Dedicated Workers and Shared Workers Wakanda Server provides multi-threaded capabilities using workers. A worker is created by referencing an individual JavaScript file. When the worker is instantiated, it then becomes an object residing in memory waiting to be called. A Wakanda worker can have any number of internal methods; however, it must implement an onmessage function to respond to outside events if there are any. The main difference between a dedicated worker and a shared worker is scope. A dedicated worker can only be addressed from the parent thread that created it while a shared worker can be addressed from any thread. Dedicated workers end when the parent thread ends while shared workers continue to exist even if their spawning thread ends. Here is a basic example of how to create a dedicated worker:
var myWorker = new Worker('WorkersFolder/dedicatedWorker.js'); var myWorker.onmessage = function(event) { var message = event.data; if (message.type == 'stopped') { close(); } }
Page 54 of 61
In the above method, a new worker is created by passing a path to a project-specific JavaScript file, named dedicatedWorker.js. By passing the file with its relative path, Wakanda uses the project folder as the default, and expects the referenced file to result in a worker. To create the worker, Wakanda Server parses the JavaScript file and creates an object in memory with all the functions defined in the file as properties. The code above goes on to define a method named onmessage that is attached to the new dedicated workers reference. The name of this method must be onmessage if it is to respond to the postMessage calls from inside the worker. The code above continues by calling postMessage to which it passes a simple object with one property named type that has the value process. Calling postMessage is the standard way to communicate with a worker. It takes a single object as a parameter. The object can be as complex as you would like; the values in the object are copied so that they can be used inside of the worker. The code then calls wait(), which allows the parent thread to continue to exist, and is ended with a call to close(). A simple example of the dedicatedWorker.js file is shown below:
function doSomeWork() { // do some work, such as writing files, sending emails, etc. } function onmessage(event) { var message = event.data; if (message.type == 'process') { doSomeWork(); postMessage({type: 'stopped'}); close(); break; } } //Worker init code goes outside of all function calls here //but may call any functions defined for the file
The first thing to note is that if the workers JavaScript file has any code outside of all function declarations, Wakanda considers it initialization code for the worker and executes it when the worker is created. Notice that the workers code also implements a method named onmessage. When the parent thread calls the worker with postMessage, the onmessage function inside of the worker is called. When the worker calls the parent thread with postMessage, the onmessage function
Page 55 of 61
attached to the workers reference in the parent is called. While both these methods must accept a single object parameter, what they do is completely independent. The onmessage function receives a single object as a parameter. In the above example, the name of the parameter is event, but you can choose another name. Regardless of what you call this parameter, it will have a property named data. The data property will contain a copy of any object passed via the corresponding postMessage call. Since the values in data are copies, you cannot use any reference types, such as entities, entity collections, or datastore classes. Instead you must pass whatever information you need to identify Wakanda datastore items, such as arrays of key values or datastore class names. In our worker example, we assign to the message variable the values passed from the parent code and then test the type property to see if the message is process. If it is, we run the method doSomeWork. In our example, doSomeWork is just a stub, but it could do a variety of tasks, such as poll a local directory for files, process entities, send emails, or clean up data. When doSomeWork completes, our worker uses postMessage to tell the parent thread that it has stopped and then calls close() to terminate. Between the calling parent thread and the worker, here is the logical order of execution in our example: 1) At the new Worker call, Wakanda Server parses the file, sets up the onmessage function, and then runs the code outside of any function declaration. The new worker is now waiting for messages even though it hasnt been told to do anything yet. 2) The new workers reference is returned into myWorker in the parent thread. 3) A new method named onmessage is attached to the workers reference (myWorker) in the parent thread, thus preparing the myWorker reference to receive a message from inside the worker. 4) The parent thread calls postMessage on myWorker by passing {type: 'process'}. This call causes the onmessage function inside the worker to run or queue up a message if the worker is still executing code. 5) The parent thread calls wait(), which keeps it from terminating, thus turning the parent thread into a worker. When you call this method, the thread stays alive until you call close(). 6) The worker receives a message through the onmessage function as a result of step 4. If it has been told to process, it calls doSomeWork. 7) When doSomeWork is finished, the worker messages the parent by calling postMessage by passing the object {type: 'stopped'}. 8) The worker then calls close(), which ends the worker and frees its resources. The message sent to the parent thread is preserved because the values were copied into the data property in step 7. Notice that the worker may close before the message it sent to the parent is handled. This is why all worker event data is copied and object references cannot be used. 9) The parent thread receives the message from the worker. If the message indicates that the worker has stopped, the parent thread closes itself.
Page 56 of 61
The order in which some of these steps execute is ambiguous. For example, steps 2 through 5 may execute before step 1 ends because they are executing in separate threads. But this wont cause a problem. If postMessage is called for a worker or parent thread that is currently running code, the resulting event is queued. This includes parent threads that haven't yet executed wait(). If you find that you need to control the order of execution between threads, use the methods mutex() or syncEvent()described below. If you call postMessage on a worker that is currently executing code, the event is stacked and will be executed when the worker finishes its current operation. It is possible that several postMessage calls be stacked so that multiple onmessage events are waiting to execute. When this happens, the onmessage events will execute in the order they were called. To end a thread, you call close() from inside the thread. You can force a dedicated worker to close from the parent thread by calling the workers terminate() method. The terminate() method will allow the worker to complete its currently running code and at the next point where it can process a new event, it will close, ignoring any queued events. If you call close() on a waiting parent thread, all the dedicated workers spawned from that thread will receive a message to terminate. If you want to allow a worker to complete all its queued events, simply pass it a new event telling it to close. Shared workers are similar to dedicated workers in that they are created using a reference to a JavaScript file, but unlike dedicated workers, shared workers can continue to exist even after the parent thread that spawned them ends. When a shared worker is created, you provide a key value in addition to the file reference. When other threads want to interact with an already existing shared worker, they do so by executing the same code as if they are creating it, but instead receive a reference to the existing shared worker. Here is a basic example of creating a shared worker:
getTaskManagerStatus:function() { var tmRef = 0; var tmInfo = {taskCount:0, errorCode:0); var taskMgr = new sharedWorker('WorkersFolder/TaskMgr.js', 'TaskMgr'); var thePort = taskMgr.MessagePort; thePort.onmessage = function(event) { var message = event.data; switch (message.type) { case 'connected': tmRef = message.ref; taskMgr.postMessage({type: 'report', ref: tmRef}); break; case 'update': taskMgrInfo.taskCount = message.count; return taskMgrInfo; taskMgr.postMessage({type: 'disconnect', ref: tmRef}); close();
Page 57 of 61
break; case 'error': taskMgrInfo.errorCode = message.errorCode; return taskMgrInfo; close(); break; } } wait(); }
The purpose of this datastore class method is to respond to a browser-side request for information on the status of the TaskMgr shared worker. At the beginning, it defines a variable to hold a number that will be used to inform the shared worker which thread is requesting an update. It then creates a basic object with which to report to the browser-side request and attempts to connect to the shared worker referenced as TaskMgr. If there is no shared worker with this reference, this call creates it. If there is already a shared worker that uses the reference TaskMgr, this call connects to it. The code attaches an onmessage function, which is expecting a message type of connected, update, or error. Finally, it waits for a response. Since the browser-based call to this method would typically be asynchronous, it is reasonable that this method wait for a response from the shared worker. The corresponding TaskMgr worker might be something like:
function doSomeWork() { try { // do something tmCount += 1; } catch(e){ tmError = 1; } } function onconnect (event) { var thePort = event.ports[0]; tmKey += 1; tmConnections[tmKey] = thePort; thePort.onmessage = function(event) { var message = event.data; var fromPort = tmConnections[message.ref]; switch (message.type) { case 'report': if (tmError!= 0) { fromPort.postMessage({type: 'error', errorCode: tmError }); close(); } else
Page 58 of 61
{ fromPort.postMessage({type: 'update', count: tmCount}); } break; case 'disconnect': tmConnections[message.ref] = null; break; } } thePort.postMessage({type: 'connected', ref: tmKey}); } var tmCount = 0; var tmKey = 0; var tmError = 0; var tmConnections = []; setTimer(doSomeWork(), 1000) //Run every second
When the task managers shared worker is created, the code outside all function declarations is executed. This code sets up some variables, including the array tmConnections that will be used to track all the threads connected to the task manager. The last line of initialization code sets up a timer that will execute the method doSomeWork every second (1000 milliseconds). The setTimer method is available only inside of a worker or waiting parent thread. Each time the doSomeWork method is executed, the tmCount variable is incremented unless there is an error. Notice the onconnection function defined for the task manager. This function is automatically called whenever another thread acquires a reference to the shared worker. This includes the thread that creates the shared worker as well as threads that connect to it when it is already running. When the onconnection method is called, the event parameter will have a property named ports, which is an array. The value in ports[0] is a reference to the parent thread that caused the onconnection function to execute. Notice that in our code we increment the variable tmKey and then store the reference to the connecting thread in the array tmConnections. Also notice that an onmessage function is attached to the port. This method will be called when the parent thread posts messages to the shared worker. In our example, we are attaching the same onmessage function to each of the connecting parent threads, but we could also attach a variety of onmessage functions depending on the situation. Our code then calls postMessage on the newly connected parent thread to inform the parent that the connection was made and returns the value in the tmKey variable. Back in the parent thread, this value is stored and used when the parent thread requests that the task manager report. This in turn causes the onmessage function, attached to the parents port inside of the shared worker, to execute. Notice that the shared workers onmessage function uses the key to determine which parent thread sent a message. If the message type is report, the task manager either responds with a count of how many times doSomeWork has successfully executed or it responds with an error code, if something went wrong, and then closes. If the task manager successfully reports to the parent thread, the parent thread lets the task manager know that it is disconnecting and closes. Back in the task manager, the disconnect causes it to clear out the port reference from the tracking array. This last step is not strictly necessary in our example code. But if we were to
Wakanda Server Concepts Page 59 of 61
augment the task manager so that it responds to all attached parent threads, it might become necessary to track which parent threads are still valid. So, it is good practice to have the parent thread inform the shared worker when it is disconnecting. There is no built-in event to inform the shared worker which parent threads are no longer connected.
Page 60 of 61
Naming Conventions In Wakanda, entities, attributes, and methods are case-sensitive when referenced in JavaScript and therefore shouldnt contain spaces. In addition, datastore classes and relation attributes become types that can be used for other attributes so their names are important. It helps to follow some naming convention when defining a Wakanda datastore model so that the difference between entities and attributes are easy to determine at a glance. In this document we have followed these naming conventions: Datastore classes start with uppercase. If a datastore class name consists of more than one word (e.g. InvoiceItem), the words are run together with an uppercase letter for each word. Entity collection names follow the same convention as datastore class names except that they are plural (e.g. Companies). Attributes and methods start with lowercase. If an attribute or method name consists of more than one word (e.g. firstName or importCompanies()) the words are run together with an uppercase letter for all words but the first. Although allowed, avoid naming attributes the same as datastore classes.
Page 61 of 61