Pup

v1.x

The BasicsMongoDB Collections

In Pup, as in Meteor, MongoDB is the database of choice. It's a great fit for beginners as the syntax for querying the database is easy to grok, it's automatically connected to your Meteor application (no need to wire this up yourself), and it's real-time by default.

The Documents collection

As an example, one collection is defined for you in Pup: Documents. The Documents collection is part of the example CRUD workflow. It's only intended to be an example, but you're more than welcome to use it as the foundation for your own documents feature.

Over on the right, we can see the definition file for the Documents collection. It contains three distinct parts: the creation of the MongoDB collection, the limiting of the collection's allow and deny rules, and the creation/attachment of the collection's schema.

Creating new collections

Creating new collections takes one line: new Mongo.Collection('<Collection Name>');. In our example, we can see our Documents collection being created and stored in a const variable. When a collection is created with new Mongo.Collection(), the MongoDB collection instance is returned. We store this in a variable because we need the instance in order to assign allow/deny rules and know which collection we're attaching our schema to.

Setting allow and deny rules

In Meteor, allow and deny rules define when, who, and how a collection can be interacted with on the client. Initially, this was promoted as a unique feature of Meteor, however, over the years has proven to be error prone. Now, performing database operations on the client is not recommended.

Even though allow and deny rules are not recommended, it's important to "lock them down" to prevent any accidental usage. In our example, we can see this happening in two stages: once for allow rules and once for deny rules. The rules that are defined focus on preventing the action: for allow rules, we return false for each database operation (insert, update, or remove) to say "do not allow this," and for the deny rules, we return true for each operation to say "do deny this."

Defining a schema

By nature, MongoDB is known as a NoSQL or "schemaless" database. Unlike a traditional SQL database where data has to be entered into specific columns in rows, in a NoSQL database, instead of rows, data is stored in documents (essentially, JSON objects). Those documents have no required structure. This means that, without a schema, we could store data in our Documents collection that looks like this:

{ title: 'Document #1', body: 'Hello.', sandwiches: true }
{ title: 'Document #2', body: 'Hello.', ketchupPopsicle: false }
{ title: 'Document #3', body: 'Hello.' }

Because no schema is enforced, these three documents can exist side-by-side in the same collection. Of course, unless we have a specific reason for this, it's not terribly productive because any UI rendering these documents will have to check that all of the possible fields exist before rendering. No fun!

To prevent any chaos in the UI, Pup relies on an NPM package called simpl-schema (the missing "e" on "simple" is not a typo). Combined with another, Atmosphere (Meteor) package collection2 to "attach" the schema to our MongoDB collection, we can simulate the behavior of working with a traditional SQL database.

In our example, we can see a simple schema—no pun intended—being attached to our Documents collection. It specifies that for all documents being inserted into the Documents collection, the following fields must be present:

owner - A String value, equal to the _id of a user in the Meteor.users collection. Note: only the String type is enforced, not that it exists in the Meteor.users collection.

createdAt - An automatically set String value (on insert of new documents), equal to an ISO-8601 formatted date.

updatedAt - An automatically set String value (on insert/update of documents), equal to an ISO-8601 formatted date.

title - A String value equal to the title field passed from the client, based on the user's input.

body - A String value equal to the body field passed from the client, based on the user's input.

Once a schema is defined, it needs to be attached to the collection it relates to, using the .attachSchema(<Schema Object>) method that's automatically defined on all MongoDB collections by the collection2 package. After this is called, the schema is attached and all insert and update operations will pass through the schema before they're actually performed on the database. If a given operation fails to pass the schema, it is rejected (throwing an error that can be passed back to the client) and the insert or update is blocked.

/imports/api/Documents/Documents.js
/* eslint-disable consistent-return */

import { Mongo } from 'meteor/mongo';
import SimpleSchema from 'simpl-schema';

const Documents = new Mongo.Collection('Documents');

Documents.allow({
  insert: () => false,
  update: () => false,
  remove: () => false,
});

Documents.deny({
  insert: () => true,
  update: () => true,
  remove: () => true,
});

Documents.schema = new SimpleSchema({
  owner: {
    type: String,
    label: 'The ID of the user this document belongs to.',
  },
  createdAt: {
    type: String,
    label: 'The date this document was created.',
    autoValue() {
      if (this.isInsert) return (new Date()).toISOString();
    },
  },
  updatedAt: {
    type: String,
    label: 'The date this document was last updated.',
    autoValue() {
      if (this.isInsert || this.isUpdate) return (new Date()).toISOString();
    },
  },
  title: {
    type: String,
    label: 'The title of the document.',
  },
  body: {
    type: String,
    label: 'The body of the document.',
  },
});

Documents.attachSchema(Documents.schema);

export default Documents;

Defining Indexes

As you prepare your product for production, one of the most important steps you can do to influence performance is to add indexes to your MongoDB collections. Indexes are a way to help MongoDB quickly locate data related to frequently used queries.

In Pup, we provided you with a module to aid in the definition of indexes /imports/modules/server/create-index.js. This file exports a single function which accepts a MongoDB collection instance as its first argument, the object defining what should be indexed as its second argument, and an optional options object as its third argument for customizing the indexes behavior.

In terms of usage, we've provided an example file in /imports/api/Documents/server/indexes.js which is imported into /imports/startup/server/api.js for use. Keep in mind: indexes are collection-specific so storing them alongside their collection directory in the /imports/api directory is helpful. The createIndex() method is intended to be used on the server only, so if you wish to move it make sure to use it in a server-only environment.

/imports/modules/server/create-index.js
/* eslint-disable */

import { Mongo } from 'meteor/mongo';

export default (Collection, index, options) => {
  if (Collection && Collection instanceof Mongo.Collection) {
    Collection.rawCollection().createIndex(index, options);
  } else {
    console.warn('[/imports/modules/server/create-index.js] Must pass a MongoDB collection instance to define index on.');
  }
};
/imports/api/Documents/server/indexes.js
import createIndex from '../../../modules/server/create-index';
import Documents from '../Documents';

createIndex(Documents, { owner: 1 });

Collection template

The following template can be used to define your own MongoDB collections. If your product will contain a lot of collections, it's wise to save this pattern to your text editor's snippets feature.

MongoDB Collection Definition Template
/* eslint-disable consistent-return */

import { Mongo } from 'meteor/mongo';
import SimpleSchema from 'simpl-schema';

const Collection = new Mongo.Collection('Collection');

Collection.allow({
  insert: () => false,
  update: () => false,
  remove: () => false,
});

Collection.deny({
  insert: () => true,
  update: () => true,
  remove: () => true,
});

Collection.schema = new SimpleSchema({
  /* Add your schema rules to this object. */
});

Collection.attachSchema(Collection.schema);

export default Collection;