nodemda-standards v2.2.1
NodeMDA Standards
Introduction
NodeMDA Standards is a plugin for the NodeMDA code generation engine.
It is a mixin
plugin that defines a standard
set of data types and Handlebars helpers that are used by most plugins in the NodeMDA universe.
The Standards mixin functions also performs a set transformations on to the meta model before code generation begins. These transformation convert associations to attributes of the classes, reducing the complexity of code templates.
Standard Data types
The Datatype object that is assigned to an attribute, parameter, or operation return type should have a name that matches one of the following in in both case and (lack of) punctuation:
Primatives
- String
- Number
- Boolean
- DateTime
Higher order string types
- Text
- FullName
- FirstName
- LastName
- Password
- Phone
- Url
- StreetAddress
- City
- State
- PostalCode
- CreditCardNumber
Higher order numeric types
- Integer
- Decimal
- Currency
Higher order date types
- Date
- Time
Common enumerations
- YesNo
- Sex
- OnOff
Security related
- SystemRole
Modeling Conventions
All code generation plugins that utilize the the "Standards" expect the following conventions to be used in the models used as input to NodeMDA. The standard is currently based on four key UML artifacts: Classes, Datatypes, Stereotypes, and Actors. The general strategy for creating your model is as follows:
Create one or more Class diagrams.
Add one or more class definitions to your diagram(s) based on your design.
Tag each class with a stereotype with one of the following names:
Entity
Indicates the class is a persistent/database entityService
Indicates the class defines a "Service" interface (e.g. for remote procedure calls)POJO
Indicates the class is a "Plain Old Javascript Object", meaning a simple class definition that holds data. This stereotype applies to all programming language targets, not just Javascript (despite its name).
Populate your classes with attributes and/or methods, making sure to specify a Datatype for your attributes and method parameters. Datatypes used MUST be one of the standard data types listed above.
There should be one class named
User
with the stereotypeEntity
in the root package of the model (i.e. the class should not be the child of a package). ThisUser
class must have AT LEAST the following three attributes:username
with a data type ofString
and a multiplicity of '1'password
with a data type ofPassword
and a multiplicity of '1'roles
with a data type ofSystemRole
a multiplicity of '0..*', and a tagged value nameddefaultValue
with a value of "'user'"
You can add an Enumeration datatype to your model in one of two ways:
- Add an "Enumeration" object that is native to your UML modeling system and populate it with Enumeration Literals
- Add a class to your model and use the "Enumeration" stereotype. In this case, each attribute you define will be used as one of the possible enumeration literal values.
Entity attributes that have a visibility other than "public" will be considered "for internal use", and thus will not be present in the CRUD user interface code. Attributes marked as "protected" are available to leave the system via the any remoting interface, but attributes marked as "private" will be filtered out by the backend before the remote request responds.
The tag
uiTableColumn
with a value of "true" can be added to one or more of an Entity's attributes to indicate which attributes are to be included in the frontend's CRUD selection table. If no attribute is explicitly tagged with "uiTableColumn", then ALL attributes that would otherwise be included on a form (see "visibility" above) are assumed to also be desired as table columns. This fact can be handy for entities with a small number of attributes, as it saves you time when modeling.Entity
attributes can be marked as "unique" to add the "unique" index to the mongo database. They can be tagged with thedbIndex
tag to create an index on that field.The
minVal
andmaxVal
tags can be added to numbers to be ranges on the possible values.Specify a "multiplicity" value for class Attributes and method Parameters to indicate if the value is optional or not. If the Multiplicity is not explicitly set, "0..1" is assumed, which translates to an "optional value" for most plugins. Attributes and parameters can be made "required" by setting the lower limit of the multiplicity to one (e.g. "1" or "1..*").
Arrays can be modeled by specifying an upper limit of the multiplicity on an attribute or parameter to a value that is greater than one (e.g. "0..*").
Special handling of associations
You can draw an association between any two classes. A navigable association has the same effect as adding an attribute to the class that has a data type of the associated class. An association is considered "navigable" if it is explicitly marked as "navigable" in the UML, or if has been given an explicit name. If an association is marked as navigable but has not been given an explicit name, a default name will be provided. An association end that is explictly marked "not navigable" will not be navigable even if it has been given a name.
An association between an
Entity
and a non-entity (POJO
) implies an embedded class (i.e. that data of the POJO is stored inside theEntity
document in the database). An association between oneEntity
and anotherEntity
implies a "reference" type relationship between two different database documents.In an association, you can specify a multiplicity on each end of the association. the relationships can be "one to one", "one to many" "one to zero or one", or "one to zero or many",
Since adding an association has the same effect as adding an attribute of that type, you can use classes in your model as the data type of an Attribute in an
Entity
class. For example, class "Person" could have an attribute named "addresses", which is an array of "Address" POJO classes. Both "assocations" and "attributes with data type of some class" in practice generates the same code as they have the same semantics. It is recommended that you model "embedded" documents as "attributes", and model "references to other documents" as associations to keep the peristence method in your models clear.
Special handling of One to Many relationships
A one to many relationship between two entities is a common pattern that is handled in a special way: the primary key of the one side is stored as a property in the many side. This property in the many side is called a foreign key. There is no need to store an actual property for the many side on the one side, since the values can be looked up in the databasee. This attribute on the one side becomes a virtual/computed attribute rather than a real one. Since this is such a common pattern, the standards plugin performs a special transformation to the meta model.
Assume you have two entites: one named Master
and one named Slave
. Assume there is a one master to many slaves
relationship. The "array" attribute that
represents the many Slave
class in Master
is named mySlaves
. The attribute in the Slave
class that represents the one Master
is named myMaster
.
After processing by the Standards template support the meta model will have:
Slave
will have aNodeMDA.Atttribute
object namedmyMaster
added to itsattributes
array. TheAttribute
object a property namedisForeignKey
that is set to TRUE. Thetype
of the attrbute will be aNodeMDA.Meta.ObjectDatatype
that references theMaster
class.Master
will NOT have aNodeMDA.Attribute
object for theSlave
in itsattributes
array. Instead, theAttriubte
object *will be created, but it will be added to an array property namedvirtuals
. It will have aname
property ofmySlaves
, and atype
property that is an instance ofNodeMDA.Meta.ObjectDatatype
that references theSlave
class. It will also have a property named_schemaDbProp.foreignKeyField
that is equal tomyMaster
, which is the name of the attribute in theSlave
class that is matched againstMaster._id
when gathering all of the related slaves.
Security
It is expected that there is a class named
User
with a stereotype ofEntity
as specified in Modeling Conventions above.Security is handled using a role based system. There are three implied roles in the system: AdminRole, UserRole, and GuestRole. Any authenticated user has the UserRole. Unauthenticated users have the GuestRole. The AdminRole is reserved for system administrators. You can add additional roles to the system by defining Actor elements in your UML model. The name of the role will be the camel case of the name you assign to the actor, unless it has the suffix of "Role", in which case that suffix will be dropped. Examples: an Actor named SubscriberRole would be assumed to represent a role named "subscriber". An Actor named "Paid Subscriber" would create a role named "paidSubscriber"
There are three basic permissions each Entity has: read, write, and delete. An Entity has one or more roles associated with each of these permissions. The AdminRole always has read, write, and delete permissions on an Enity.
Each Entity is "owned" either by the system, or by a specific authenticated user. A "user owned" Entity is owned by the creator of the Entity, and by default it can only be modified by the owner or the administrator. A system owned Entity does not belong to any particular user, and whether or not it can be modified depends on how the permissions are modeled. An Entity is considered "user owned" if ownership is assigned to any role other than the AdminRole. If any user owned entities are defined, a fourth implied role named "owner" is added to the system.
The recommended way to define a permission is to draw a dependency from the Entity to one or more Actors. Each dependency will grant read, write, and delete permissions to users who have the role represented by the Actor, unless you refine the permissions using a tagged value (see below). Admin users always have these permissions, so it is not necessary to explicitly grant the AdminRole permissions unless it is the ONLY role you are assigning, and you want to restrict the Entity to "admin only" visibility and modification.
You can refine the permissions granted to a particular role by adding a tagged value named "permissions" to the dependency. The value of the tag can be one or more of "own", "read", "write", or "delete", each separated by either a space, comma, or one of the other special separator characters: ".", "-", "/", ";", or ":". In reality, only the first letter of the permission is looked at in a case insensitive manner, so you can define a permission value in whatever combination makes the most sense to you using those separators. Other examples for "read,write,delete" include "r-w-d" and "Rd:Wr:Del". The "own" permission can only be assigned to a single role for any given Entity. Any role other than "AdminRole" given the "own" permission implies a user owned entity (see above).
If no explicit permissions are defined for an entity by drawing a dependency, ownership is assigned to the Admin role and the other permissions depends on the Entity's visibility. A "public" visibility will allow unauthenticated users to read, write, and delete the data (i.e. R-W-D to GuestRole). If the visibility is "protected", then R-W-D permissions are granted to UserRole. If it is private, then no user, including the AdminRole, can even read the data via the REST interface. The Entity will exist strictly for internal system use only. Note that most UML modeling software defaults to "public" visiblity, and that grants GuestRole read,write,delete permissions. For this reason, it is best to be explicit about security and to always draw security dependencies on your entities.
Control generation of code for REST and CRUD
For every Entity defined in the system, a CRUD user interface is generated in the front end app. You can prevent this UI from being generated by adding the tag "noUI=true" to the Entity.
For every Entity defined in the system a REST interface is added to the API that allows for the normal CRUD operations. You can prevent this interface from being generated by setting the visibility of your Entity to "private", or by adding the tag "noREST=true" to the Entity. Note that the UI depends upon the REST interface, so if you use the noREST=true noUI will automatically be set to true also.
Template support
Meta model mixins
To support all of the above conventions, this plugin provides the following mixin modifications to the meta model:
onModel
- Getters
entities()
- returns all classes whosestereotypeName
isEntity
.
- Getters
onAttribute
- Getters
jsIdentifierName()
returns the attribute name as the JavaScript identifier (commented-out logic shows how to prefix_
for private attributes).schemaDbProperties()
returns a JSON string of an object of database related properties:persistence
how is this attribute stored in a persistent store? Will be"embed"
if the field should be embedded, orundefined
otherwise.unique
true if this attribute is unique in any collection it is stored inforeignKeyField
- In a one to many relationship, this is the name of the attribute in the many class that matches the_id
of this attribute's parent. Special handling of One to Many relationships
_schemaDbProp
the pure javascript version ofschemaDbProperties()
isForeignKey
- will be true if this attribute's parent is the many side of a one to many relationship. The attribute is used to match against the_id
property of the class on the one side of the relationship. Special handling of One to Many relationshipsisEntity()
checks if the attribute’s type is a class with stereotypeEntity
.hasIndex()
checks for adbIndex
tag.importedBySchema()
checks if the attribute is an object embedded or has a foreign key property.visibleToForm()
indicates if this attribute should be visible on any UI related input formvisibleToTable()
indicates if this attribute should be visible as a column in any UI related table
- Functions
setSchemaDbProperty(name, value)
attaches schema metadata (e.g.,persistence: 'embed'
orunique: true
).
- Getters
onObjectDatatype
- Getters
jsClassNameWithPath()
usesjsPathToIdentifier()
to convert the object’s path.
- Getters
onClass
- Getters
jsClassNameWithPath()
converts full class path for imports.jsPackageName()
converts package name to a JS-friendly path.relativeParentPrefix()
computes how many directory levels to go up for import statements.packageDirPath()
returns the class’s package path, ensuring it ends with a slash unless in the root.isRoleRestricted()
checks if the class has any dependencies on actors.roleList()
collects roles fromdependentActors
, including any inherited from the parent class.stringifyRoleList()
JSON-encodesroleList
with single quotes.isEntity()
checks if the stereotype isEntity
.isEnumeration()
checks if the stereotype isEnumeration
orenumeration
.isUserEntity()
returns TRUE if this class is namedUser
and has theEntity
stereotypereadPermissions()
,writePermissions()
,deletePermissions()
,ownPermission()
return security settings.isUserOwned()
returnstrue
if the class ownership is assigned to a role other thanadmin
.genCRUD()
returnstrue
if the UI code should be generated (requiresgenREST
and not taggednoUI
).genREST()
returnstrue
if the class is not private and not taggednoREST
.formAttribs()
returns an array of all attributes that arevisibleToForm
tableAttribs()
returns an array of all attribute that arevisibleToTable
entityAttribs()
returns an array of all attributes that have an object datatype that is anEntity
classtableColumnsVisible()
returns a count of the number of attributes that arevisibleToTable
tableColumnsInvisible()
returns a count of the number of attributes that are NOTvisibleToTable
- Functions
addPermission(perm, roleName)
modifies the class’s internalpermissions
structure (e.g., adding'read'
,'write'
,'delete'
, or'own'
).
- Getters
onDependency
- Getters
isActor()
checks ifotherObject
is anActor
.permissions()
returns an array of parsed permissions from thepermissions
tag (defaults toread, write, delete
).
- Getters
onActor
- Getters
roleName()
produces a camelCase role name by stripping any “Role” suffix from the actor name.
- Getters
onMetaElement
- Getters
jsCommentsFormatted()
splits the element’scomment
into lines of at most 80 characters, replacing newlines with<p>
.singularName()
returns the singular form of the element name (usespluralize
).pluralName()
returns the element name in plural form.
- Getters
Handlebars helper functions
1. isNonEmptyArray.js
Purpose
Checks if a value is an array with at least one element.
// Example usage in a .hbs template:
{{#if (isNonEmptyArray myArrayVar)}}
The array has content.
{{else}}
The array is empty or not an array.
{{/if}}
2. kebabCase.js
Purpose
Converts a string to kebab-case (lowercase words separated by -
).
// Example usage:
{{kebabCase "MyExampleString"}}
// Outputs: "my-example-string"
3. singularOf.js
Purpose
Returns the singular form of a word using pluralize
.
// Example usage:
{{singularOf "People"}}
// Outputs: "Person"
4. jsStringEscape.js
Purpose
Escapes a string so it can be safely inserted as a valid JavaScript literal (quotes are removed).
// Example usage:
var myString = "{{jsStringEscape "Hello \"there\""}}";
// Outputs: var myString = "Hello \"there\"";
5. camelToWords.js
Purpose
Transforms a camelCase or PascalCase string into a spaced phrase, capitalizing the first letter. Also replaces underscores with spaces.
// Example usage:
{{camelToWords "myVariableName"}}
// Outputs: "My Variable Name"
6. setProperties.js
Purpose
Allows you to set multiple hash arguments as top-level properties on the template context for a block.
{{#setProperties title="Hello" count=5}}
Title: {{title}}, Count: {{count}}
{{/setProperties}}
7. outputIfEqual.js
Purpose
Outputs the block if two parameters are strictly equal. Otherwise outputs the {{else}}
block.
{{#outputIfEqual class.name "Person"}}
This is the Person class.
{{else}}
This is not the Person class.
{{/outputIfEqual}}
8. ifBoth.js
Purpose
Renders the block if both condition1
and condition2
are truthy. Otherwise renders the {{else}}
block.
{{#ifBoth class.isEntity class.hasOperations}}
Entity with operations
{{else}}
Not an entity with operations
{{/ifBoth}}
9. uppercaseFirst.js
Purpose
Capitalizes the first letter of a string.
{{uppercaseFirst "employee"}}
// Outputs: "Employee"
10. isdefined.js
Purpose
Checks if a value is not undefined
.
{{#if (isdefined class.name)}}
The class name is {{class.name}}
{{else}}
This class is unnamed
{{/if}}
11. renderOnce.js
Purpose
Prevents repeated output if a certain object is already rendered. Useful for avoiding circular references in recursive templates.
{{#renderOnce class}}
{{class.name}} details ...
{{/renderOnce}}
12. concat.js
Purpose
Concatenates multiple arguments into a single string.
<p>{{concat "api/" class.name "/endpoint"}}</p>
// Outputs: <p>api/Person/endpoint</p> for class.name = "Person"
13. lowercaseFirst.js
Purpose
Lowercases the first character of a string.
{{lowercaseFirst "Employee"}}
// Outputs: "employee"
14. safePreserve.js
Purpose
Returns preserve!
if the two string parameters match, otherwise preserve
. This is typically used with NodeMDA output directives.
##output {{safePreserve class.name "User"}}
15. lowercase.js
Purpose
Converts a string to all lowercase.
{{lowercase "HelloWORLD"}}
// Outputs: "helloworld"
16. pluralOf.js
Purpose
Returns the plural form of a word using pluralize
.
{{pluralOf "Person"}}
// Outputs: "People"
17. rootNamespaceName.js
Purpose
Splits a dot-delimited name (e.g. my.company.project
) and returns the first path element as a Handlebars SafeString.
Root namespace: {{rootNamespaceName "my.company.project"}}
<!-- Outputs: "my" -->
18. eval.js
Purpose
If the first argument is a function, calls it with subsequent arguments. Otherwise returns an empty string.
{{eval someFunction arg1 arg2}}
// If `someFunction` is a JS function, it is invoked as someFunction(arg1, arg2).
19. initJSNamespace.js
Purpose
Initializes nested JavaScript objects for a given namespace on a variable. Produces lines like myVar.myNamespace = myVar.myNamespace || {};
.
{{initJSNamespace "myVar" "MyCompany.Project"}}
<!--
Outputs:
myVar.MyCompany = myVar.MyCompany || {};
myVar.MyCompany.Project = myVar.MyCompany.Project || {};
-->
20. camelCase.js
Purpose
Converts any string to lowerCamelCase by splitting on spaces or periods, capitalizing each word after the first, then joining them.
{{camelCase "My example phrase"}}
// Outputs: "myExamplePhrase"
21. outputUnlessEqual.js
Purpose
Outputs the block if param1
and param2
are not strictly equal. Otherwise renders the {{else}}
block.
{{#outputUnlessEqual class.stereotypeName "Entity"}}
This is not an entity.
{{else}}
This is an entity.
{{/outputUnlessEqual}}
22. defaultTo.js
Purpose
Returns the first argument if it is truthy. Otherwise returns the second argument.
{{defaultTo class.name "UnnamedClass"}}
<!-- If class.name is falsy, outputs "UnnamedClass". Otherwise outputs class.name. -->
Summary
StandardSupport.js
supplies a consistent approach to:
1. Associations: Turned into class attributes for simpler template use.
2. Validation: Ensures a root User
class with mandatory attributes.
3. Mixin Additions: Provides a variety of getters/functions for code generation (path resolution, naming conventions, security checks, schema metadata).
4. Permissions: Applies defaults based on UML Dependency
to Actor
, or by class visibility.
5. Enumerations: Transforms UML classes with stereotype Enumeration
into real enumerations, and adjusts any referencing attributes.
All these operations ensure NodeMDA Standards–based plugins can rely on a uniform, enhanced meta model.