Database Configuration
Concepts
Warn: KeystoneJS requires MongoDB v2.4 or greater.
In KeystoneJS, 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 keystone.List
, and pass it list options.
You then add
fields to the list. Behind the scenes, a Keystone 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 Keystone.
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()
.
Lists
Usage
new keystone.List(key:string [, options:object]);
The syntax for creating a Keystone List is very similar to the syntax for creating a Mongoose Schema, with the exception of the constructor, which is var MyList = new keystone.List(key, options)
.
Once you have created a new List, add fields to it using MyList.add(fields:object)
, 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 Keystone and finalise its configuration.
The options can be found here
Example
A simple Post model for a blog might look like this:
Post.js
var keystone = require('keystone');
var Types = keystone.Field.Types;
var Post = new keystone.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();
NOTE This example implements the optional
map
,autokey
anddefaultSort
options, see the api documentation for more details.NOTE It also specifies
title
,state
,author
andpublishedAt
as the default columns to display in the Admin UI, with state and publishedAt being given column widths.NOTE The
author
field is a relationship with theUser
model, as seen in the yo generator config.
Drilldown Example
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.
Example: Including the author in the drilldown for Posts
var Post = new keystone.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
});
Inheritance Example
The inheritance option can be used to allow a list to inherit its fields from another list using Mongoose model discriminators.
Parent lists may not themselves inherit from other lists.
Example: Inheriting List fields from other lists
var keystone = require('keystone');
var BasePage = new keystone.List('BasePage', {
map: { name: 'title' },
autokey: { path: 'slug', from: 'title', unique: true },
});
BasePage.add(
{
title: { type: String, required: true },
slug: { type: String, readonly: true },
}
);
BasePage.register();
var ChildPage = new keystone.List('ChildPage', { inherits: BasePage });
ChildPage.add({ child_content: { type: String, readonly: true } });
ChildPage.register();
Schema Plugins
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();
});
Querying Data
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:
Loading Posts
var keystone = require('keystone'),
Post = keystone.list('Post');
Post.model.find()
.where('state', 'published')
.populate('author')
.sort('-publishedAt')
.limit(5)
.exec(function(err, posts) {
// do something with posts
});
Promises
There exists 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:
Loading Posts, doing something asynchronous, doing something
var keystone = require('keystone'),
Post = keystone.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);
});
Pagination Querying
To query data with pagination, you can use List.paginate.
Creating Items
To create new items, again use the mongoose model:
Creating Posts
var keystone = require('keystone'),
Post = keystone.list('Post');
var newPost = new Post.model({
title: 'New Post'
});
if (shouldBePublished) {
newPost.state = 'published';
}
newPost.save(function(err) {
// post has been saved
});
Automatic keys Because we set the
autokey
option on ourPost
list, it will have generated a unique key based on thetitle
before it was saved to the database.newPost.slug == 'new-post';
Deleting Items
To delete items, first load the data, then use the remove
method:
Deleting a Post
var keystone = require('keystone'),
Post = keystone.list('Post');
Post.model.findById(postId)
.remove(function(err) {
// post has been deleted
});
Headings
Define headings to display within the flow of your documents. Headings can be defined as a String
or Object
and can depend on another field value for display.
Person.add(
'User',
{ name: { type: Types.Name, required: true, index: true, initial: true } },
'Permissions',
{ isAdmin: { type: Boolean, label: 'Can access Keystone', index: true } },
// header object
{ heading: 'Activities' },
{ place: { type: Types.Select, options: ['GT', 'UGA'] } },
// header with dependsOn
{ heading: "GT Activities", dependsOn: { place: 'GT' } },
{ type: { type: Types.Select, options: ['ZC', 'MP'], dependsOn: { place: 'GT'} }
);
Options
heading
The text to display
dependsOn
The heading will only be displayed when the paths specified in the object match the current data for the item. dependsOn
Fields
When adding fields
to Lists
, you can either specify basic data types or Keystone Field Types.
Overview
Keystone 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:
- Rich controls in Keystone’s Admin UI
- Complex data types; e.g. the
location
field stores several strings and an GeoJSON lng/lat point - Formatting and validation methods
- Additional virtual properties; e.g. the
name
field provides aname.full
virtual which concatenates the storedname.first
andname.last
- Underscore methods; e.g. the
password
field provides apassword.compare
method for testing against the encrypted hash - Metadata about how fields relate to each other; e.g. which fields depend on certain values in other fields Basic data types are mapped to their corresponding Keystone field types:
Data type Field type String Text Number Number Date DateTime Boolean Boolean
Field Options
For a full list of options that are available to all fields, see the field options list.
Underscore Methods
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 keystone = require('keystone'),
Post = keystone.list('Post');
Post.model.findById(postId).exec(function(err, post) {
console.log(post._.createdAt.format('Do MMMM YYYY')); // 25th August 2013
});