Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Harvester should be able to link to resources on remote services #69 #122

Merged
merged 2 commits into from
Jul 23, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# http://editorconfig.org
root = true

[*]
indent_style = space
indent_size = 4
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
end_of_line = lf

[*.md]
trim_trailing_whitespace = false

[*.json]
insert_final_newline = false

[*.yml]
insert_final_newline = false

250 changes: 2 additions & 248 deletions lib/adapters/mongodb.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,10 @@ function Adapter() {
var value = isArray ? val[0] : val;
var isObject = _.isPlainObject(value);
var ref = isObject ? value.ref : value;
var inverse = isObject ? value.inverse : undefined;

// Convert strings to associations
if (typeof ref == 'string') {
obj.ref = ref;
obj.inverse = inverse;
obj.type = String;
schema[key] = isArray ? [obj] : obj;
}
Expand Down Expand Up @@ -142,7 +140,6 @@ function Adapter() {
model = typeof model == 'string' ? this.model(model) : model;
return new Promise(function (resolve, reject) {
model.findByIdAndRemove(id, function (error, resource) {
resource = _this._dissociate(model, resource);
_this._handleWrite(model, resource, error, resolve, reject);
});
});
Expand Down Expand Up @@ -267,7 +264,7 @@ function Adapter() {
json[path] = resource[path];
//Distinguish between refs with UUID values and properties with UUID values
var hasManyRef = (type.options.type && type.options.type[0] && type.options.type[0].ref);
var isRef = (type.options.ref || hasManyRef);
var isRef = !!(type.options.ref || hasManyRef);
var instance = type.instance ||
(type.caster ? type.caster.instance : undefined);
if (path != '_id' && instance == 'String' && uuidRegexp.test(resource[path]) && isRef) {
Expand Down Expand Up @@ -314,250 +311,7 @@ function Adapter() {
if (error) {
return reject(error);
}
this._updateRelationships(model, resource).then(function (resource) {
resolve(_this._deserialize(model, resource));
}, function (error) {
reject(error);
});
};

/**
* Update relationships manually. By nature of NoSQL,
* relations don't come for free. Don't try this at home, kids.
*
* @api private
* @param {Object} model
* @param {Object} resource
* @return {Promise}
*/
adapter._updateRelationships = function (model, resource) {
var _this = this;

/**
* Get fields that contain references.
*/
var references = [];
_.each(model.schema.tree, function (value, key) {
var singular = !_.isArray(value);
var obj = singular ? value : value[0];
if (typeof obj == 'object' && obj.hasOwnProperty('ref')) {
references.push({
path: key,
model: obj.ref,
singular: singular,
inverse: obj.inverse
});
}
});

var promises = [];
_.each(references, function (reference) {
var relatedModel = _this._models[reference.model];
var relatedTree = relatedModel.schema.tree;
var fields = [];

// Get fields on the related model that reference this model
if (typeof reference.inverse == 'string') {
var inverted = {};
inverted[reference.inverse] = relatedTree[reference.inverse];
relatedTree = inverted;
}
_.each(relatedTree, function (value, key) {
var singular = !_.isArray(value);
var obj = singular ? value : value[0];
if (typeof obj == 'object' && obj.ref == model.modelName) {
fields.push({
path: key,
model: obj.ref,
singular: singular,
inverse: obj.inverse
});
}
});

// Iterate over each relation
_.each(fields, function (field) {
// One-to-one
if (reference.singular && field.singular) {
promises.push(_this._updateOneToOne(
relatedModel, resource, reference, field
));
}
// One-to-many
if (reference.singular && !field.singular) {
promises.push(_this._updateOneToMany(
relatedModel, resource, reference, field
));
}
// Many-to-one
if (!reference.singular && field.singular) {
promises.push(_this._updateManyToOne(
relatedModel, resource, reference, field
));
}
// Many-to-many
if (!reference.singular && !field.singular) {
promises.push(_this._updateManyToMany(
relatedModel, resource, reference, field
));
}
});
});

return new Promise(function (resolve, reject) {
Promise.all(promises).then(
function () {
resolve(resource);
}, function (errors) {
reject(errors);
}
);
});
};

/**
* Update one-to-one mapping.
*
* @api private
* @parameter {Object} relatedModel
* @parameter {Object} resource
* @parameter {Object} reference
* @parameter {Object} field
* @return {Promise}
*/
adapter._updateOneToOne = function (relatedModel, resource, reference, field) {
return new Promise(function (resolve, reject) {
// Dissociation
var dissociate = {$unset: {}};
dissociate.$unset[field.path] = 1;
relatedModel.where(field.path, resource.id).update(dissociate, function (error) {
if (error) return reject(error);

// Association
var associate = {$set: {}};
associate.$set[field.path] = resource.id;

if (!resource[reference.path]) return resolve();
relatedModel.findByIdAndUpdate(resource[reference.path], associate, function (error) {
if (error) return reject(error);
resolve();
});
});
});
};

/**
* Update one-to-many mapping.
*
* @api private
* @parameter {Object} relatedModel
* @parameter {Object} resource
* @parameter {Object} reference
* @parameter {Object} field
* @return {Promise}
*/
adapter._updateOneToMany = function (relatedModel, resource, reference, field) {
return new Promise(function (resolve, reject) {
// Dissociation
var dissociate = {$pull: {}};
dissociate.$pull[field.path] = resource.id;
relatedModel.where(field.path, resource.id).update(dissociate, function (error) {
if (error) return reject(error);

// Association
var associate = {$addToSet: {}};
associate.$addToSet[field.path] = resource.id;

if (!resource[reference.path]) return resolve();
relatedModel.findByIdAndUpdate(resource[reference.path], associate, function (error) {
if (error) return reject(error);
resolve();
});
});
});
};

/**
* Update many-to-one mapping.
*
* @api private
* @parameter {Object} relatedModel
* @parameter {Object} resource
* @parameter {Object} reference
* @parameter {Object} field
* @return {Promise}
*/
adapter._updateManyToOne = function (relatedModel, resource, reference, field) {
return new Promise(function (resolve, reject) {
// Dissociation
var dissociate = {$unset: {}};
dissociate.$unset[field.path] = 1;

relatedModel.where(field.path, resource.id).update(dissociate, function (error) {
if (error) return reject(error);

// Association
var associate = {$set: {}};
associate.$set[field.path] = resource.id;
var ids = {_id: {$in: resource[reference.path] || []}};

relatedModel.update(ids, associate, {multi: true}, function (error) {
if (error) return reject(error);
resolve();
});
});
});
};

/**
* Update many-to-many mapping.
*
* @api private
* @parameter {Object} relatedModel
* @parameter {Object} resource
* @parameter {Object} reference
* @parameter {Object} field
* @return {Promise}
*/
adapter._updateManyToMany = function (relatedModel, resource, reference, field) {
return new Promise(function (resolve, reject) {
// Dissociation
var dissociate = {$pull: {}};
dissociate.$pull[field.path] = resource.id;

relatedModel.where(field.path, resource.id).update(dissociate, function (error) {
if (error) return reject(error);

// Association
var associate = {$addToSet: {}};
associate.$addToSet[field.path] = resource.id;
var ids = {_id: {$in: resource[reference.path] || []}};

relatedModel.update(ids, associate, {multi: true}, function (error) {
if (error) return reject(error);
resolve();
});
});
});
};

/**
* Remove all associations from a resource.
*
* @api private
* @parameter {Object} model
* @parameter {Object} resource
* @return {Object}
*/
adapter._dissociate = function (model, resource) {
model.schema.eachPath(function (path, type) {
var instance = type.instance ||
(type.caster ? type.caster.instance : undefined);
if (path != '_id' && instance == 'String' && uuidRegexp.test(resource[path])) {
resource[path] = null;
}
});
return resource;
resolve(_this._deserialize(model, resource));
};

// expose mongoose
Expand Down
12 changes: 6 additions & 6 deletions lib/harvester.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,22 +151,22 @@ Harvester.prototype.onChange = function (name, handlers) {
};

/**
* Define a resource and setup routes simultaneously. A resourceSchema field may be either a native type, a plain object, or a string that refers to a related resource.
* Define a resource and setup routes simultaneously. A schema field may be either a native type, a plain object, or a string that refers to a related resource.
*
* Valid native types: `String`, `Number`, `Boolean`, `Date`, `Array`, `Buffer`
*
* Alternatively, the object format must be as follows:
*
* ```javascript
* {type: String} // no association
* {ref: 'relatedResource', inverse: 'relatedKey'} // "belongs to" association to "relatedKey" key on "relatedResource"
* [{ref: 'anotherResource', inverse: 'someKey'}] // "has many" association to "someKey" on "anotherResource"
* [{ref: 'anotherResource', inverse: null}] // "has many" one-way association to "anotherResource"
* {ref: 'relatedResource'} // "belongs to" association to "relatedResource"
* 'relatedResource' // "belongs to" association to "relatedResource"
* ['anotherResource'] // "has many" one-way association to "anotherResource"
* ```
*
* @param {String} name the name of the resource
* @param {Object} resourceSchema the resourceSchema object to add
* @param {Object} options additional options to pass to the resourceSchema
* @param {Object} schema the schema object to add
* @param {Object} options additional options to pass to the schema
* @return {this}
*/
Harvester.prototype.resource = function (name, resourceSchema, options) {
Expand Down
Loading