Linz.js

Linz is a framework for creating administration interfaces. Linz is not a CMS, but is capable of CMS like functionality. Linz is a good choice of framework when the administration interface is the website itself.

Linz is built on Node.js, Express and MongoDB.

Linz is quite new and under rapid development. It is used quite successfully in a number of production sites however. This documentation is the first effort in making this open source project accessible to more developers.

Getting started with Linz

This will help you create a new Linz-based website. If you’d like to develop Linz itself, see Getting started with Linz development.

While we’re working on our documentation, you can get started with Linz via our example project, see mini-twitter.

Linz tries to force as little new syntax on you as possible. Of course, this is unavoidable in certain situations, and there are some conventions you’ll need to learn. We’ve tried to keep them as simple as possible.

Linz does make use of many other open source tools, such as Mongoose or Express. Linz tries to keep the usage of these tools as plain and simple as possible. Linz doesn’t wrap, customise or prettify syntax for other libraries/tools used within Linz. The three primary opensource tools that Linz relies on are:

  • Express
  • Mongoose
  • Passport

The following will be a general overview of some of the core concepts of Linz.

Singleton

When you require Linz, you’re returned a singleton. This has the advantage that no matter where you require Linz, you get the same Linz instance.

Initialization

Linz must be initialized. During initialization, Linz can optionally accept any of the following (in any order):

  • An initialized Express intance.
  • An initialized Passport instance.
  • An initialized Mongoose instance.
  • An options object to customise Linz.

For example:

var express = require('express'),
    mongoose = require('mongoose'),
    passport = require('passport'),
    linz = require('linz');

linz.init(express(), mongoose, passport, {
  'load configs': false
});

If neither an initialized instance of Express, Passport or Mongoose, nor an options object have been passed, Linz will create them:

// use anything that is passed in _app = _app || express(); _mongoose = _mongoose || require(‘mongoose’); _passport = _passport || require(‘passport’); _options = _options || {};

Options object

An object can be used to customize Linz. For example:

linz.init({
  'mongo': `mongodb://${process.env.MONGO_HOST}/db`
});

For a complete list of customizations you can make, view Linz’s defaults.

Events

The Linz object is an event emitter, and will emit the initialized event when Linz has finished initializing.

It will also emit an event whenever a configuration is set (i.e. using the linz.set method). The name of the event will be the same as the name of the configuration that is set.

A common pattern for setting up Linz, using the event emitter, is as follows:

server.js:

var linz = require('linz');

linz.on('initialised', require('./app'));

// Initialize Linz.
linz.init({
  mongo: `mongodb://${process.env.DB_HOST || 'localhost'}/lmt`,
  'user model': 'mtUser'
});

app.js:

var http = require('http'),
  linz = require('linz'),
  routes = require('./routes'),
  port = process.env.APP_PORT || 4000;

module.exports = function () {

  // Mount routes on Express.
  linz.app.get('/', routes.home);
  linz.app.get('/bootstrap-users', routes.users);

  // Linz error handling midleware.
  linz.app.use(linz.middleware.error);

  // Start the app.
  http.createServer(linz.app).listen(port, function(){
    console.log('');
    console.log(`mini-twitter app started and running on port ${port}`);
  });

};

Directory structure

Linz expects a common directory structure. If provided, it will load content from these directories. These directories should live alongside your Node.js entry point file (i.e. node server.js).

  • models: a directory of model files.
  • schemas: a directory of schemas, which are used as nested schemas within a model.
  • configs: a directory of config files.

You can read more about each of the above and what Linz expects in the documentation covering each area.

Models

One of the primary reasons to use Linz is to ease model development and scaffold highly customizable interfaces for managing these models. Linz provides a simple DSL you can use to describe your model. Using the content of your DSL, Linz will scaffold an index, overview, edit handlers and provide a basic CRUD HTTP API for your model.

All Linz models are bootstrapped with two mandatory properties:

  • dateCreated with a label of Date created.
  • dateModified with a label of Date modified.

You create Models in the model directory, one file per model. The file should have the following basic structure:

person.js:

var linz = require('linz');

// Create a new mongoose schema.
var personSchema = new linz.mongoose.Schema({
  name:  String,
  email: String
});

// Add the Linz formtools plugin.
personSchema.plugin(linz.formtools.plugins.document, {
  model: {
    label: 'Person',
    description: 'A person.'
  },
  labels: {
    name: 'Name',
    email: 'Email'
  },
  list: {
    fields: {
      name: true,
      email: true
    }
  },
  form: {
    name: {
      fieldset: 'Details',
      helpText: 'The users full name.'
    },
    email: {
      fieldset: 'Details'
    }
  },
  overview: {
    summary: {
      fields: {
        name: {
          renderer: linz.formtools.cellRenderers.defaultRenderer
        },
        email: {
          renderer: linz.formtools.cellRenderers.defaultRenderer
        }
      }
    }
  },
  fields: {
    usePublishingDate: false,
    usePublishingStatus: false
  }
});

var person = module.exports = linz.mongoose.model('person', personSchema);

The file is broken down in the following parts:

  • You require Linz, as you’ll need to register the model with Linz.
  • Create a standard Mongoose schema.
  • Use the linz.formtools.plugins.document Mongoose plugin to register the model with Linz, passing in an object containing Linz’s model DSL.
  • Create a Mongoose model from the schema, and export it.

Mongoose schemas

Linz works directly with Mongoose schemas. Anything you can do with a Mongoose schema is acceptable to Linz.

Model DSL

Linz uses a Model DSL, which is an object that can be used to describe your model. Linz will use this information to scaffold user interfaces for you. The Model DSL contains six main parts:

  • model contains basic information such as the label and description of the model.
  • labels contains human friendly versions of your model’s properties, keyed by the property name.
  • list contains information used to scaffold the list displaying model records.
  • form contains information used to scaffold the edit handler for a model record.
  • overview contains information used to scaffold the overview for a model record.
  • fields contains directives to enable/disable fields that Linz automatically adds to models.
  • permissions is a function used to limit access to a model.

You supply the DSL to Linz in the form of an object, to the linz.formtools.plugins.document Mongoose plugin:

personSchema.plugin(linz.formtools.plugins.document, {
  model: {
    // ...
  },
  labels: {
    // ...
  },
  list: {
    // ...
  },
  form: {
    // ...
  },
  overview: {
    // ...
  },
  fields: {
    // ...
  },
  permissions: function () {
  }
});

Models model DSL

model should be an Object with two keys label and description. The label should be a singular noun describing the model, and the description a short sentence describing the noun.

The label is used in many places and is automatically pluralized based on the usage context. The description is only used on the Models index within Linz.

For example:

model: {
  label: 'Person',
  description: 'A person.'
}

Models label DSL

labels is used to provide a label and description for the model.

labels should be an Object, keyed by field names and strings of the human friendly versions of your field names.

For example:

labels: {
  name: 'Name',
  email: 'Email'
}

You can customize the labels for the default dateModified and dateCreated using this object.

Models list DSL

list is used to customize the model index that is generated for each model.

list should be an Object, containing the following top-level keys:

  • actions
  • fields
  • sortBy
  • toolbarItems
  • showSummary
  • filters
  • paging
  • groupActions
  • recordActions
  • export

These allow you to describe how the model index should function. The list DSL is discussed in more detail in List DSL.

Models form DSL

form is used to customize the model record create and edit pages.

form should be an Object, keyed by field names of the model, in the order you’d like each field’s edit control rendered. For example:

form: {
  name: {
    fieldset: 'Details',
    helpText: 'The users full name.'
  },
  email: {
    fieldset: 'Details'
  }
}

This will generate a form with two fields that you can provide data for. Both fields will appear in the Details fieldset, in the order name and then email.

Each field object can contain the following keys:

  • label
  • placeholder
  • helpText
  • type
  • default
  • list
  • visible
  • disabled
  • fieldset
  • widget
  • required
  • query
  • transform
  • transpose
  • schema
  • relationship

These allow you to describe how the create and edit forms should function. The form DSL is discussed in more detail in Form DSL.

Model permissions

Model permissions is an in-depth topic and should be considered amongst other permission capabilities. Read more about Permissions.

Model statics, virtuals and methods

When working with models, Linz makes use of specific Mongoose statics, virtuals and methods if they’ve been provided.

The following documents them, and their functionality.

listQuery static

You can create a static called listQuery for a model with the following signature:

function listQuery (query, callback)

If found, Linz will execute this function with a Mongoose query before executing it, when retrieving data for the model list view. This provides an opportunity to customise the query before execution.

For example, if you’d like to return more fields from MongoDB than those listed in list.fields you can do it here:

model.static.listQuery = listQuery (query, callback) => callback(null, query.select('anotherField anotherOne'));

Permissions

Linz has a unique permissions model, mostly due to the fact that it assumes nothing (well, nothing that you can’t alter anyway) about your user model; only that you have one.

Many frameworks define a user model that you must adhere to. Linz doesn’t. This provides an opportunity for a simplified yet highly flexible permissions model.

Permissions can be provided for both models and configs.

There are a few contexts you should be aware of.

The full scope of contexts are:

  • In the context of all models:
    • models.canList
  • In the context of a particular model:
    • model.canCreate
    • model.canDelete
    • model.canEdit
    • model.canList
    • model.canView
  • In the context of all configs:
    • configs.canList
  • In the context of a particular config:
    • config.canEdit
    • config.canList
    • config.canReset
    • config.canView
    • config.canView

Linz enforces permissions in two places:

  • The UI
  • A route execution

Linz will not render buttons, links to or actions for functionality that a user doesn’t have access to. Routes are completely protected. So even if a route was discovered, a user without permissions would not be able to resolve it.

Default permissions

This is Linz’s default permissions implementation:

function (user, context, permission, callback) {
  return callback(true);
}

In short, there are no permissions.

Global permissions

Linz implementations can provide a function (in the options object when initializing Linz) called permissions. It should have the signature:

function permission (user, context, permission, callback)

This function will be called whenever Linz is evaluating the models.canList and configs.canList. Most commonly when generating navigation for a user, but also on the models list and configs list pages.

The user is the user making the request for which permissions are being sought.

The context will be either a string, or an object. If it is a string, it will be either:

// In the context of all models
'models'

// In the context of all configs
'configs'

// In the context of a particular model
{
  'type': 'model',
  'model': 'modelName'
}

// In the context of a particular config
{
  'type': 'config',
  'config': 'configName'
}

The permission will be one of the following strings:

  • canCreate
  • canDelete
  • canEdit
  • canList
  • canReset (configs only)
  • canView

The callback accepts the following signature:

function callback (result)

result is a boolean. Please note, this is different from the standard Node.js callback signature of function callback (err, result). You should design your function so that it returns false in the event of an error and logs the error for a post-mortem.

Throwing errors and failing at the point of checking permissions would not be a good look for anyone, hence the design to not provide this capability. This is something that needs to be handled by a developer.

Model and config permissions function

Determining permissions for models and configs is more contextually sensitive. To do this, when defining a model or config, you can also provide a permissions key.

The key can have a value of either an object or a function. If an object is provided, it is used directly. If a function is provided, you have the benefit of knowing which user the permissions are being requested for. A function should have the following signature:

function modelPermission (user, callback)

The callback accepts the following signature:

function callback (err, result)

err should be null if no error occurred. If an error has occurred, you can return it to the callback which will then default the result to false. result should be an object.

The result object should contain, optionally, the following keys with boolean values:

  • canEdit
  • canDelete
  • canList
  • canCreate
  • canView

Each key is optional, and defaults to true if not provided. Linz evaluates the values with the === operator so an explicit false must be provided to limit permissions.

List DSL

The Models list DSL is used to customise the model index that is generated for each model. The list DSL has quite a few options, as the model index is highly customizable.

list should be an object, containing the following top-level keys:

  • actions
  • fields
  • sortBy
  • toolbarItems
  • showSummary
  • filters
  • paging
  • groupActions
  • recordActions
  • export

These allow you to describe how the model index should function.

list.actions

list.actions should be an Array of Objects. Each object describes an action that a user can make, at the model level. Each action should be an Object with the following keys:

  • label is the name of the action.
  • action is the last portion of a URL, which is used to perform the action.

For example:

actions: [
  {
    label: 'Import people',
    action: 'import-from-csv'
  }
]

This will generate a button, on the model index, next to the model label. Multiple actions will produce a button titled Actions with a drop-down list attached to it, containing all possible actions.

The evaluated string /{linz-admin-path}/model/{model-name}/action/{action.action} will be prefixed to the value provided for action to generate a URL, for example /admin/model/person/import-from-csv. It is the developers responsibility to mount the GET route using Express, and respond to it accordingly.

The actions will be rendered in the order they’re provided.

list.fields

list.fields is used to customize the fields that appear in the listing on the model index.

list.fields should be an Object, keyed by each field in your model. The value for each key should be true to include the field or false to exclude the field. For example:

fields: {
  name: true,
  username: true
}

Linz will convert the above into the following:

fields: {
  name: {
    label: 'Name',
    renderer: linz.formtools.cellRenderers.default
  },
  username: {
    label: 'Username',
    renderer: linz.formtools.cellRenderers.default
  }
}

If you like, you can pass an object rather than the boolean. This also allows you to customize the cell renderer used to display the data within the column.

If you provide a label, it will override what is defined in the Models label DSL.

The fields will be rendered in the order they’re provided.

list.sortBy

list.sortBy is used to customise the sort field(s) which the data in the model index will be retrieved with.

list.sortBy should be Array of field names, for example:

sortBy: ['name', 'username']

This Array will be used to populate a drop-down list on the model index. The user can choose an option from the drop-down to sort the list with.

list.toolbarItems

list.toolbarItems can be used to provide completely customised content on the toolbar of a model index. The toolbar on the model index sits directly to the right of the Model label, and includes action buttons and drop-downs.

list.toolbarItems should be an Array of Objects. Each object should provide a render key with the value of a Function. The function will be executed to retrieve HTML to be placed within the toolbar. The function will be provided the request req, the response object res and callback function which should be executed with the HTML. The callback function has the signature callback(err, html) For example:

toolbarItems: [
  {
    renderer: function (req, res, cb) {

      let locals = {};
      return cb(null, templates.render('toolbarItems', locals));

    }
  }
]

list.showSummary

list.showSummary can be used to include or exclude the paging controls from a model index.

list.showSummary expects a boolean. Truthy/falsy values will also be interpreted, for example:

showSummary: true

list.filters

list.filters can be used to include filters which will alter the data included in the dataset for a particular model. Filters can contain a custom user interface, but Linz comes with a standard set of filters.

list.filters should be an Object, keyed by each field in your model. Each Object must contain a filter, which should be an object adhering to the Linz model filter API. For example:

filters: {
  dateModified: {
    filter: linz.formtools.filters.dateRange
  }
}

The following will allow your model to be easily filtered by a date range filter, on the dateModified property. For a complete list of the filters available see https://github.com/linzjs/linz/tree/master/lib/formtools/filters.

list.paging

list.paging can be used to customise the paging controls for the model index. Paging controls will only be shown when the number of results for a model index, are greater than the per page total.

list.paging should be an Object, with the following keys:

  • active is an optional Boolean used to turn paging on or off. It defaults to true.
  • size is the default page size. It defaults to 20.
  • sizes is an Array of the page sizes available for a user to choose from on the model index. It defaults to [20, 50, 100, 200].

For example:

paging: {
  active: true,
  size: 50,
  sizes: [50, 100, 150, 200]
}

If you don’t provide a paging object it defaults to:

paging: {
  active: true,
  size: 20,
  sizes: [20, 500, 100, 200]
}

list.groupActions

list.groupActions can be used to define certain actions that are only available once a subset of data has been chosen.

Each record displayed on a model index has a checkbox, checking two or more records creates a group. If groupActions have been defined for that model, those actions will become chooseable by the user.

list.groupActions should be an Array of Objects. Each object describes an action that a user can make, and the object takes on the same form as those described in list.actions.

You’re responsible for mounting a GET route in Express to respond to it.

list.recordActions

list.recordActions can be used to customise record specific actions. These are actions that act upon a specific model record. They appear in a drop-down list for each record in a model list.

list.recordActions should be an Array of Objects. Each object describes an action that a user can make, specific to the record, and the object takes on the same form as those described in list.actions.

list.recordActions can also accept a function, as the value to a disabled property. If provided, the function will be excuted with the following signature disabled (record, callback).

The callback has the following signature callback (error, isDisabled, message). isDisabled should be a boolean. true to disable the record action, false to enable it and you can provide a message if the action is to be disabled.

You’re responsible for mounting a GET route in Express to respond to it.

list.export

list.export is used to denote that a particular model is exportable. Linz takes care of the exporting for you, unless you want to provide a custom action to handle it yourself.

When a user clicks on an export, they’ll be provided a pop-up modal asking them to choose and order the fields they’d like to export.

list.export should be an Array of Objects. Each object describes an export option, for example:

export: [
  {
    label: 'Choose fields to export',
    exclusions: 'dateModified,dateCreated'
  }
]

Each object should contain the following keys:

  • label which is the name of the export.
  • exclusions which is a list of fields that can’t be exported.

If you’d like to provide your own export route, you can. Replace the exclusions key with an action key that works the same as list.actions. Rather than a modal, a request to that route will be made. You’re responsible for mounting a GET route in Express to respond to it.

Form DSL

The Models form DSL is used to customise the create and edit forms that are generated for each model. The form DSL has quite a few options as the model create and edit forms are highly customizable.

The form DSL is used to construct create and edit form controls (for example checkboxes, or text inputs) for a model record. Each key in the form object represents one of your model’s fields.

The type of form control used for each field can be defined explicitly, or determined by Linz (the default) based on the fields data type, as specificed when defining the field with Mongoose.

Each form control comes in the form of a widget, and can be explicitly altered by providing a different Linz widget, or creating your own widget.

form should be an object. It should contain a key, labelled with the name of the model field you’re providing information for.

For example, if you had a model with the fields name and email your form DSL might look like:

form: {
  name: {
    // configure the edit widget for the name field
  },
  email: {
    // configure the edit widget for the name field
  }
}

Each field object can contain the following top-level keys:

  • label
  • placeholder
  • helpText
  • type
  • default
  • list
  • visible
  • disabled
  • fieldset
  • widget
  • required
  • query
  • transform
  • transpose

These allow you to describe how the model create and edit forms should function.

Specialized contexts

There are two specialized contexts in which the form DSL operates:

  • When creating a model
  • When editing a model

From time to time, you’ll want to have different settings for one field, based on the context. Linz supports this through use of create and edit keys. Each of the above top-level keys can also be provided as a child of either create and edit. For example:

form: {
  username: {
    create: {
      label: 'Create a username',
      helpText: 'You can\'t change this later on, so choose wisely.'
    },
    edit: {
      label: 'The person\'s username',
      disabled: true,
      helpText: 'Once created, you can\'t edit the username.'
    }
  }
}

You can also use a combination of the default context and the specialized contexts create and edit contexts, for example:

form: {
  username: {
    label: 'The person\'s username',
    edit: {
      label: 'Uneditable username'
    }
  }
}

On the create form, the label for the username field will be The person’s username, but Uneditable username on the edit form.

The specialized create and edit contexts always supersede the default context.

{field-name}.label

The label property is optional. If not provided, it takes the label from the Models label DSL. If a label hasn’t been provided for that particular model field, it simply shows the name of the field itself.

The label property gives you an opportunity to customize it explicitly for the create and edit views.

{field-name}.placeholder

When you have the field of an appropriate type (such as text field), you can define the placeholder which sets the content of the HTML’s <input> tag placeholder attribute.

{field-name}.helpText

The helpText property can be used to supply additional text that sits below the form input control, providing contextual information to the user filling out the form.

{field-name}.type

The type property is intended to help Linz with two things:

  • Manage the data that the field contains in an appropriate manner.
  • To determine which widget to use if the widget property wasn’t provided.

type accepts the following strings:

  • array to render checkboxes for multiple select.
  • boolean to render radio inputs.
  • date to render a date input.
  • datetime to render a datetime input.
  • datetimeLocal to render a datetime-local input.
  • digit to render a text input with a regex of [0-9]*.
  • documentarray to render a custom control to manage multiple sub-documents.
  • email to render an email input.
  • enum to render a select input.
  • hidden to render a hidden input.
  • number to render a text input with a regex of [0-9,.]*.
  • password to render a password input.
  • string to render a text input.
  • tel to render a tel input with a regex of ^[0-9 +]+$.
  • text to render a text input.
  • url to render a url input.

The default widget, and the widget for all other types is the text widget.

{field-name}.default

The default property can be supplied to define the default value of the field. The default if provided, will be used when a field has no value.

If the default property is not provided, Linz will fallback to the default value as provided when defining the Mongoose schemas.

{field-name}.list

The list property is a special property for use with the enum type. It is used to provide all values from which a list field value can be derived.

Please bear in mind, that the list property is not involved in Mongoose validation.

The list property can either be an array of strings, or an array of objects.

For example, an array of strings:

list: [ 'Dog', 'Cat', 'Sheep' ]

If an array of objects is supplied, it must be in the format:

form: {
  sounds: {
    list: [
      {
        label: 'Dog',
        value: 'woof.mp3'
      },
      {
        label: 'Cat',
        value: 'meow.mp3'
      },
      {
        label: 'Sheep',
        value: 'baa.mp3'
      }
    ]
  }
}

There is also a more advanced use case in which you can provide a function which Linz will execute. This will allow you to generate at run time rather than start time, after Linz has been initialized:

form: {
  sounds: {
    list: function (cb) {
      return cb(null, {
        label: 'Dog',
        value: 'woof.mp3'
      },
      {
        label: 'Cat',
        value: 'meow.mp3'
      },
      {
        label: 'Sheep',
        value: 'baa.mp3'
      });
    }
  }
}

{field-name}.visible

The boolean visible property can be set to a value of false to stop the field from being rendered on the form.

{field-name}.disabled

The boolean disabled property can be set to a value of true to render the input field, with a disabled attribute.

{field-name}.fieldset

The fieldset property should be supplied to control which fields are grouped together under the same fieldset.

The fieldset property should be human readable, such as:

form: {
  username: {
    fieldset: 'User access details'
  }
}

{field-name}.widget

The widget property can be set to one of the many built-in Linz widgets. For example:

form: {
  sounds: {
    widget: linz.formtools.widget.multipleSelect()
    list: [
      {
        name: 'Dog',
        value: 'woof.mp3'
      },
      {
        name: 'Cat',
        value: 'meow.mp3'
      },
      {
        name: 'Sheep',
        value: 'baa.mp3'
      }
    ]
  }
}

{field-name}.required

The boolean required property can be set to true to require that a field has a value before the form can be saved (using client-side) validation.

{field-name}.query

The query property can be used to directly alter the Mongoose query object that is generated while querying the database for records to display.

query should be an object with the following keys:

  • filter
  • sort
  • select
  • label

{field-name}.transform

The transform property will accept a function that if provided, will be executed before a record is saved to the database.

Define a transform function if you’d like to manipulate the client-side data that is stored in the database.

In some instances, client-side data requirements are different from that of data storage requirements. transform in combination with transpose can be used effectively to manage these scenarios.

{field-name}.transpose

The transpose property will accept a function that if provided, will be executed before a field’s value is rendered to a form.

Define a transpose function if you’d like to manipulate the server-side data that is rendered to a form.

In some instances, data storage requirements are different form that of client-side data requirements. transpose in combination with transform can be used effectively to manage these scenarios.

Cell renderers

Linz tries to provide as many customisation options as possible. One of those is in the form of a what we call a cell renderer.

Cell renderers can be used within record overviews and model indexes. They’re used to represent data to the user in a human friendly way.

You can do many things with cell renderers that will improve the user experience. For example, you could take latitude and longitude values and render a map, providing visual context about location information specific to the record.

Built-in cell renderers

There are already many built-in cell renderers. They can all be accessed in the following namespace linz.formtools.cellRenderers.

The following shows how to define a specific cell renderer for a list field:

list: {
  fields: {
    websiteUrl: {
      label: 'Website url',
      renderer: linz.formtools.cellRenderers.url
    }
  }
}

The following provides a description of each built-in cell renderer:

  • date used with date field types to render a date, as per the date format setting.
  • datetime used with datetime field types to render a datetime, as per the datetime format setting.
  • localDate used with datetime field types to render a <time> tag as per the date format setting.
  • datetimeLocal used with datetime-local field types to render a <time> tag as per the datetime format setting.
  • overviewLink can be used to provide a link in the list, to the overview for a particular record.
  • array can be used to format an array in the format value 1, value 2, value 3.
  • boolean can be used to format a boolean in the format Yes or No.
  • reference can be used to render the title for a ref field type.
  • url can be used with url field types to render an <a> tag linking to the URL stored as the value of the field.
  • documentarray can be used with an embedded document to render a table showing embedded documents.
  • text can be used to render text, or any value as it is.
  • default is used by Linz as the default cell renderer if a specific type can’t be matched. It attempts to support arrays, dates, numbers, booleans and url field types.

Custom cell renderers

You can quite easily create and use your own cell renderer. All cell renderers must have the same signature:

function renderer (value, record, fieldName, model, callback)

The value is the value that needs to be rendered. The record is a copy of the entire record that the value belongs to. The fieldName is the name of the field that is being rendered. The model is a reference to the model that the record belongs to.

The callback is a function that accepts the standard Node.js callback signature:

function callback (err, result)

The result should be HTML. The HTML will be used directly, without manipulation. As it is HTML, you can provide inline JavaScript and CSS as required to add functionality your cell renderer.

The following is an example of a cell renderer that will look up data for a reference field and render the title:

function renderReference (val, record, fieldName, model, callback) {

  // Make sure we have the neccessary value, without erroring.
  if (!val || typeof val === 'string' || typeof val === 'number') {
    return callback(null, val);
  }

  // Retrieve the related documents title.
  linz.mongoose.models[val.ref].findById(val._id, (err, doc) => {

    if (err) {
      return callback(err);
    }

    return callback(null, (doc && doc.title) ? doc.title : `${val} (missing)`);

  });

};

Forgot password process

Linz has the capability to support a forgot password process. It can be used to allow the user to reset their password.

If enabled, it will render a Forgot your password? link on the log in page, which will facilitate the ability for a user to reset their password. It uses an email as proof of user record ownership. If the user can access a link sent to an email identified with a user record, then they can reset the password.

To enable this process, you need to:

  • Have a user model that stores an email address.
  • Define a sendPasswordResetEmail Mongoose static on your user model.
  • Define a verifyPasswordResetHash Mongoose method on your user model.
  • Define an updatePassword Mongoose static on your user model.

The process works as follows:

  • User clicks the Forgot your password? link on the log in page.
  • The user is directed to the Forgot your password page, and prompted to enter their email address.
  • The sendPasswordResetEmail static is executed with the email address they provided.
  • The sendPasswordResetEmail static should generate a unique hash for the user, and send an email to the user containing a link to reset their password.
  • The user will receive the email, and click on the link.
  • The link will be verified by executing the verifyPasswordResetHash method.
  • If the password reset hash can be be verified, the user will be provided the opportunity to enter a new password meeting the conditions of the admin password pattern setting.
  • The new password will be provided to the updatePassword static to store the updated password against the user record.

More succintly:

  1. Send a password reset email.
  2. Verify ownership of the email.
  3. Collect a new password and update their user record.

Send a password reset email

This part of the process entails:

  • Retreiving a user record based on an email address.
  • Generating a unique hash for the user record.
  • Creating a link for the user to continue the process.
  • Sending an email to the email address provided.

These actions should take place within the sendPasswordResetEmail executed by Linz.

The sendPasswordResetEmail static

The sendPasswordResetEmail static should have the following signature:

function sendPasswordResetEmail (userEmail, req, res, callback)

It needs to be a Mongoose static on your user model.

userEmail is the email address provided by the user who is trying to reset their password, req is the current request object, res is the current response object and callback is the function to execute when you’ve completed the neccessary steps.

The callback accepts the standard Node.js signature:

function callback (err)

If an Error is provided, Linz will render the error, otherwise it will consider the process complete.

Retrieving a user record based on an email address

Use the userEmail argument to search your user model for a corresponding record. If a record can’t be found, return an Error to the callback.

Make sure you take into consideration the following scenarios:

  • Multiple user records associated with the same email address.
  • No user record associated with the email address.

Genearing a unique hash for the user record

Once you have the user record, generate a unique hash for the user record. We recommend including the username, _id, email and dateModified.

The hash you generate must be verifyable by generating the same hash, at a later time, with the same information in the database (i.e. username, _id, email and dateModified).

A good Node.js package to consider to generate a hash is the bcrypt.js package.

Send an email

Once you have the link, you simply need to send it to the email address with instructions on what to do next; click on the link.

This is something you’ll have to implement yourself. Linz does not provide any capabilities to send emails. Linz is based on Express though, so you have all of it’s templating capabilities at hand. See using template engines with Express.

Verifying ownership of the email address

This part of the process involves verifying ownership of the email address. The user will receive the email, and click on the link. We want to make sure the link hasn’t been tampered with and that we can generate the same hash that was provided in the link.

Linz will retrieve the hash from the url and pass it to the verifyPasswordResetHash method.

It must be a Mongoose method on your user model.

Your verifyPasswordResetHash Mongoose method should have the following signature:

function verifyPasswordResetHash (candidateHash, callback)

The candidateHash is the hash value that was retreived from the Url. The callback is a standard Node.js callback:

function callback (err, result)

The result should be a boolean value.

Your verifyPasswordResetHash method should go through the same process to create the hash as it did in the first process. It should then verify that the candidateHash is the same as your freshly generated hash using the data from your database.

If the candidateHash checks out and you can successfully match it, return true to the callback.

Updating the users password

If the hash was verified, the user is provided an opportunity to enter a new password. The new password must meet the requirements of the admin password setting.

The new password is provided to the updatePassword Mongoose static on your user model. The updatePassword static should have the following signature:

function updatePassword (id, newPassword, req, res, callback)

id is the _id of the user model record. newPassword is the new password provided by the user. req is the current request object. res is the current response object. callback is a standard Node.js callback:

function callback (err)

If an Error is provided, Linz will render the error, otherwise it will consider the process complete.

The user will be notified that their password has been updated, and prompted to log into Linz again.

Getting started with Linz development

The linz-development repository is a complete environment for hacking on Linz. Using Vagrant, and a few commands, you’ll have a complete development environment up and running in no time.

Visit the linz-development repository for more information on how to get started hacking on Linz.

A note on documentation

Documentation is now a primary concern for the project. All PRs should be accompanied with updated documentation that describe in detail how to use a new feature, new capability, updates to an existing DSL or a new DSL.

mini-twitter

Mini Twitter is a complete working example of Linz. Head over to the mini-twitter GitHub repository to download mini-twitter and check out Linz.

Linz definitions

The following are a list of words you’ll see used many times in the Linz documentation. You can familiarize yourself with our terminology here.

DSL
Domain specific language. Linz uses these frequently to take instruction as to how certain functionality should be scaffolded. There is a DSL for Models, Configurations and Schemas.