Skip to content

Commit

Permalink
Merge branch 'master' of github.com:emmerge/graviton
Browse files Browse the repository at this point in the history
  • Loading branch information
fractallian committed Sep 17, 2014
2 parents 1d3f3bd + 046c695 commit ced16fe
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 18 deletions.
95 changes: 81 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,51 @@ Allows you to:
* Transform your mongo docs into models with attributes and methods.
* Use other mrt packages to handle validation (meteor-simple-schema) hooks (meteor-collection-hooks) and relational pub/sub (reactive-relations).

Collections defined with Graviton automatically convert retreieved objects into models. You specify the type(s) when define the collection. Passing {transform: null} to find() etc. will bypass model transformation. The raw document is stored in model.attributes. Use built-in transformation methods like set, push, pop to make changes to your model locally. Call model.save() to persist changes to the database. All methods work on both server and client.
Collections defined with Graviton automatically convert retrieved objects into models. You specify the type(s) when define the collection. Passing `{transform: null}` to `find()` etc. will bypass model transformation. The raw document is stored in model.attributes. Use built-in transformation methods like set, push, pop to make changes to your model locally. Call `model.save()` to persist changes to the database. All methods work on both server and client.

## Installation

Graviton can be installed with [Meteorite](https://github.com/oortcloud/meteorite/). From inside a Meteorite-managed Meteor app:

``` sh
$ mrt add graviton
```

# API Docs

## Meteor.Collection.prototype
The following are added to all your meteor collections:
* `all()` alias to find().fetch()
* `build()` returns a new local Gravition.Model based on your collection definition. Does not save to db.
* `create()` calls build() to generate a Model then inserts it into the db.
* `all()` is an alias for `find().fetch()`
* `build()` returns a new local `Gravition.Model` based on your collection definition. Does not save to db. The instance can be saved using `Model.save()`.
* `create()` calls build() to generate a Model instance then inserts it into the db.

## Graviton
* `Graviton.define(collectionName, options)` Use to define your collections. Returns a Meteor.Collection instantiated with a transform function based on the options passed.

*Options*
* `persist` Passing false will define a local collection. default=true
* `modelCls` Either a model constructor generated by calling Graviton.Model.extend or an object like {key1: ModelClsA, key2: ModelClsB} if you want polymorphism. `collection.build({_type: 'key2'})` would return an instance of ModelClsB.
* `persist` Passing false will define a local collection. defaults to `true`
* `modelCls` A model constructor generated by calling Graviton.Model.extend
* or an object like `{key1: ModelClsA, key2: ModelClsB}` if you want polymorphism. `collection.build({_type: 'key2'})` would return an instance of `ModelClsB`.
* `defaultType` Use when supplying a object for modelCls. Specify the type to use when object being transformed does not contain _type.
* `timestamps` If true and you have the collection-hooks package installed, createdAt and updatedAt timestamps will be added to your models.
* `timestamps` If `true` and you have the `collection-hooks` package installed, `createdAt` and `updatedAt` timestamps will be added to your models.
* `<relationName>` Use to define relationships with other collections. See Relations section below.


* `Graviton.getProperty(key)` Use to access deeply-nested attributes without worry of throwing errors. Use period-delimited strings as keys. For example: `var obj = {}; Gravition.getPropery({}, 'some.deep.nested.prop')` would simply return undefined instead of throwing an error because obj.some is undefined. This method works best with keys that are meant to be stored in mongo since periods are not allowed in them.
* `Graviton.setProperty(thing, [value])` Set deeply-nested attributes. Example: `var obj = {}; Gravition.setProperty(obj, 'some.deeply.nested.prop', 'hello')` would result in `{some: {deeply: {nested: {prop: 'hello'}}}}` You can pass an object instead of key, value to set several at once.
* `Graviton.isModel(obj)` Use to check if an object is a model.

## Relations
Pass the following as keys to Graviton.define do declare relationships with other collections. Emample:
```
Car = Graviton.define("cars", {

* belongsTo:
* belongsToMany:
* hasOne:
* hasMany:
* embeds:
* embedsMany:

Pass the following as keys to Graviton.define do declare relationships with other collections. Example:
```javascript
CarModel = Graviton.Model.extend({
belongsTo: {
owner: {
klass: 'people',
Expand All @@ -52,6 +68,7 @@ Car = Graviton.define("cars", {
}
},
hasOne: {
// note that manufacturer should really be a belongsTo relationship since manufacturer shouldn't have a single carId
manufacturer: {
klass: 'manufacturers',
foreignKey: 'carId'
Expand All @@ -73,10 +90,14 @@ Car = Graviton.define("cars", {
klass: 'windows'
}
}
},{});

Car = Graviton.define("cars", {
modelCls: CarModel
});
```
Would make the following possible:
```
```javascript
var car = Car.findOne();
car.owner(); // returns a model
car.owner(person); // sets the ownerId of person
Expand All @@ -96,9 +117,55 @@ car.windows.at(2); // only builds one model

## Graviton.Model

# Example
Graviton transforms collection objects into Models for you. This allows them to carry useful metadata and functions with the data. The vanilla Graviton.Model allows for basic functionality. Defining an extension of the Graviton.Model allows you to specify details of the collection's relationships or other custom functionality.

* `Graviton.Model.extend({Options}, {ExtensionPrototype});` Use to define your collections. Returns a Meteor.Collection instantiated with a transform function based on the options passed.

*Options*
* `defaults`: an object containing default key:value pairs for the collection. These key:values will be added to all model instances where there is not already a stored value with the same key. Functions should not be placed here as stored records cannot have functions as values.
* `initialize`: a function which will be run on initialization.
* _relationships_: define how this model relates to other collections. `belongsTo`, `belongsToMany`, `hasOne`, `hasMany`, etc.

*ExtensionPrototype*
* This object contains the extension prototype. This is the place to add functions to the model. Values could also be placed here if they relate to this specific model. These do not behave as attributes - any values placed here will not be stored.


coming soon
# Examples

### full app example

[Graviscope](https://github.com/mhwheeler/Graviscope) - a graviton version of the Microscope app built in the book Discover Meteor

### Working with relations

### `Model.hasMany.add()` example (simplified from graviscope)

```javascript
PostModel = Graviton.Model.extend({
hasMany: {
comments: {
collection: 'comments',
foreignKey: 'postId'
}
}
},{};

Posts = Graviton.define('posts', {
modelCls: PostModel
});
```
```javascript
post = Posts.findOne();
comment = Comments.build();
comment.set({author:'Steve', body:'Check out the Posts.hasMany.comments relationship.'})
post.comments.add(comment);
console.log('Newly created comment id', comment._id);
```
### `belongsTo` examples (simplified from graviscope)
```javascript

```
27 changes: 25 additions & 2 deletions graviton.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,23 @@ Graviton = {
_collections: {}
};

// Meteor.users is a special collection which should not be transformed. Here it is added to Graviton._collections
// so it can be referenced by other other collections without a model/transformation.
Meteor.startup(function() {
Graviton._collections.users = Meteor.users;
});

// convenience
/**
*
* Meteor.Collection.prototype
*
*/

// all() convenience method == find().fetch()
Meteor.Collection.prototype.all = ManyRelation.prototype.all;

// build an instance of this collections model type but do not save it to the db
// returns the built model.
Meteor.Collection.prototype.build = function(obj) {
if (!_.isObject(obj)) obj = {};
var mdl = this._graviton.model(obj);
Expand All @@ -31,6 +41,12 @@ Meteor.Collection.prototype.create = function(obj, callback) {
return model;
};

/**
*
* Graviton
*
*/

// use a period-delimited string to access a deeply-nested object
Graviton.getProperty = function(obj, string) {
var arr = string.split(".");
Expand Down Expand Up @@ -58,6 +74,13 @@ Graviton.setProperty = function(obj, key, val) {
}
};

// Helper function to deal with objects which may have keys which are illegal in mongo
// 1. Mongo keys cannot start with $
// -- convert starts with $ to starts with #
// -- also convert starts with # to starts with ## to avoid collisions
// 2. Mongo keys cannot contain .
// -- convert . to @
// -- also convert @ to @@ to avoid collisions
Graviton.sanitizeKeysForMongo = function(obj) {
var nk;
for (var k in obj) {
Expand Down Expand Up @@ -96,7 +119,7 @@ var getModelCls = function(obj, options) {
return Graviton.Model;
};

// use this to declare new models
// declare new collections of models
// options contain the relations etc.
Graviton.define = function(collectionName, options) {
if (!options) options = {};
Expand Down
50 changes: 49 additions & 1 deletion lib/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -318,8 +318,30 @@ Model.prototype.save = function(callback) {
};

Model.prototype.remove = function(callback) {

var removed_id = this._id;
var self = this;
var relations;

// find all the relations so that we can handle cascade options
if (this._options.relations) {
relations = _.pick(this._options.relations, ['hasMany', 'hasOne']);
relations = _.extend(relations.hasMany || {}, relations.hasOne);
}

// before destroying the parent, check for relational restrictions
var denyRelations = _.filter(relations, function(relation) {
return relation.onRemove === 'deny';
});
_.each(denyRelations, function(relation) {
//console.log('collection:', self._collection._name, 'relation:', relation.relationName, 'is on remove deny restricted');
//console.log('self[relation.relationName].findOne()',self[relation.relationName].findOne());
if (self[relation.relationName].findOne())
throw Error('Cannot remove record, relation \''+relation.relationName+'\' denies removal if child records exist.');
});

// remove the document from the collection itself
if (callback) {
var self = this;
this._collection.remove(this._id, function(err, res) {
if (!err) self.setId(null);
callback(err, res);
Expand All @@ -328,5 +350,31 @@ Model.prototype.remove = function(callback) {
this._collection.remove(this._id);
this.setId(null);
}

// after destroying the parent, remove or nullify the children based on relational options
_.each(relations, function(relation) {
self._id = removed_id; //mock up the model like it still exists to allow the relations to work naturally

if (relation.onRemove == 'cascade') {
//console.log('collection:', self._collection._name, 'relation:', relation.relationName, 'should onRemove cascade');
_.each(self[relation.relationName].all(), function(child) {
//console.log('child', relation.collection, 'to delete:', child._id);
child.remove();
})
}
else if (relation.onRemove == 'unset') {
//console.log('collection:', self._collection._name, 'relation:', relation.relationName, 'should remove reference');
_.each(self[relation.relationName].all(), function(child) {
//console.log('child', relation.collection, 'to remove reference to:', child._id);
var updateObj = {};
updateObj['$unset'] = {};
updateObj['$unset'][relation.foreignKey] = true;
child.update(updateObj);
})
}

self._id = null; //reset the removed model.
});

return this;
};
2 changes: 1 addition & 1 deletion smart.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"description": "Relations and models for Meteor collections",
"homepage": "https://github.com/thredder/graviton",
"author": "Jeremy Dunn",
"version": "0.0.3",
"version": "0.0.4",
"git": "https://github.com/thredder/graviton.git",
"packages": {
"async": "0.2.9.3"
Expand Down

0 comments on commit ced16fe

Please sign in to comment.