Skip to article frontmatterSkip to article content

Mike Language Guide

1.Introduction to Mike

1.1.What is Mike?

Mike is a declarative language which allows you to define data models and user interface structures in a concise and straightforward manner. Based on the description written in the Mike format, Mike will generate the Java code for a full application which can be run and deployed immediately. The generated Java code itself can also be freely modified and extended to better suit your needs. The Mike descriptor and the generated Java code can be developed in parallel, and Mike will not overwrite your changes. Instead, they will be merged in a similar manner to a version control system such as git. In this way, Mike is your cooperative virtual colleague.

The following sections will walk you through the process of writing a Mike descriptor for a hypothetical example use case and provide an overview of the features available in the Mike language. (For questions about specific details and edge cases, consult the Reference.)

1.2.An introductory example

Let’s begin by creating a basic Customer Relationship Management (CRM) system with just a single entity, Customer, and a single page displaying a table of all customers recorded by the system.

main.mike
# This is the data model
data {
    entity Customer {
        email: String @primary
        name: String
        phoneNumber: String
    }
}

# And this is the user interface
ui {
    page CustomersPage @home {
        table CustomersTable: Customer
    }
}

1.3.Generating and running the application

Follow these steps:

  1. Sign up for a developer account at codemike.dev.

  2. On the dashboard, create a new project called “CRM”, and copy the API key that was generated for it.

  3. Download the newest version of the Mike CLI tools from the Releases page.

  4. Follow the installation instructions.

  5. Create a new directory to hold the application, such as crm.

  6. Enter this directory, and issue the mike init command.

  7. The init command will ask a series of questions. For the project’s name, use “CRM”. When it asks for your API key, use the key copied in step 2. For the remainder, you can accept the default values. This will result in the creation of two configuration files, config.yaml and mike.yaml, and a dummy Mike descriptor file, main.mike.

  8. Copy and paste the example code from above into the crm/mike/main.mike file, overwriting its previous contents.

  9. Issue the mike generate command. This will create the Java code and other required files for the application.

  10. For running the application, there are two main alternatives:

    Using Docker
    Manually

    If Docker is available, the simplest is to invoke docker compose up, which will start the application with no other dependencies required. (This may take several minutes.)

    Note that if you subsequently make changes to the generated code, docker compose up --build will be necessary instead to force the Docker image to be rebuilt. See also the documentation in the README file for the generated project.

  11. Navigate to http://localhost:8888 in your web browser to see the running application.

  12. We are initially greeted by the login page. To log in as the administrator, the username and password are both “admin” by default. (These can be changed from within the application.)

If all goes well, you will see an initially-empty table with an “Add new” button you can use to add new customers to the system.

(For more details on the features provided by the mike command, refer to the Mike CLI user manual.)

1.4.Explaining the example

Every Mike descriptor contains two main top-level sections, data { ... } which specifies the data model (database schema), and ui { ... } which specifies the structure of the user interface.

In this example, the data model consists of just a single Customer entity with three members, each with type String (holding textual data). The email member is marked @primary, which means it will be used as the entity’s primary key in the database. Every entity must have exactly one member designated as the @primary one.

The user interface, meanwhile, so far consists of a single page called CustomersPage. This page is marked as the @home page, which means it will be the page that’s initially displayed after user login. (If no pages are marked as @home, an empty page will be created to serve as the home page by default.) This page contains a single table declaration named CustomersTable, which specifies Customer as the entity type to be displayed in the table. By default, the table will contain a column for each member of the entity, and supports creating, viewing, editing, and deleting instances. (Later chapters will describe how to make different choices for the table’s contents and behavior, if so desired.)

In terms of its structure, a Mike descriptor consists of a nested hierarchy of declarations, which are the lines that begin with a keyword (here data, ui, entity, page, and table) or a name (here email, name, and phoneNumber). A declaration may be annotated with one or more attributes that modify its behavior, such as @primary and @home, and may have one or more child declarations specified within braces, { ... }. The declarations, attributes, and other features supported by Mike will be introduced in the rest of this Guide. (You may also consult the Reference for full details.)

Each declaration -- other than the top-level data and ui blocks -- has a name (in this example, Customer, email, CustomersPage, and so on). These names serve a dual purpose: they may be used to refer to that declaration from within the descriptor itself (for example, table CustomersTable: Customer refers to the previously declared entity Customer), and they will be used for the names of classes and other elements in the generated code, so for example, a Java-side class Customer will be generated based on this descriptor. (Note that naming conventions on the Mike side are enforced, so for example, we could not have written entity customer or Name: String!)

Keywords and names are sometimes optional. In the above example, name: String is short for member name: String, because member is the default keyword within the context of an entity. Similarly, the entity keyword itself may be omitted from Customer, if desired. And if we were to simply write table: Customer without specifying a name, a default name for it would be derived from its type, which in this case would be equivalent to writing table CustomerTable: Customer (note the singular “Customer”, as opposed to “Customers” in our manually-written example). Full details about which keywords are optional and how default names are derived are available in the Reference.

Comments, which will be ignored by Mike, may be written beginning with the # character, and continue until the end of the line.

1.5.What’s next?

In this simple example, we’ve only scratched the surface of Mike’s features. In the following chapters, we’ll expand our CRM system to include more entities and relationships and more sophisticated UI structures. We’ll learn about topics such as:

By the end of this Guide, you’ll have a detailed understanding of how to use Mike to create an application that satisfies your individual needs and requirements.

2.Entities and relationships

In this chapter, we’ll build upon our basic CRM system by adding more entities, creating multiple pages, and defining relationships between entities.

2.1.Multiple entities, pages, and the navigation element

Let’s start by expanding our data model in the main.mike file to include Sales and Products, in addition to our existing Customer entity. We will also add a separate page in the UI for each of these.

main.mike
data {
    entity Customer {
        email: String @primary
        name: String
        phoneNumber: String
    }

    entity Product {
        sku: String @primary
        name: String
        price: Float
    }

    entity Sale {
        id: Int @primary
        date: Date
        totalAmount: Float
    }
}

ui {
    navigation {
        item CustomersItem: CustomersPage
        item ProductsItem: ProductsPage
        item SalesItem: SalesPage
    }

    page CustomersPage @home {
        table CustomersTable: Customer
    }

    page ProductsPage {
        table ProductsTable: Product
    }

    page SalesPage {
        table SalesTable: Sale
    }
}

What’s new here?

We can now navigate to separate tables for each of our three entity types but, so far, they are completely independent of each other. The next section will demonstrate how to create relationships between them.

To generate and run this application, follow the steps from the earlier section, starting from step 7 if you’ve already generated an application from the previous example. (If you haven’t, then of course, start from the first step. The only difference is which example code to copy into the main.mike file.)

2.2.Relationships between entities

Let’s now flesh out our data model by adding relationships between the entities:

main.mike
data {
    entity Customer {
        email: String @primary
        name: String!
        phoneNumber: String
        sales: [Sale] @mappedBy(customer)  # (new)
    }

    entity Product {
        sku: String @primary
        name: String!
        price: Float!
    }

    entity Sale {
        id: Int @primary
        customer: Customer!  # (new)
        products: [Product]  # (new)
        date: Date
        totalAmount: Float
    }
}

(The ui section is the same as before.)

There are several new concepts here. We will consider them one at a time.

The Type! syntax means that the given member is mandatory: at the database level, it is NOT NULL, and in the UI, a value for it must be provided by the end user when creating and editing the entity’s instances. This syntax is actually shorthand for an attribute called @mandatory: if we write name: String @mandatory, this means exactly the same thing as name: String!. By default, anything not marked mandatory is considered optional (nullable); except that @primary also implies @mandatory, i.e., the primary key must always be assigned a value.

As we can see, each entity type we declare can subsequently be used in the types of entity members, such as customer: Customer! within Sale. (Declarations can be in any order, and do not need to precede their uses, as we can see with the sales member in Customer.) If we think of this in programming language terms, it’s fairly straightforward: an object of one type holds a reference to an object of another type. In database terms, meanwhile, we’re defining a relation between the two types (or more concretely, between the database tables storing their respective instances). In the case of customer: Customer! in Sale, it means that each Sale is associated with exactly one Customer. (In the reverse direction, however, a given Customer can implicitly be associated with any number of Sales!)

This brings us to [Type] and @mappedBy. In a similar way to how Type! means “mandatory value”, [Type] means “many values” -- and is shorthand for the @many attribute. So in the line sales: [Sale] @mappedBy(customer) within the Customer entity, we’ve now explicitly specified, in the form of Mike syntax, what was mentioned at the end of the previous paragraph: that each Customer can be associated with multiple Sales.

The @mappedBy(customer) attribute, meanwhile, ties the two together: it says that the Sales a given Customer is associated with are the same as the Sales which refer to that Customer (through their customer member[1]). They’re two sides of the same coin. This is important, because it’s not the only possible state of affairs! If we were to leave off the @mappedBy attribute, then the customer and sales members would be, metaphorically speaking, each the “heads” side of two separate coins. This is perhaps easier to understand with an example:

With @mappedBy:

Here, the contents of Customer.sales is always the inverse of Sale.customer. In the UI, only the contents Sale.customer can be provided by the end user, and Customer.sales will automatically reflect those changes wherever it is displayed.

Without @mappedBy:

In this case, the contents of Customer.sales can be anything -- it doesn’t need to be related to Sale.customer in any way. (It could happen to hold the same values as in the previous table, but it would be a mere coincidence.) In the UI, the contents of Sale.customer and Customer.sales can both be edited independently by the end user, and neither has any influence on the other.

Now if, in fact, we were to simply delete @mappedBy(customer) from the above, Mike would issue a warning, asking us to clarify our intent: did we really mean to define two completely independent relations, rather than the forwards and reverse directions of the same relation? This is to guard against @mappedBy being omitted purely by accident. If it was truly deliberate, then we can use another attribute -- @unidirectional -- to make our intention clear. (In the context of the above example, it would suffice to add @unidirectional to either the customer or the sales member, and then Mike won’t feel the need to warn us any more.)

It’s worth noting that declaring a corresponding “backwards reference” member with @mappedBy (like sales in the example) for each “forwards reference” member (like customer) is entirely optional. We can delete the sales line, and there would be no adverse consequences. Conversely, within Product, we could add the line sales: [Sale] @mappedBy(products), which we had heretofore omitted.

A @mappedBy member can be considered “virtual” in the sense that it is not stored or represented in any way in the underlying database: it is solely a higher-level convenience. The practical effect of adding a @mappedBy member is twofold:

Finally, let us return to the products: [Product] line within the Sale entity. As there’s no @mappedBy here (nor could there be, since no member of Product refers to Sale), the contents of this member are directly stored in the database, in the same way as described for Customer.sales in the “without @mappedBy” scenario from above.

However, whereas a scalar (non-@many) reference to an entity is represented in the database as a column holding that entity’s primary key type together with a foreign key constraint, a @many member (recall that products: [Product] is shorthand for products: Product @many) is stored as a separate relation table. The relation table has two columns, one which holds a foreign key to the database table representing the referring entity (Sale in the example), and the other, to the referred-to entity (Product). If we recall that each Sale is associated with any number of Products and each Product with any number of Sales (which we can make explicit by adding a @mappedBy member in Product, as mentioned earlier), then the aforementioned relation table records which pairs of Sale and Product are associated with each other. Since many-to-many relationships are symmetric, if we want to explicitly declare both sides of the relation, then which of the two sides to put the @mappedBy attribute on is usually a matter of taste.

At the level of a programming language, the products member holds a collection of Product instances, such as a Set<Product>.

To recap:

Finally, what has changed in the generated UI compared to the one generated from the example descriptor in the previous section?

3.Refining the user interface

Up to this point, the pages used for creating, viewing, and editing entities have been generated entirely automatically, and the columns displayed in a table have always corresponded to the entity’s members. In this chapter, we will learn how to customize the UI in more flexible ways to fit our requirements more closely.

3.1.Customizing table columns

So far, whenever we wrote a table component, Mike has automatically expanded it to a definition like this one:

table SalesTable: Sale {
    column: id
    column: customer
    column: date
    column: totalAmount
}

By default, a column is included for each member of the entity, except for ones with a @many attribute (due to performance considerations); meanwhile, columns for @primary keys of type UUID are set to hidden by default (as these are generated randomly, and are not meaningful for end users). A default column that refers to an entity will also link to its view page by default (how to accomplish this manually will be described later).

If we prefer, we can also specify a different set of columns to be displayed manually:

main.mike
table SalesTable: Sale {
    column: customer.name
    column: customer.email
    column: date
    column: totalAmount
    column: products.name
}

This will add separate columns for the customer’s name and e-mail address, and will add a new column with a comma-separated list of the names of the products that were sold. When we use a dot, such as in customer.email, this is known as a member path: Sale has a member called customer, and Customer has a member called email, so we can access this sub-member and display it directly.

When a member is @many, as with products, its contents are displayed in the column as a comma-separated list, and the same is true if we access a nested member (here .name) within that @many member, in which case the contents of that member specifically are presented in the list. (Note, however, that displaying the contents of a @many member within another @many member is not currently supported: at most one @many member may be used in each path.)

3.2.Customizing the create page

Whenever we’ve declared a table, Mike has also automatically created associated pages for creating, editing, and viewing instances of that entity type. These can also be explicitly specified using Mike syntax instead, and the default create page for Sale looks like this:

page SaleCreatePage {
    form SaleCreateForm: Sale
}

Much like how the table component is used to display existing instances, the form component is used to create new ones.

We can specify a custom create page like so:

main.mike
page SalesPage {
    table SalesTable: Sale @createPage(NewSalePage)
}

page NewSalePage {
    form NewSaleForm: Sale {
        input: customer 
        input: products
    }
}

Here, all we have changed is that for demonstration purposes, we have removed the input fields for date and totalAmount. We will learn about further customization possibilities in a following chapter.

By default, if no inputs are specified explicitly, then a form will contain an input field for each member of the entity, except for those with a @mappedBy or a @value attribute (which will be introduced in a later chapter), and except for the @primary member if it’s of type Int or UUID (because values for these are provided by the system, not the user). A create form must include an input for every @mandatory member, while for the other members, whether to include them is up to us. (In contrast to columns, inputs do not support nested member paths, only top-level members within the given entity.)

If, for any reason, we want to disable creating new entities in a given table entirely, then instead of a @createPage, we can use @omitted:

main.mike
table SalesTable: Sale @omitted(CREATE)

In this case, the code for creating new entity instances will be completely omitted from the code generated for the table.

3.3.Customizing the edit page

The default edit page Mike creates for us looks like this:

page SaleEditPage requires Sale as sale {
    form SaleEditForm: Sale using sale
}

Two new, and closely related, features can be observed here. Unlike the create page seen earlier, an edit page logically needs to operate on an existing entity instance. To declare that the page is capable of receiving such an entity instance, we write requires Sale, while as sale specifies that, within the page, we can refer to the received Sale instance using the name sale. These are analogous to the type and the name of a function parameter, respectively. When pressing the edit button in a table row (depicted as a pencil icon), it will pass that entity instance to our page while navigating to it.

Continuing on to the form declaration, meanwhile, using signifies that this form will be used to edit an entity instance: in this case, the sale instance received by the encompassing page. A form with a using clause is an edit form, while a form without one is a create form.

Declaring a custom edit page can now be done as we might expect:

main.mike
page SalesPage {
    table SalesTable: Sale @editPage(ModifySalePage)
}

page ModifySalePage requires Sale as sale {
    form ModifySaleForm: Sale using sale {
        input: products
        input: date
        input: totalAmount
    }
}

The effect of this definition is that the customer of a Sale (which we have omitted from among the inputs!) may not be edited after the fact by using the edit button in the SalesTable’s generated UI, only its other members may be.

If we wish to disable the editing of items in a table, we can use @omitted(EDIT) in the same way as with @omitted(CREATE) above; and relatedly, we can also use @omitted(DELETE) to disable their deletion. To disable all three of these at once, we can use the @readonly attribute, which serves as a shorthand for the combination of the three @omitted attributes seen so far.

We can also use @omitted(DELETE) on a form so that the form can only be used to edit entity instances and not to delete them.

3.4.Customizing the view page

The final customizable action page is the view page. Its default definition looks like this:

page SaleDetailsPage requires Sale as sale {
    details SaleDetails: Sale using sale
}

The details element seen here is used for displaying a non-editable detailed view of a single entity instance. As seen earlier with the edit page and edit form, in order to view an entity instance, we need to equip the page with requires Sale as sale to be able to receive it, and we then pass it on to the details element by way of the using sale clause. (For the record, a details element without a using clause is also technically permitted for prototyping purposes, but it has no contents to display and serves no useful function otherwise.)

By default, a details will have a field for every member of the displayed entity, except for the @primary key if it’s of type UUID.

To customize it:

main.mike
page SalesPage {
    table SalesTable: Sale @viewPage(ViewingSalePage)
}

page ViewingSalePage requires Sale as sale {
    details SaleDetails: Sale using sale @editPage(SaleEditPage) {
        field: id
        field: customer.name @link
        field: customer.email @link(ViewingCustomerPage)
        field: date
        field: totalAmount
    }
    table SaleProductsTable: Product using sale.products
}

This example demonstrates several new features:

(If we desired to, we could have chosen custom columns for this table as well, in the same way as before; but for now, we accept the default ones.)

And (as we might expect by this point), if we want to disable the ability to view item details in a table, then we can use @omitted(VIEW) for this purpose.

3.5.Other table customization features

The table and column elements support a number of other features and attributes we haven’t covered so far.

The most significant of these is @searchBar. If specified on a table, its UI will be extended with a text field for searching among the textual contents of the data displayed by the table, and the table will then show only the rows with matching hits. (All of the data existing in the database is searched, not only the parts that are currently visible on the screen.) By default, the search happens in all table columns (including hidden ones!) of type String, plus columns of entity type, in which case those of its String members will be searched which contribute to the entity’s textual representation in the table cells. (By default, this is just the @primary member, but this can be customized using the @display attribute, discussed in the next chapter.)

Alternatively, we can also explicitly specify one or more members or member paths as arguments to the @searchBar attribute, in which case the search will be performed in the contents of those paths. (The paths specified here do not necessarily need to correspond to any of the members or paths that were declared as columns!)

And if we’ve applied the @searchBar attribute, then we can optionally also use @searchBarDefault("example text...") to specify the default contents of the search field.

Along with the optional table-level search bar, the table also has filters on each column which can likewise be used to restrict the table’s contents according to certain criteria, depending on the type of the column: for example, in the case of a Date column, the user can specify a date range. If we don’t want these filters to be present, then we can use @hiddenFilters to hide them.

We can also set individual columns to be hidden by default using the @hidden attribute. The end user can however choose to make them visible again, if they so desire.

And finally, we can also specify a default sort order for the rows of the table by applying the @sorted attribute to one or more columns. If specified on only one column, the table will be sorted according to that column’s contents by default. (As with @hidden, this is only a default, and the end user will be free to re-sort by different columns instead.) We can use the optional @sorted(ASC) or @sorted(DESC) argument to specify whether the order should be ascending or descending (the default is ascending). If we want to sort by multiple columns at the same time, then we need to add a further argument to each @sorted attribute to specify their relative priority: @sorted(ASC, 1), @sorted(DESC, 2), and so on (both arguments must be provided in this case). Rows will initially be sorted according to the column with the highest priority (1) and, in the event of ties between one or more rows, the tie will be broken based on the contents of the next-highest priority column (2, 3, ...), and so on.

4.Customizing UI appearance

In this chapter, we’ll learn how to arrange components into layouts, alter titles and textual display formats, and other fine-tuning to enhance user-friendliness and visual appeal.

4.1.Containers and layouts

As we’ve already seen, a page can contain multiple elements which are arranged vertically by default. We can choose a different arrangement using the @layout attribute:

main.mike
page ViewingSalePage requires Sale as sale @layout(HORIZONTAL) {
    details SaleDetails: Sale using sale
    table SaleProductsTable: Product using sale.products
}

The other available options are:

To nest different layouts within each other, we can use a container:

main.mike
page ViewingSalePage requires Sale as sale {
    details SaleDetails: Sale using sale
    container TablesContainer @layout(VSPLIT) {
        table SaleProductsTable: Product using sale.products
        table OtherSalesToCustomer: Sale using sale.customer.sales
    } 
}

Both containers and pages support the same @layout options (a page can be thought of as having an implicit top-level container). Containers can be nested within each other to arbitrary depth.

4.2.Titles, headings, and icons

By default, the displayed titles of UI elements are determined based on their Mike-level names by splitting them into words at capitalization boundaries (for example, SaleDetails becomes “Sale details”). To choose different titles, we can use the @title attribute:

main.mike
table CustomersTable: Customer {
    column: email @title("E-mail")
    ...
}
form NewCustomerForm: Customer {
    input: email @title("E-mail") @placeholder("jane.doe@example.com")
    ...
}

(This works the same way for fields, as well.)

Incidentally, as also demonstrated above, we can specify placeholder text for input fields by using the @placeholder attribute, which will be shown until the user enters a value.

Since repeating the same title on every element where a given entity member is displayed may grow tiresome, we also have the option of specifying the title directly within the entity itself:

main.mike
entity Customer {
    email: String @primary @title("E-mail")
    ...
}

In this case, @title("E-mail") will be used as the default title on all columns, fields, and inputs that display the contents of the email member; but we can still override it on a case-by-case basis with a @title attribute directly on the UI element, if needed.

In the case of nested member paths, such as field: customer.email, the default title is derived from the last path component, i.e., in this case it would be either “E-mail” or “Email”, depending on whether we’ve applied @title("E-mail") to the email member as shown above.

We can also use @title on container, table, details, and form elements, but this will only have an effect when they are contained within a page or container that has a TABBED layout, as described earlier.

We can also apply titles to pages and navigation items:

main.mike
page CustomersPage @title("List of customers") { ... }

page ProductsPage @title("All products") { ... }

navigation {
    item: CustomersPage @title("Customers")
    item: ProductsPage @title("Products")
}

The specified page title will appear in the browser’s tab bar or title bar. If we omit the @title for an item, but do provide one for the page it references, then the item’s title will also be taken from the page’s title by default.

Navigation items can also have icons, specified using the @icon attribute:

main.mike
item: ProductsPage @icon("fas:box-full")

Icon names are specified using the FontAwesome format.

And finally, we can add additional heading elements to “block-level” UI components by using the @heading attribute:

main.mike
table CustomersTable: Customer @heading("All Customers")

The @heading attribute is supported for navigation, page, container, table, details, and form declarations. If a heading is specified, then the element’s title will also use the same text by default. This is primarily relevant for pages and for elements within TABBED layouts.

When using @heading on or within a page that has a requires ... as ... clause, we also have the option of expanding it into a template string using reference path parameters:

main.mike
page ViewingSalePage requires Sale as sale {
    details SaleDetails: Sale using sale @heading("Sale to {0} on {1}", sale.customer.name, sale.date)
}

(Reference paths were also discussed earlier in the context of using clauses.)

This will display a heading of the form “Sale to Bob Smith on 2021-03-16”. Template strings are further described in the next section.

4.3.Display formats

Whereas titles and headings are displayed alongside data, we can customize the display format of the data itself using the @prefix, @suffix, and @display attributes:

main.mike
table SalesTable: Sale {
    column: customer @display("{0} ({1})", name, email)
    column: totalAmount @suffix(" USD")  # (for example)
}

The @display attribute can only be used with entity types, while @prefix and @suffix are only valid for non-entity types. All three attributes are available for columns, fields, as well as inputs.

As might be expected, @prefix and @suffix prepend or append (respectively) the given text to the displayed value. They can also be used together.

Meanwhile, @display substitutes its parameters into the provided template string: {0} is replaced by the first parameter, {1} by the second, and so on. The parameters are provided in the form of member paths, where the first path component is a member of the entity being displayed: in the above example, that is Customer, since that’s the type of the column the @display attribute is used on. (And not Sale, which is the type for the entire table!).

As we have already seen with @title, since specifying @display separately on each column, field, and input can be repetitive, it can also be specified at the entity level:

main.mike
entity Customer @display("{0} ({1})", name, email) {
    email: String @primary @title("E-mail")
    name: String!
    ...
}

entity Sale @display("Sale to {0} on {1}", customer.name, date) {
    id: Int @primary
    customer: Customer!
    date: Date
    ...
}

And likewise as with @title, the entity-level @display will then be used as the default, but can still be overridden on the individual columns, fields, and inputs if needed.

4.4.Language resource files

Every textual string displayed in the UI is added to the language resource files, typically located at src/main/resources/messages.properties in the generated project. These can be used to localize (translate) the application into different languages. For more details on that process, see here. In the mike/config.yaml file, we can specify which localizations we want our application to be made available in using the languages key, in which case additional language-specific resource files will be generated for those languages as well. (Note that Mike only creates these files, but doesn’t, at the moment, also translate their contents for us.)

When @title and @heading are used explicitly in the descriptor, then the strings specified in their arguments are the ones added to the language resource files. Meanwhile, in cases where a default title is displayed even in the absence of an explicit @title or @heading, then that default title is added to the language resource files instead! As a consequence, rather than specifying a @title at the Mike level, if we prefer, we can also achieve a similar effect by editing the default string in the messages.properties file.

4.5.Themes and colors

In the aforementioned mike/config.yaml file, we can also use the theme key to choose from one of three predefined color themes for our application: LUMO (the Vaadin default), MIKE, or MYSTIC_SANDS. We can also opt for a custom theme using CUSTOM.

In the latter case, our custom colors can be specified using the customTheme key. Under customTheme / font, we can specify the primary and base colors to be used for text, while under customTheme / scheme, the specified base, primary, and shade colors will be used for backgrounds.

Here is an example of a config.yaml file with a custom theme:

config.yaml
mainMikeFile: main.mike
java:
  package:
    base: com.yourpackage.app
projectName: test-project
theme: CUSTOM
customTheme:
  font:
    primary: '#BD1111FF'
    base: '#448769FF'
  scheme:
    primary: '#565C44FF'
    base: '#444444FF'
    shade: '#D5D1B2FF'

5.User accounts and personalized views

Applications generated by Mike come ready with built-in features for creating, managing, and logging in with user accounts. In this chapter, we will see how to link application data with these user accounts, and how to create personalized views showing the data from the currently logged-in user’s perspective.

5.1.Users as entities

Suppose we also want to represent in our application the sales representatives managing the relationship with each customer. We can declare a SalesRep entity type and reference it from the Customer and Sale entity types like so:

main.mike
entity SalesRep @display("{0} ({1} {2})", username, firstName, lastName) {
    username: String! @primary
    email: String!
    firstName: String!
    lastName: String!
}
entity Customer {
    ...
    salesRep: SalesRep!
}
entity Sale {
    ...
    soldBy: SalesRep!
}

At the level of a database schema, this definition makes sense. However, there is an issue: at the level of the whole application, the SalesRep declared this way is “just a normal entity”, and is completely independent of the actual user accounts used by sales representatives to log in and perform their activities! At best, the two could be kept in sync very tediously by always creating a corresponding SalesRep instance whenever a user account is added for a new member who joins the team, and likewise always editing and deleting them in tandem.

We can instead fuse the two into one by using the @userAccount attribute:

main.mike
entity SalesRep @userAccount @display("{0} ({1} {2})", username, firstName, lastName) {
    firstName: String
    lastName: String
}

This has two primary effects:

  1. Each user account in the system now directly is a SalesRep instance. This has some implications: they can be created through the built-in user management facilities, not Mike-level create forms (because of the need for setting a password and for email verification); and as a further consequence, members in such an entity may not be @mandatory unless they have a @default (since the built-in user creation page would have no way to initialize their values; see the following chapter for details on @default).

  2. Three additional members are now implicitly part of the SalesRep entity type:

    id: UUID @primary
    username: String! @unique
    email: String! @unique

    (A UUID is a universally unique identifier; @unique will also be discussed in the next chapter.)

    These are the members which are configured through the built-in user management page, but which are also available through the Mike descriptor. For practical purposes, it’s the same as if we had written the same member declarations ourselves: they can be used just like normal members (see, e.g., username within @display, above). The one exception is that they cannot be used for inputs in an edit form: edits must be performed using the built-in pages, due to email verification and similar concerns.

As alluded to above, in effect, one half of the SalesRep entity can be managed through the application’s predefined user management facilities (e-mail, username, password, roles, and blocked status); whereas the remainder, which we explicitly declare as part of SalesRep in the descriptor, can be managed through Mike-level views and pages, as with any other entity.

5.2.The user’s point of view

Now that we have appropriate entity definitions, let’s turn our attention to the UI. A sales representative doesn’t want to see a table of all customers in the system: they want to see the customers that they, specifically, are in contact with. We can accomplish this like so:

main.mike
data {
    ...
    entity SalesRep @userAccount {
        ...
        customers: [Customer] @mappedBy(salesRep)
        salesMade: [Sale] @mappedBy(soldBy)
    }
}
ui {
    navigation @heading("Logged in as: {0} {1}", SalesRep.CURRENT.firstName, SalesRep.CURRENT.lastName) {
        item: DashboardPage
        item: ProfilePage
    }
    page DashboardPage @home @title("Dashboard") {
        table: Customer using SalesRep.CURRENT.customers @title("Customer relationships")
        table: Sale using SalesRep.CURRENT.salesMade @title("Recent sales") {
            column: date @sorted(DESC)
            column: customer
            column: totalAmount
        }
    }
    page ProfilePage @title("Edit profile") {
        form: SalesRep using SalesRep.CURRENT {
            input: firstName
            input: lastName
        }
    }
}

The key feature here is SalesRep.CURRENT: in any position where a reference path can be provided -- so, in using clauses and in the parameters of a @heading -- we can use this syntax to refer to the SalesRep who is currently logged into the application. By also adding @mappedBy members to SalesRep collecting the customers and sales which they are involved with, we can then create tables showing exactly these.

Similarly, we can enable the sales representative to edit their own profile information by specifying using SalesRep.CURRENT itself as the edit form’s path. (We could do likewise with details; and, in both cases, with any hypothetical non-@many member of the SalesRep, such as a SalesRep.CURRENT.manager, for example.)

5.3.Re-use of edit and view pages

Now suppose we also want a page for administrative control over SalesReps:

main.mike
navigation {
    ...
    item: SalesRepsPage
}
page SalesRepsPage {
    table: SalesRep @editPage(EditSalesRepPage)
}
page EditSalesRepPage requires SalesRep as salesRep {
    form: SalesRep using salesRep {
        input: firstName
        input: lastName
    }
}

We may be bothered (especially in a real-world context, where definitions are more elaborate than these) by the duplication between EditSalesRepPage and the earlier ProfilePage: they’re almost the same, except one has using salesRep, where salesRep is a parameter of the page, while the other has using SalesRep.CURRENT.

If we want to use the same page for both purposes, we can:

main.mike
navigation {
    ...
    item: EditSalesRepPage using SalesRep.CURRENT @title("Edit profile") 
}

The value retrieved at runtime from the using path will be passed as the page’s requires parameter. We can do likewise with any page having a requires, and any non-@many member or path of a matching type that can be indirectly accessed through .CURRENT.

5.4.Users with different roles

What if there isn’t just one kind of user using the application -- what if, for example, sales representatives and their managers are both present in the system? In this case, the recommended approach is to put the relevant members (which they don’t have in common) not into the @userAccount type directly, but into separate entity types which refer back to it:

main.mike
entity User @userAccount {
    firstName: String
    lastName: String
    asSalesRep: SalesRep @mappedBy(user)
    asManager: Manager @mappedBy(user)
}
entity SalesRep {
    user: User!
    manager: Manager!
    customers: [Customer] @mappedBy(salesRep)
    salesMade: [Sale] @mappedBy(soldBy)
}
entity Manager {
    user: User!
    managedTeam: [SalesRep] @mappedBy(manager)
}

Notably, this pattern also permits a User to be both a SalesRep and a Manager, in case this flexibility is ever needed.

5.5.The predefined User type

If we don’t manually declare an entity type with the @userAccount attribute, then an additional built-in type called User is implicitly available, with its definition being simply:

entity User @userAccount

That is, with just the automatic id, username, and email members already mentioned above. This may be convenient if we only want to refer to user accounts from other entity types (for example, in conjunction with the @value attribute discussed in an upcoming section), but don’t need user accounts to have references to any other entity types.

5.6.A note on the security configuration option

We can change the authentication technology used by our application using the security option in the config.yaml configuration file. One of the available options is "NONE", in which case features related to user management will be entirely omitted from the generated application’s codebase. In this case, unsurprisingly enough, the User and User.CURRENT features will not be available either.

6.Specialized data modelling features

In this chapter, we return to the data section of the descriptor, and review the remainder of Mike’s data modelling features.

6.1.Additional built-in types

We have already seen many of the built-in types in action:

The two remaining ones are:

6.2.Enums

As in many programming languages, we can introduce new enumerated types:

main.mike
entity Customer {
    status: CustomerStatus!
    ...
}

enum CustomerStatus {
    LEAD
    ACTIVE
    VIP @display("VIP")
    INACTIVE
    FORMER
}

In the CRM context, we can use this to keep track of the current status of our relationships to our customers. To specify the displayed form of the values, we can use @display: by default, names are converted to “Sentence case” just as with titles, so in this case, the name VIP would have resulted in the default textual form “Vip”, which is not ideal, since it’s an acronym.

If needed, we can also customize the enum’s database storage type and representation:

main.mike
enum CustomerStatus: Int {
    case LEAD @storedAs(100)
    case ACTIVE @storedAs(500)
    case VIP @display("VIP") @storedAs(1000)
    case INACTIVE @storedAs(50)
    case FORMER @storedAs(10)
}

The two possible storage types are Int and String. The default, if not otherwise specified (as we’ve done using CustomerStatus: Int above), is String.

The @storedAs attribute specifies the database representation for an individual case (which is the optional keyword for enum members). If the storage type is Int, then by default, they would be stored as 0, 1, 2, and so on, in the same order as listed in the enum. The storage type must be explicitly specified in order to use @storedAs, and if the storage type is Int, then (to avoid potential ambiguities) @storedAs must be specified for either all of the cases or for none of them. If the storage type is String, then whether to use @storedAs can be chosen on a case-by-case basis, and the default representation is simply the name of the case in textual form (e.g., "ACTIVE").

6.3.Constraints and indexes

Although in our CRM system, customers are primarily identified by their e-mail addresses, we may also wish to enforce that two customers can’t have the same phone number. We can do this using a @unique attribute:

main.mike
entity Customer {
    email: String @primary
    phoneNumber: String @unique
    ...
}

In some cases, we want the combination of the values of multiple members to be unique across the entire database. For example, if we were to separately record a Product entry for each released version of a given product, then it would be acceptable for two Products to have the same name (as long as their versions differ), or of course the same version (provided they have different names), but if both of these were to coincide, that would be a problem. We can represent this constraint using a composite @unique constraint at the entity level:

main.mike
entity Product @unique(name, version) {
    id: UUID @primary
    name: String!
    version: Int!
    ...
}

In fact, when @unique is specified on a single member such as phoneNumber above, this is merely shorthand for @unique(phoneNumber) on the entity.

(In principle, we might want to go even further and actually identify products by the combination of their name and version, which would correspond to a composite @primary constraint. Unfortunately, these are not yet supported. The above solution using @unique is, however, equivalent for most practical purposes.)

When used on a member that refers to an entity, @unique gives rise to a one-to-one relationship:

main.mike
entity Customer {
    billingInfo: BillingInfo @mappedBy(customer)
    ...
}

entity BillingInfo {
    id: UUID @primary
    customer: Customer! @unique
    billingAddress: String!
    creditCardLast4: String!
    paymentMethod: PaymentMethod!
}

(As before, declaring the @mappedBy side of the relationship is optional.)

Observe that since BillingInfo.customer is specified as @unique, there can be at most one BillingInfo object that refers to any given Customer, and therefore, the type of Customer.billingInfo is also simply BillingInfo (scalar!), rather than [BillingInfo], as it would have to be in the absence of @unique. (For most purposes, which side of the relationship is the @unique one, and which the @mappedBy one, can be chosen arbitrarily. One notable difference is that only the @unique side can be made @mandatory, because on the @mappedBy side, we can’t require another entity instance, referring back to this one, to actually exist in the database.)

In a similar manner to how @unique constraints are declared, we can also request the creation of database indexes for given members or combinations of members. For example, supposing we needed to store a large number of Sale instances and frequently query them by date:

main.mike
entity Sale {
    date: Date @indexed
    ...
}

(This attribute is only for optimization purposes, and doesn’t affect the behavior of the application in any way beyond its performance characteristics.)

Note that @unique and @primary already imply the creation of database indexes for those members, so manually specifying @indexed as well is unnecessary. And while we omit providing an example here, the composite @indexed(member1, member2, ...) attribute on entities can be used in a completely analogous way to the composite @unique(...) described earlier, as well.

6.4.Default and system-computed values

If an entity member has a sensible default value, we can specify it using the aptly named @default attribute:

main.mike
entity Customer {
    status: CustomerStatus! @default(LEAD)
    ...
}

entity Product {
    version: Int! @default(1)
    ...
}

These defaults will be reflected as the default values for input fields in create forms, and will be stored to the database if the end user doesn’t select a different value. The attribute’s argument must be a value of the same type as of the given member. In the case of enums, this takes the form of the name of the chosen case.

For members which should have their values determined entirely by the system, rather than by the end user, we can use the @value attribute:

main.mike
entity Customer {
    lastModified: DateTime @value(MODIFIED_AT)
    ...
}

entity Product {
    discontinued: Date @value(DELETED_AT)
    ...
}

entity Sale {
    soldBy: SalesRep @value(CREATED_BY)
    date: Date @value(CREATED_AT)
    totalAmount: Float @value(COMPUTED)
    ...
}

There are currently eight supported arguments: CREATED_AT, MODIFIED_AT, DELETED_AT, CREATED_BY, MODIFIED_BY, DELETED_BY, IS_DELETED, and COMPUTED.

The first three of these will store the date/time when, logically enough, the entity instance was first created, last modified (including initial creation, if there have been no changes since then), or soft deleted, respectively. (When using hard deletion, of course, there would be no object or record left to store the value in!) All three of these can be used with members of type Date as well as DateTime. In the case of DELETED_AT, the member will be null until it is deleted, while CREATED_AT and MODIFIED_AT both implicitly activate the @mandatory attribute as well.

The next three, suffixed with _BY, work completely analogously, except that they store the user who performed the operation, rather than the time when it happened. In this case the member’s type must be either the built-in User type, or the manually-declared entity that has a @userAccount attribute. Meanwhile, @value(IS_DELETED) is analogous to DELETED_AT, except that it can (only) be used on members of type Bool, and will result in its value being set from false to true when soft deletion occurs.

Each of these attributes may be used at most once per entity.

The @value(COMPUTED) attribute is similar to the others in the sense that the member’s value is system-determined, except that in this case, the procedure to do so isn’t provided directly by Mike. Instead, this attribute is meant to be used when we’ve written a procedure to determine the member’s value in the application’s generated code ourselves (in the above example, by summing the prices of the sold products). The primary effect of this attribute is that the member will not be present in the default create and edit forms. (N.B., if we want to use @value(COMPUTED) together with @mandatory, we must also provide a @default value for technical reasons, so that the object is possible to construct.)

7.Working with generated code

7.1.Merges and conflicts

After Mike generates code for us, we can develop this code further ourselves to better suit our individual requirements. And at the same time, we can also continue to modify the Mike descriptor to make high-level changes in the application, and then have Mike re-generate the code for it.

If we modify both the descriptor and the generated code, the results are then merged together. (If you’ve used a version control system such as git before, then you may already be familiar with this process.) Conceptually, it’s as if Mike first checks the current state of the generated code to see what changes we’ve made since the last time Mike generated it for us, then generates a “clean” copy based on the newest version of the descriptor file, and finally, re-applies our custom changes on top of the new version of the generated code.

If the changes made by us and by Mike affect separate parts of the code, then we have nothing further to do! However, if both changes affect the same lines, then this results in a conflict. (Again, those with prior experience using version control systems may already be familiar with this phenomenon.)

In the event of a conflict, Mike will produce a message reminding us what to do:

[CONFLICTS]
   src/main/java/com/yourpackage/app/entity/Test.java

Please fix these conflicts, then run `mike resolve`.
(Or to undo the `mike generate` command and reset the code to its previous state, run `mike abort`.)

Suppose the conflict arose because in the generated code, in the Test.java file, we changed the type of the value member from Integer to Double, while, in the Mike descriptor, we changed its name from value to distance. Then, after running mike generate, the file will end up containing sections like this one:

src/main/java/com/yourpackage/app/entity/Test.java
<<<<<<< HEAD
public Double getValue() {
   return value;
}
public void setValue(Double value) {
=======
public Integer getDistance() {
   return distance;
}
public void setDistance(Integer distance) {
>>>>>>> 1a58548c875425631e65c8948e14dda433e025cc

The <<<<<<<, =======, and >>>>>>> lines are known as conflict markers and divide this section of code into two versions: above, with the changes we made manually, and below, with the changes that Mike made in the generated code as a result of us changing the descriptor. In this particular case, to correctly resolve the conflict, we need to rewrite the code to incorporate both changes at the same time, and then remove the conflict markers:

src/main/java/com/yourpackage/app/entity/Test.java
public Double getDistance() {
   return distance;
}
public void setDistance(Double distance) {

(In other situations, it may also happen that we want to keep one or the other set of changes in their entirety, and throw the other one away.)

After we’ve fixed all sections of the code that had conflict markers in the same way, then the last step is to run mike resolve, which will finalize the results of the initial mike generate command which had detected and reported the conflict.

Alternatively, if, upon seeing the conflict, our reaction is to go “oops” because that wasn’t supposed to happen, then we can run mike abort to restore the state of the code to what it was before we ran the mike generate command. (This affects only the generated code, not the Mike file. In the absence of other changes, if we were to run mike generate again, then the same conflict would most likely reoccur.)

7.2.Interoperating with pre-existing code

Suppose we have some hand-written Java classes for Hibernate entities, and/or some pre-existing Java enum types, and we want to use these together with Mike. That is, we already have, for example, a Customer class in Java, and we don’t want Mike to generate that for us, but we do want to use the Customer type on the Mike side, to display a table of Customers in the UI, for example.

This is what the @external attribute is for. If we apply this attribute to an entity or an enum declaration, then Mike will skip generating the code for it, but will still generate the code for every other part of the application as if that type’s code had also been generated. So, when using this attribute, we need to take special care that our own class’s or enum’s definition is compatible with the conventions and APIs used by Mike-generated code!

7.3.Structure of the generated code

To find your way around the generated code, it may be helpful to consult the demo CRM project developer documentation.

Footnotes
  1. Nothing forbids having, e.g., customer1: Customer and customer2: Customer both within the same entity, so @mappedBy requires specifying which member is involved explicitly.