In the original, pre Fall 2020, meteor-application-template-react we have a single collection Stuffs
. I’ve copied the code here:
import { Mongo } from 'meteor/mongo';
import SimpleSchema from 'simpl-schema';
import { Tracker } from 'meteor/tracker';
/** Define a Mongo collection to hold the data. */
const Stuffs = new Mongo.Collection('Stuffs');
/** Define a schema to specify the structure of each document in the collection. */
const StuffSchema = new SimpleSchema({
name: String,
quantity: Number,
owner: String,
condition: {
type: String,
allowedValues: ['excellent', 'good', 'fair', 'poor'],
defaultValue: 'good',
},
}, { tracker: Tracker });
/** Attach this schema to the collection. */
Stuffs.attachSchema(StuffSchema);
/** Make the collection and schema available to other code. */
export { Stuffs, StuffSchema };
As you can see Stuffs
is a Mongo collection with a SimpleSchema attached to do validation. We then export the collection and the schema.
To initialize the Stuffs
collection we use meteor-application-template-react/app/imports/startup/server/Mongo.js copied here:
import { Meteor } from 'meteor/meteor';
import { Stuffs } from '../../api/stuff/Stuff.js';
/* eslint-disable no-console */
/** Initialize the database with a default data document. */
function addData(data) {
console.log(` Adding: ${data.name} (${data.owner})`);
Stuffs.insert(data);
}
/** Initialize the collection if empty. */
if (Stuffs.find().count() === 0) {
if (Meteor.settings.defaultData) {
console.log('Creating default data.');
Meteor.settings.defaultData.map(data => addData(data));
}
}
We import the Mongo collection and call methods on the raw Mongo collection, Stuffs.find().count()
and Stuffs.insert(data)
. Similarly, in the meteor-application-template-react/app/imports/ui/pages/EditStuff.jsx we import the raw Mongo collection and call Stuffs.update
.
import 'uniforms-bridge-simple-schema-2'; // required for Uniforms
import { Stuffs, StuffSchema } from '../../api/stuff/Stuff';
/** Renders the Page for editing a single document. */
class EditStuff extends React.Component {
/** On successful submit, insert the data. */
submit(data) {
const { name, quantity, condition, _id } = data;
Stuffs.update(_id, { $set: { name, quantity, condition } }, (error) => (error ?
swal('Error', error.message, 'error') :
swal('Success', 'Item updated successfully', 'success')));
}
/** If the subscription(s) have been received, render the page, otherwise show a loading icon. */
render() {
return (this.props.ready) ? this.renderPage() : <Loader active>Getting data</Loader>;
}
// additional code snipped.
We use the StuffSchema to create the Uniforms form to edit the Stuff.
There are several issues with the current meteor-application-template-react implementation. We did this on purpose to not overwhelm the ICS 314 students. We’ll talk about two issues with the Stuffs
collection.
The Stuffs
collection is very simple, there are no foreign key relationships. If there were, we’d need to do the querying in the UI code and this becomes very difficult to ensure integrity. Even with the simplicity of the Stuffs
collection, we don’t validate the update data. Does it make any sense to have a negative quantity? Where should we enforce this requirement? In the StuffSchema
? In the UI?
We define the Meteor publications in meteor-application-template/app/imports/startup/server/Publications.js. We have two different publications ‘Stuff’, the stuff owned by the logged in user, and ‘StuffAdmin’, all the stuff. The subscriptions are in the UI code, EditStuff.jsx, ListStuff.jsx, and ListStuffAdmin.jsx.
There are many issues that arise from splitting up the publication and subscriptions into different files.
How do we know that the Stuffs
collection has an owner
field for the publication? The publication line looks like return Stuffs.find({ owner: username });
. I had an ICS 314 team that published a collection that didn’t have an owner field. It took days to hunt down that bug.
Notice that the subscriptions are using const subscription = Meteor.subscribe('Stuff');
or const subscription = Meteor.subscribe('StuffAdmin');
. What would happen if there is a typo in the String ‘Stuff’ or ‘StuffAdmin’? What if we changed Publications.js
changing the string ‘Stuff’ to ‘Stuffs’? How would we know to update all the subscriptions to match the new string?
To solve these two issues, we’ve wrapped the Stuff
collection in a class. This class has methods for accessing the collection, defining, updating, and removing items. It also has a publish method to create all the publications and separate subscribe methods for the subscriptions. This way users of the collection don’t have to worry about strings and typos.
We created an abstract BaseCollection class that wraps the Mongo collection.
class BaseCollection {
/**
* Superclass constructor for all meteor-application-template-react-production entities.
* Defines internal fields needed by all entities: _name, _collectionName, _collection, and _schema.
* @param {String} type The name of the entity defined by the subclass.
* @param {SimpleSchema} schema The schema for validating fields on insertion to the DB.
*/
constructor(type, schema) {
this._name = type;
this._collectionName = `${type}Collection`;
this._collection = new Mongo.Collection(this._collectionName);
this._schema = schema;
this._collection.attachSchema(this._schema);
}
It wraps the collection and applys the schema to the collection. We also define the following methods:
count()
: Runs count on the collection.define(object)
: Creates a new document in the collection. This method needs to be overridden in the subclasses. It throws a Meteor.Error
in the BaseCollection
.update(docID, object)
: Updates a given document in the collection. This method needs to be overridden in the subclasses. It throws a Meteor.Error
in the BaseCollection
.removeIt(docID)
: Removes a document from the collection. This method needs to be overridden in the subclasses. It throws a Meteor.Error
in the BaseCollection
.find(selector, options)
: Runs find on the collection.findDoc(docID)
: A stricter form of findOne, in that it throws an exception if the entity isn’t found in the collection.findOne(selector, options)
: Runs findOne on this collection.getType()
: Returns the type of this collection.getCollectionName()
: Returns the name of the collection.getSchema()
: Returns the schema that is applied to the collection.isDefined(docID)
: Returns true if the collection contains a document with the given docID.publish()
: Publishes the collection.The StuffCollection
now extends BaseCollection
.
import BaseCollection from '../base/BaseCollection';
export const stuffConditions = ['excellent', 'good', 'fair', 'poor'];
class StuffCollection extends BaseCollection {
constructor() {
super('Stuffs', new SimpleSchema({
name: String,
quantity: Number,
owner: String,
condition: {
type: String,
allowedValues: stuffConditions,
defaultValue: 'good',
},
}));
}
StuffCollection
a subclasses of BaseCollection
implements the following methods:
define({ name, quantity, owner, condition })
: This method can check the validity of the parameters before inserting the new item.update(docID, { name, quantity, condition })
: This method controls which properties you can change. You can’t change the owner of an item.removeIt(name)
: This method allows us to check any dependencies and decide whether we can remove the item from the collection or remove some of the other dependencies before removing the item.publish()
: This method creates the two publications, Stuff
and StuffAdmin
. Since the publications and subscriptions are in the same file we ensure they use the same strings.subscribeStuff()
: Returns Meteor.subscribe('Stuff')
, the subscription to the items owned by the current user. We call the method and don’t need to know the exact name of the publication.subscribeStuffAdmin()
: Returns Meteor.subscribe('StuffAdmin')
, the subscription to all the items in the collection.StuffCollection
has full control over insertion, updating and removing items.The define
, update
, and removeIt
methods allows the StuffCollection to validate the parameters to ensure database integrity.
StuffCollection
provides the two publications and methods to subscribe.app/imports/startup/server/Publications.js now calls Stuffs.publish()
. As shown below.
// imports/startup/server/Publications.js
import { Stuffs } from '../../api/stuff/StuffCollection';
/** Publish all the collections you need. */
Stuffs.publish();
The subscriptions in the UI code changes slightly. EditStuff.jsx, ListStuff.jsx, and ListStuffAdmin.jsx have one line changed. For example the ListStuff.jsx
subscriptions now looks like:
/** withTracker connects Meteor data to React components. https://guide.meteor.com/react.html#using-withTracker */
export default withTracker(() => {
// Get access to Stuff documents.
const subscription = Stuffs.subscribeStuff(); // was const subscription = Meteor.subscribe('Stuff');
return {
stuffs: Stuffs.find({}).fetch(),
ready: subscription.ready(),
};
})(ListStuff);
We don’t need to worry about the exact name of the publication since the StuffCollection
takes care of that.
HACC-Hui uses the above concepts to wrap all the collections. I’ve already defined the collection class hierarchy.
I’ve kept the StuffCollection for reference.