Setting up and using Data Models
In LandmarkJS, your data schema and models are controlled by Lists, and documents in your database are often called Items.
To define a data model, you create a new landmark.List
, and pass it list options.
You then add
fields to the list. Behind the scenes, a Landmark List will create a mongoose schema, and add the appropriate paths to it for the fields you define.
The schema
is accessible, allowing you to plug in other mongoose functionality like virtuals, methods and pre / post hooks.
When you have finished setting up your List, call list.register()
to initialise it and register it with Landmark.
To query your data, you use the list.model
(which is a mongoose model).
List Items are mongoose documents. To create new items, use new list.model()
and when you're ready to save it (or to save changes to an existing Item), call item.save()
.
new landmark.List(key[, options]);
The syntax for creating a Landmark List is very similar to the syntax for creating a Mongoose Schema, with the exception of the constructor, which is var MyList = new landmark.List(key, options)
.
Once you have created a new List, add fields to it using MyList.add(fields)
, where fields is an object of keys (for field paths) and values (for field types, or options).
Fields are defined by an object with a type
property, which must be a valid Field Type or basic data type. Using the object syntax you can specify additional options for the field. Common field options and field-type-specific options are detailed in the fields documentation.
When all the fields and options have been set on the list, call MyList.register()
to register the list with Landmark and finalise its configuration.
A simple Post model for a blog might look like this:
var landmark = require('landmark-serve'),
Types = landmark.Field.Types;
var Post = new landmark.List('Post', {
autokey: { path: 'slug', from: 'title', unique: true },
map: { name: 'title' },
defaultSort: '-createdAt'
});
Post.add({
title: { type: String, required: true },
state: { type: Types.Select, options: 'draft, published, archived', default: 'draft' },
author: { type: Types.Relationship, ref: 'User' },
createdAt: { type: Date, default: Date.now },
publishedAt: Date,
image: { type: Types.CloudinaryImage },
content: {
brief: { type: Types.Html, wysiwyg: true, height: 150 },
extended: { type: Types.Html, wysiwyg: true, height: 400 }
}
});
Post.defaultColumns = 'title, state|20%, author, publishedAt|15%'
Post.register();
This example implements the optional map
, autokey
and defaultSort
options, described below.
It also specifies title
, state
, author
and publishedAt
as the default columns to display in the Admin UI, with state and publishedAt being given column widths.
The author
field is a relationship with the User
model, as described in the getting started guide.
Lists support the following options:
label String |
The label used for the list in the Admin UI. Defaults to a friendly form of key |
path String |
The path used for the list in the Admin UI. Defaults to a slugified form of key . |
singular String |
The singular label for the items in the list. Used in the Admin UI, defaults to a singular form of label |
plural String |
The plural label for the items in the list. Used in the Admin UI, defaults to a plural form of singular |
schema String |
Options for the Mongoose Schema for the List. Among other things, this option lets you specify a custom name for the collection. See the mongoose schema docs for a list of available options. Warning: do not modify the |
drilldown String |
A space-delimited list of relationships to display as drilldown in the Admin UI |
sortable Boolean |
Adds a hidden field sortOrder to the schema, and enables drag and drop sorting in the Admin UI |
sortContext String |
A List:relationship pair to control when drag and drop sorting is available in the Admin UI |
searchFields String |
A space-delimited list of paths to use for searching in Admin UI |
defaultSort String |
The default column or path to sort on in the Admin UI |
defaultColumns String |
A comma-delimited list of default columns to display in the Admin UI List View.
You can specify width in either pixels or percent after a | pipe character.
|
map Object |
An object that maps fields to special list paths. Each path defaults to its key if a field with that key is added. Mappable paths include |
autokey Object |
Adds a plugin to the list that automatically generates a key for each document when it is saved, based on the value of another field or path. The value of the option should be an object with the following keys: | Autokey paths are automatically be indexed; you may also want to include them in compound indexes.
noedit Boolean |
Prevents editing of items in the list through the Landmark Admin UI |
nocreate Boolean |
Prevents creation of new items in the list through the Landmark Admin UI |
nodelete Boolean |
Prevents deletion of items from the list through the Landmark Admin UI |
hidden Boolean |
Hides the list in the Landmark Admin UI |
If you're wondering how to control which navigation area Lists are categorised under in the Admin UI, check out the nav
option in the LandmarkJS Configuration docs.
The drilldown option is a nice way to improve the usability of the Admin UI by providing context to the item a user is currently editing.
By default, the drilldown will just show the list that the item belongs to.
You can, however, set it to a Relationship
field in the schema, and it will display the item currently stored in that relationship field.
If there would be several relationships that may be relevant to display in the drilldown list, you can separate their paths with spaces.
var Post = new landmark.List('Post', {
autokey: { path: 'slug', from: 'title', unique: true },
map: { name: 'title' },
defaultSort: '-createdAt',
drilldown: 'author' // author is defined as a Relationship field in the example above
});
You can specify virtuals, methods, statics as well as pre and post hooks for your Lists using the schema
. You can also use mongoose plugins from the plugins website.
For example, in our Post list above, we might want to automatically set the publishedAt
value when the state
is changed to published
(but only if it hasn't already been set).
We might also want to add a method to check whether the post is published, rather than checking the state
field value directly.
Before calling Post.register()
, we would add the following code:
Post.schema.methods.isPublished = function() {
return this.state == 'published';
}
Post.schema.pre('save', function(next) {
if (this.isModified('state') && this.isPublished() && !this.publishedAt) {
this.publishedAt = new Date();
}
next();
});
To query data, you can use any of the mongoose query methods on the list.model
.
For example: to load the last 5 posts
with the state published
, populating the linked author
, sorted by reverse published date:
var landmark = require('landmark-serve'),
Post = landmark.list('Post');
Post.model.find()
.where('state', 'published')
.populate('author')
.sort('-publishedAt')
.limit(5)
.exec(function(err, posts) {
// do something with posts
});
Promises
There exist another way to work with events in Javascript that is included in mongoose query methods. Instead of passing a callback to the exec method, we can use what it returns: a Promise. Promises are very useful for clean chaining of events with propagation of error.
For example: load 100 posts
, then do something asynchronous, then do something with result:
var landmark = require('landmark-serve'),
Post = landmark.list('Post');
Post.model.find()
.limit(100)
.exec()
.then(function (posts) { //first promise fulfilled
//return another async promise
}, function (err) { //first promise rejected
throw err;
}).then(function (result) { //second promise fulfilled
//do something with final results
}, function (err) { //something happened
//catch the error, it can be thrown by any promise in the chain
console.log(err);
});
To create new items, again use the mongoose model:
var landmark = require('landmark-serve'),
Post = landmark.list('Post');
var newPost = new Post.model({
title: 'New Post'
});
if (shouldBePublished) {
newPost.state = 'published';
}
newPost.save(function(err) {
// post has been saved
});
Because we set the autokey
option on our Post
list, it will have generated a unique key based on the title
before it was saved to the database.
newPost.slug == 'new-post';
To delete items, first load the data, then use the remove
method:
var landmark = require('landmark-serve'),
Post = landmark.list('Post');
Post.model.findById(postId)
.remove(function(err) {
// post has been deleted
});
When adding fields to Lists, you can either specify basic data types or Landmark Field Types.
Landmark Fields allow you to easily add rich, functional fields to your application's models. They are designed to describe not just the structure of your data, but also the intention of your data. They provide:
location
field stores several strings and an GeoJSON lng/lat pointname
field provides a name.full
virtual which concatenates the stored name.first
and name.last
password
field provides a password.compare
method for testing against the encrypted hashBasic data types are mapped to their corresponding Landmark field types:
Data type | Field type |
---|---|
String |
Text |
Number |
Number |
Date |
DateTime |
Boolean |
Boolean |
All field types support several common options, which can specify database settings (such as index
and default
), or can provide information for Landmark's Admin UI (such as label
).
Fields can be nested inside objects, as in mongoose schemas.
All mongoose schema type options are passed to the mongoose schema, so you can also use any options mongoose supports.
Common field options include:
label String |
The label of each field is generated from the field path; set this option to override the default. |
required Boolean |
Validates that the field has a value before an item can be saved (also passed to mongoose and enforced using a database index). |
initial Boolean |
Causes the field to be displayed in the Create Item form, in the Admin UI. |
noedit Boolean |
Renders the field as read-only in the admin UI. |
note String |
Is displayed with the field in the admin UI. |
hidden Boolean |
The field will always be hidden in the Admin UI if this is set to true |
To improve the usability of the Admin UI, it is possible to hide fields when no value is set, or depending on the value of other fields.
collapse Boolean |
Displays an + add link in the admin UI when the field has no value. Will completely hide field UI when noedit is also set to true, when the field has no value |
dependsOn Object |
The field will only be displayed when the paths specified in the object match the current data for the item |
Landmark's fields support a simple syntax for configuring dynamically updated fields. You can set a field to update its value whenever:
To use the watching functionaliy, set the following two options:
watch Boolean or String or Object |
When Provide a space-delimited list of paths to recalculate the field value whenever one of those paths changes. Provide an object of key / value pairs to recalculate the field value whenever one of those paths changes to the value specified. |
value Function |
The function to generate the field value when a watched path is changed. Must return the new value. The |
Some field types include helpful underscore methods, which are available on the item at the field's path preceded by an underscore.
For example: use the format
underscore method of the createdAt
DateTime
field of the Posts List (above) like this
var landmark = require('landmark-serve'),
Post = landmark.list('Post');
Post.model.findById(postId).exec(function(err, post) {
console.log(post._.createdAt.format('Do MMMM YYYY')); // 25th August 2013
});
Landmark enhances MongoDB's ability to store the ObjectIDs of related documents in a field (or many related ObjectIDs in an Array) with support for Relationship fields and Definitions in Models.
ObjectId
or Array
— Displayed as an auto-suggest field in the Admin UIStores references to ObjectIDs from another Model in an ObjectID field or array to create one-many or many-many relationships.
Specify the related Model using the ref
option. For a many-many relationship, set the many
option to true
.
For example, if you wanted to link a Post model to a single Author and many PostCategories, you would do it like this:
Post.add({
author: { type: Types.Relationship, ref: 'User' },
categories: { type: Types.Relationship, ref: 'PostCategory', many: true }
});
You can populate related data for relationship fields thanks to Mongoose's populate functionality. To populate the author and category documents when loading a Post from the example above, you would do this:
Post.model.findOne().populate('author categories').exec(function(err, post) {
// the author is a fully populated User document
console.log(post.author.name);
});
Note that if no ObjectId is stored, or an invalid ObjectId is stored (e.g. a document has been deleted), author
will be undefined
in the example above.
What if, in the example above, you wanted to see a list of the Posts by each Author? Because the relationship field is on the Post, you need to tell the Author (and the PostCategory) Model that it is being referred to. Doing so allows the Admin UI to represent the relationship from both sides.
You do this by calling the relationship
method on the Model
like this:
User.relationship({ path: 'posts', ref: 'Post', refPath: 'author' });
As you can see, the options provided to the relationship
method mirror those of the relationship field it refers to.
Relationship definitions are optional; if you leave them out, the relationships simply won't be displayed in the Admin UI from the other side of the relationship. The relationship field will still work as expected.
Filtering one-to-many related items is easy; simply specify the ID of the item you wish to filter on like any other value:
Post.model.find().where('author', author.id).exec(function(err, posts) {
// ...
});
To filter many-to-many related items, use an in
condition and specify one (or more) ids as an array:
Post.model.find().where('categories').in([category.id]).exec(function(err, posts) {
// ...
});
Boolean
Boolean
— Displayed as a checkbox in the Admin UI{ type: Types.Boolean }
Text
String
— Displayed as a text field in the Admin UI{ type: Types.Text }
Textarea
String
— Displayed as a textarea field in the Admin UI{ type: Types.Textarea }
Email
String
— Displayed as a text field in the Admin UIInput must look like a valid email address (can be blank unless field is required)
{ type: Types.Email, displayGravatar: true }
Url
String
— Displayed as a text field in the Admin UI.{ type: Types.Url }
Html
String
— Displayed as a text field or WYSIWYG Editor in the Admin UI.{ type: Types.Html, wysiwyg: true }
Color
Color
— Displayed as a text field with a color picker{ type: Types.Color }
Date
Date
— Displayed as a date picker in the Admin UIInput should either be a valid Date, or a string in the format YYYY-MM-DD (can be blank unless field is required)
To default Date fields to the current time, set the default
option to Date.now
{ type: Types.Date }
Datetime
Datetime
— Displayed as a date and time picker in the Admin UIInput should either be a valid Date, or a string in the format YYYY-MM-DD
(can be blank unless field is required)
To default Date fields to the current time, set the default
option to Date.now
{ type: Types.Datetime, default: Date.now }
Key
String
— Displayed as a text field in the Admin UIAutomatically converts input to a valid key (no spaces or special characters). White space is replaced with a separator.
{ type: Types.Key }
Number
Number
— Displayed as a number field in the Admin UIInput should either be a valid Number, or a string that can be converted to a number (can be blank unless field is required)
{ type: Types.Number }
Money
Number
— Displayed as a number field in the Admin UIInput should either be a valid Number, or a string that can be converted to a number (leading symbols are allowed; can be blank unless field is required). Money fields do not understand currency.
{ type: Types.Money }
Select
String
or Number
— Displayed as a select field in the Admin UI{ type: Types.Select, options: 'first, second, third' }
Markdown
Object
— Displayed as a textarea field in the Admin UI{ type: Types.Markdown }
Name
Object
— Displayed as firstname lastname fields in the Admin UI{ type: Types.Name }
Password
String
— Displayed as a password field in the Admin UI, with a 'change' button.Passwords are automatically encrypted with bcrypt, and expose a method to compare a string to the encrypted hash.
The encryption happens with a pre-save hook added to the schema, so passwords set will not be encrypted until an item has been saved to the database.
{ type: Types.Password }
Location
Object
— Displayed as a combination of fields in the Admin UIContains a standard set of strings for storing an address, and a longitude / latitude point with a 2dsphere
index.
Also provides autocomplete functionality using Google's Places API (requires a Google Maps API Key to be provided, must only be used in accordance with Google's terms of service).
See the Google configuration documentation for details on how to set up Google Maps in LandmarkJS.
{ type: Types.Location }
Note: the schema paths are based on Australian address formats, and should be updated to be more appropriate for other international formats. If you have feedback on how the structure should be internationalised, please open a ticket.
CloudinaryImage
Object
— Displayed as an image upload field in the Admin UIAutomatically manages images stored in Cloudinary, including uploading, resizing and deleting.
See the Cloudinary configuration documentation for details on how to set up Cloudinary in LandmarkJS.
{ type: Types.CloudinaryImage }
Remember that if you are uploading images to a CloudinaryImage
field using an HTML form, you need to specify enctype="multipart/form-data"
in your form
tag.
CloudinaryImages
Array
— Displayed as a series of images, and an upload field in the Admin UIStores multiple images in a array as a nested Schema
, each of which expose the same methods as the cloudinaryimage
field.
{ type: Types.CloudinaryImages }
LocalFile
Object
— Displayed as a file upload field in the Admin UIStores files on the local file system.
{ type: Types.LocalFile }
S3 File
Object
— Displayed as an file upload field in the Admin UIAutomatically manages files stored in Amazon S3, including uploading and deleting.
{ type: Types.S3File }
AzureFile
Object
— Displayed as an file upload field in the Admin UIAutomatically manages files stored in Windows Azure Storage, including uploading and deleting.
{ type: Types.AzureFile }
Embedly
Object
— Displayed as read-only data in the Admin UIAutomatically retrieves data from the Embedly API about the value of another field (specified with the from
option).
It stores the retrieved data (which includes the provider, media type, full URL, HTML embed code, width, height, thumbnail picture and more).
The api call to retrieve the data is implemented as a pre-save hook, and is only triggered if the from path value has changed.
See the Embed.ly configuration documentation for details on how to set up Embed.ly in LandmarkJS.
{ type: Types.Embedly, from: 'path' }
See the Examples page for projects that demonstrate real-world usage of the various list options and field types.