diff --git a/rpg-docs/.meteor/packages b/rpg-docs/.meteor/packages index 0838d8c1..487418fb 100644 --- a/rpg-docs/.meteor/packages +++ b/rpg-docs/.meteor/packages @@ -18,4 +18,4 @@ aldeed:autoform conielo:autoform-polymer-paper msavin:mongol matb33:collection-hooks -sewdn:collection-behaviours +zimme:collection-softremovable diff --git a/rpg-docs/.meteor/versions b/rpg-docs/.meteor/versions index 97e54621..5aa24f35 100644 --- a/rpg-docs/.meteor/versions +++ b/rpg-docs/.meteor/versions @@ -15,6 +15,7 @@ blaze-tools@1.0.3 boilerplate-generator@1.0.3 callback-hook@1.0.3 check@1.0.5 +coffeescript@1.0.6 conielo:autoform-polymer-paper@0.1.1 dburles:collection-helpers@1.0.2 dburles:mongo-collection-instances@0.3.3 @@ -67,7 +68,6 @@ retry@1.0.3 routepolicy@1.0.5 service-configuration@1.0.4 session@1.1.0 -sewdn:collection-behaviours@0.2.0 sha@1.0.3 spacebars@1.0.6 spacebars-compiler@1.0.5 @@ -79,3 +79,5 @@ underscore@1.0.3 url@1.0.4 webapp@1.2.0 webapp-hashing@1.0.3 +zimme:collection-behaviours@1.0.3 +zimme:collection-softremovable@1.0.3 diff --git a/rpg-docs/Model/Character/Actions.js b/rpg-docs/Model/Character/Actions.js index 7ac458e2..ee1fd1ed 100644 --- a/rpg-docs/Model/Character/Actions.js +++ b/rpg-docs/Model/Character/Actions.js @@ -27,5 +27,8 @@ Schemas.Action = new SimpleSchema({ Actions.attachSchema(Schemas.Action); +Actions.attachBehaviour('softRemovable'); +makeChild(Actions); + Actions.allow(CHARACTER_SUBSCHEMA_ALLOW); Actions.deny(CHARACTER_SUBSCHEMA_DENY); diff --git a/rpg-docs/Model/Character/Attacks.js b/rpg-docs/Model/Character/Attacks.js index 3ad6862d..ce317555 100644 --- a/rpg-docs/Model/Character/Attacks.js +++ b/rpg-docs/Model/Character/Attacks.js @@ -49,10 +49,8 @@ Schemas.Attack = new SimpleSchema({ allowedValues: ["editable", "feature", "class", "buff", "equipment", "racial", "inate"] }, //the id of the feature, buff or item that created this effect - sourceId: { - type: String, - regEx: SimpleSchema.RegEx.Id, - optional: true + parent: { + type: Schemas.Parent }, color: { type: String, @@ -67,5 +65,8 @@ Schemas.Attack = new SimpleSchema({ Attacks.attachSchema(Schemas.Attack); +Attacks.attachBehaviour('softRemovable'); +makeChild(Attacks); //children of lots of things + Attacks.allow(CHARACTER_SUBSCHEMA_ALLOW); Attacks.deny(CHARACTER_SUBSCHEMA_DENY); diff --git a/rpg-docs/Model/Character/Buffs.js b/rpg-docs/Model/Character/Buffs.js index 471c15d7..20d8cbe6 100644 --- a/rpg-docs/Model/Character/Buffs.js +++ b/rpg-docs/Model/Character/Buffs.js @@ -20,11 +20,8 @@ Schemas.Buff = new SimpleSchema({ Buffs.attachSchema(Schemas.Buff); -Buffs.before.remove(function (userId, buff) { - Effects.find({sourceId: buff._id, type: "buff"}).forEach(function(effect){ - Effects.remove(effect._id); - }); -}); +Buffs.attachBehaviour('softRemovable'); +makeParent(Buffs); //parents of effects and attacks Buffs.allow(CHARACTER_SUBSCHEMA_ALLOW); Buffs.deny(CHARACTER_SUBSCHEMA_DENY); diff --git a/rpg-docs/Model/Character/SubSchemas/Characters.js b/rpg-docs/Model/Character/Characters.js similarity index 94% rename from rpg-docs/Model/Character/SubSchemas/Characters.js rename to rpg-docs/Model/Character/Characters.js index 894c0a5f..ebba3cfb 100644 --- a/rpg-docs/Model/Character/SubSchemas/Characters.js +++ b/rpg-docs/Model/Character/Characters.js @@ -458,28 +458,3 @@ Characters.deny({ return _.contains(fields, 'owner'); } }); - -CHARACTER_SUBSCHEMA_ALLOW = { - // the user must be logged in, and the user must be a writer of the character - insert: function (userId, doc) { - var char = Characters.findOne( doc.charId, { fields: {owner: 1, writers: 1} } ); - return ( userId && char.owner === userId || _.contains(char.writers, userId) ); - }, - update: function (userId, doc, fields, modifier) { - var char = Characters.findOne( doc.charId, { fields: {owner: 1, writers: 1} } ); - return ( userId && char.owner === userId || _.contains(char.writers, userId) ); - }, - remove: function (userId, doc) { - var char = Characters.findOne( doc.charId, { fields: {owner: 1, writers: 1} } ); - return ( userId && char.owner === userId || _.contains(char.writers, userId) ); - }, - fetch: ["charId"] -}; - -CHARACTER_SUBSCHEMA_DENY = { - update: function (userId, docs, fields, modifier) { - // can't change character - return _.contains(fields, 'charId'); - }, - fetch: ["charId"] -}; diff --git a/rpg-docs/Model/Character/Classes.js b/rpg-docs/Model/Character/Classes.js index f6dd0c93..d0513475 100644 --- a/rpg-docs/Model/Character/Classes.js +++ b/rpg-docs/Model/Character/Classes.js @@ -21,5 +21,8 @@ Schemas.Class = new SimpleSchema({ Classes.attachSchema(Schemas.Class); +Classes.attachBehaviour('softRemovable'); +makeParent(Classes); //parents of effects and attacks + Classes.allow(CHARACTER_SUBSCHEMA_ALLOW); Classes.deny(CHARACTER_SUBSCHEMA_DENY); diff --git a/rpg-docs/Model/Character/Effects.js b/rpg-docs/Model/Character/Effects.js index a4d456c8..1fb7613b 100644 --- a/rpg-docs/Model/Character/Effects.js +++ b/rpg-docs/Model/Character/Effects.js @@ -35,11 +35,9 @@ Schemas.Effect = new SimpleSchema({ defaultValue: "editable", allowedValues: ["editable", "feature", "class", "buff", "equipment", "racial", "inate"] }, - //the id of the feature, buff or item that created this effect - sourceId: { - type: String, - regEx: SimpleSchema.RegEx.Id, - optional: true + //the thing that created this effect + parent: { + type: Schemas.Parent }, //which stat the effect is applied to stat: { @@ -91,5 +89,8 @@ Characters.after.insert(function (userId, char) { } }); +Effects.attachBehaviour('softRemovable'); +makeChild(Effects); //children of lots of things + Effects.allow(CHARACTER_SUBSCHEMA_ALLOW); Effects.deny(CHARACTER_SUBSCHEMA_DENY); diff --git a/rpg-docs/Model/Character/Experience.js b/rpg-docs/Model/Character/Experience.js index 1fb2dfd3..ce806ec7 100644 --- a/rpg-docs/Model/Character/Experience.js +++ b/rpg-docs/Model/Character/Experience.js @@ -21,5 +21,7 @@ Schemas.Experience = new SimpleSchema({ Experiences.attachSchema(Schemas.Experience); +Experiences.attachBehaviour('softRemovable'); + Experiences.allow(CHARACTER_SUBSCHEMA_ALLOW); Experiences.deny(CHARACTER_SUBSCHEMA_DENY); diff --git a/rpg-docs/Model/Character/Features.js b/rpg-docs/Model/Character/Features.js index 41cc8f55..2e367521 100644 --- a/rpg-docs/Model/Character/Features.js +++ b/rpg-docs/Model/Character/Features.js @@ -22,20 +22,8 @@ Features.helpers({ } }); -//Delete effects where this the removed feature is source -Features.before.remove(function (userId, feature) { - Effects.find({sourceId: feature._id, type: "feature"}).forEach(function(effect){ - Effects.remove(effect._id); - }); -}); - -//keep the effects up to date with enabled state -Features.after.update(function (userId, feature, fieldNames, modifier, options) { - var enabled = feature.enabled !== "disabled"; - Effects.find({sourceId: feature._id, type: "feature"}).forEach(function(effect){ - Effects.update(effect._id, { $set: {charId: feature.charId, enabled: enabled, name: feature.name} }); - }); -}, {fetchPrevious: false}); +Features.attachBehaviour('softRemovable'); +makeParent(Features); //parents of effects and attacks Features.allow(CHARACTER_SUBSCHEMA_ALLOW); Features.deny(CHARACTER_SUBSCHEMA_DENY); diff --git a/rpg-docs/Model/Character/Notes.js b/rpg-docs/Model/Character/Notes.js index 85036995..eaf2b9e3 100644 --- a/rpg-docs/Model/Character/Notes.js +++ b/rpg-docs/Model/Character/Notes.js @@ -9,5 +9,7 @@ Schemas.Note = new SimpleSchema({ Notes.attachSchema(Schemas.Note); +Notes.attachBehaviour('softRemovable'); + Notes.allow(CHARACTER_SUBSCHEMA_ALLOW); Notes.deny(CHARACTER_SUBSCHEMA_DENY); diff --git a/rpg-docs/Model/Character/Proficiencies.js b/rpg-docs/Model/Character/Proficiencies.js index a511e049..3c05adde 100644 --- a/rpg-docs/Model/Character/Proficiencies.js +++ b/rpg-docs/Model/Character/Proficiencies.js @@ -15,15 +15,12 @@ Schemas.Proficiency = new SimpleSchema({ defaultValue: "editable", allowedValues: ["editable", "feature", "buff", "equipment", "inate"] }, - //the id of the feature, buff or item that created this effect - sourceId: { - type: String, - regEx: SimpleSchema.RegEx.Id, - optional: true - }, }); Proficiencies.attachSchema(Schemas.Proficiency); +Proficiencies.attachBehaviour('softRemovable'); +makeChild(Proficiencies); + Proficiencies.allow(CHARACTER_SUBSCHEMA_ALLOW); Proficiencies.deny(CHARACTER_SUBSCHEMA_DENY); diff --git a/rpg-docs/Model/Character/SpellLists.js b/rpg-docs/Model/Character/SpellLists.js index 03663bcd..d5d58d5f 100644 --- a/rpg-docs/Model/Character/SpellLists.js +++ b/rpg-docs/Model/Character/SpellLists.js @@ -23,15 +23,8 @@ SpellLists.helpers({ } }); -SpellLists.before.remove(function (userId, list) { - if(Meteor.isServer){ - Spells.remove({listId: list._id}); - } else { - Spells.find({listId: list._id}).forEach(function(spell){ - Spells.remove(spell._id); - }); - } -}); +SpellLists.attachBehaviour('softRemovable'); +makeParent(SpellLists); //parents of spells SpellLists.allow(CHARACTER_SUBSCHEMA_ALLOW); SpellLists.deny(CHARACTER_SUBSCHEMA_DENY); diff --git a/rpg-docs/Model/Character/Spells.js b/rpg-docs/Model/Character/Spells.js index c91f0ef2..3af0e22e 100644 --- a/rpg-docs/Model/Character/Spells.js +++ b/rpg-docs/Model/Character/Spells.js @@ -2,7 +2,6 @@ Spells = new Mongo.Collection("spells"); Schemas.Spell = new SimpleSchema({ charId: {type: String, regEx: SimpleSchema.RegEx.Id}, - listId: {type: String, regEx: SimpleSchema.RegEx.Id}, prepared: {type: String, defaultValue: "unprepared", allowedValues: ["prepared","unprepared","always"]}, name: {type: String, trim: false}, description: {type: String, optional: true, trim: false}, @@ -21,5 +20,8 @@ Schemas.Spell = new SimpleSchema({ Spells.attachSchema(Schemas.Spell); +Spells.attachBehaviour('softRemovable'); +makeChild(Spells); //children of spell lists + Spells.allow(CHARACTER_SUBSCHEMA_ALLOW); Spells.deny(CHARACTER_SUBSCHEMA_DENY); diff --git a/rpg-docs/Model/Character/SubSchemas/SubSchemas/Adjustment.js b/rpg-docs/Model/Character/SubSchemas/Adjustment.js similarity index 100% rename from rpg-docs/Model/Character/SubSchemas/SubSchemas/Adjustment.js rename to rpg-docs/Model/Character/SubSchemas/Adjustment.js diff --git a/rpg-docs/Model/Character/SubSchemas/SubSchemas/Attribute.js b/rpg-docs/Model/Character/SubSchemas/Attribute.js similarity index 100% rename from rpg-docs/Model/Character/SubSchemas/SubSchemas/Attribute.js rename to rpg-docs/Model/Character/SubSchemas/Attribute.js diff --git a/rpg-docs/Model/Character/SubSchemas/SubSchemas/DeathSaves.js b/rpg-docs/Model/Character/SubSchemas/DeathSaves.js similarity index 98% rename from rpg-docs/Model/Character/SubSchemas/SubSchemas/DeathSaves.js rename to rpg-docs/Model/Character/SubSchemas/DeathSaves.js index 00d7d8bb..f7b11d00 100644 --- a/rpg-docs/Model/Character/SubSchemas/SubSchemas/DeathSaves.js +++ b/rpg-docs/Model/Character/SubSchemas/DeathSaves.js @@ -19,4 +19,4 @@ Schemas.DeathSave = new SimpleSchema({ type: Boolean, defaultValue: false } -}); \ No newline at end of file +}); diff --git a/rpg-docs/Model/Character/SubSchemas/SubSchemas/Skill.js b/rpg-docs/Model/Character/SubSchemas/Skill.js similarity index 100% rename from rpg-docs/Model/Character/SubSchemas/SubSchemas/Skill.js rename to rpg-docs/Model/Character/SubSchemas/Skill.js diff --git a/rpg-docs/Model/Inventory/Containers.js b/rpg-docs/Model/Inventory/Containers.js index 8d1d74d3..e68f5a21 100644 --- a/rpg-docs/Model/Inventory/Containers.js +++ b/rpg-docs/Model/Inventory/Containers.js @@ -30,14 +30,7 @@ Containers.helpers({ } }); -Containers.before.remove(function (userId, container) { - if(Meteor.isServer){ - Items.remove({container: container._id}); - } else { - Items.find({container: container._id}).forEach(function(item){ - Items.remove(item._id); - }); - } -}); +Containers.attachBehaviour('softRemovable'); +makeParent(Containers); //parents of items Containers.allow(CHARACTER_SUBSCHEMA_ALLOW); diff --git a/rpg-docs/Model/Inventory/Items.js b/rpg-docs/Model/Inventory/Items.js index ccf7ac3a..dadf6508 100644 --- a/rpg-docs/Model/Inventory/Items.js +++ b/rpg-docs/Model/Inventory/Items.js @@ -4,7 +4,6 @@ Schemas.Item = new SimpleSchema({ name: {type: String, defaultValue: "New Item", trim: false}, plural: {type: String, optional: true, trim: false}, description:{type: String, optional: true, trim: false}, - container: {type: String, regEx: SimpleSchema.RegEx.Id}, //id of container it is normally stowed in charId: {type: String, regEx: SimpleSchema.RegEx.Id}, //id of owner quantity: {type: Number, min: 0, defaultValue: 1}, weight: {type: Number, min: 0, defaultValue: 0, decimal: true}, @@ -14,8 +13,8 @@ Schemas.Item = new SimpleSchema({ defaultValue: "none", allowedValues: ["none", "head", "armor", "arms", "hands", "held", "feet"] }, - equipped: {type: Boolean, defaultValue: false}, - color: {type: String, allowedValues: _.pluck(colorOptions, "key"), defaultValue: "q"} + enabled: {type: Boolean, defaultValue: false}, + color: {type: String, allowedValues: _.pluck(colorOptions, "key"), defaultValue: "q"} }); Items.attachSchema(Schemas.Item); @@ -36,24 +35,8 @@ Items.helpers({ } }); -//remove effects and attacks if their item source is removed -Items.before.remove(function (userId, item) { - Effects.find({sourceId: item._id, type: "equipment"}).forEach(function(effect){ - Effects.remove(effect._id); - }); - Attacks.find({sourceId: item._id, type: "equipment"}).forEach(function(attack){ - Attacks.remove(attack._id); - }); -}); - -//keep the effects and attacks on the correct character and enabled when equipped -Items.after.update(function (userId, item, fieldNames, modifier, options) { - Effects.find({sourceId: item._id, type: "equipment"}).forEach(function(effect){ - Effects.update(effect._id, { $set: {charId: item.charId, enabled: item.equipped, name: item.name} }); - }); - Attacks.find({sourceId: item._id, type: "equipment"}).forEach(function(attack){ - Attacks.update(attack._id, { $set: {charId: item.charId, enabled: item.equipped, name: item.name} }); - }); -}, {fetchPrevious: false}); +Items.attachBehaviour('softRemovable'); +makeChild(Items); //children of containers +makeParent(Items); //parents of effects and attacks Items.allow(CHARACTER_SUBSCHEMA_ALLOW); diff --git a/rpg-docs/lib/constants/characterAssetAllowDeny.js b/rpg-docs/lib/constants/characterAssetAllowDeny.js new file mode 100644 index 00000000..c6069db6 --- /dev/null +++ b/rpg-docs/lib/constants/characterAssetAllowDeny.js @@ -0,0 +1,24 @@ +CHARACTER_SUBSCHEMA_ALLOW = { + // the user must be logged in, and the user must be a writer of the character + insert: function (userId, doc) { + var char = Characters.findOne( doc.charId, { fields: {owner: 1, writers: 1} } ); + return ( userId && char.owner === userId || _.contains(char.writers, userId) ); + }, + update: function (userId, doc, fields, modifier) { + var char = Characters.findOne( doc.charId, { fields: {owner: 1, writers: 1} } ); + return ( userId && char.owner === userId || _.contains(char.writers, userId) ); + }, + remove: function (userId, doc) { + var char = Characters.findOne( doc.charId, { fields: {owner: 1, writers: 1} } ); + return ( userId && char.owner === userId || _.contains(char.writers, userId) ); + }, + fetch: ["charId"] +}; + +CHARACTER_SUBSCHEMA_DENY = { + update: function (userId, docs, fields, modifier) { + // can't change character + return _.contains(fields, 'charId'); + }, + fetch: ["charId"] +}; diff --git a/rpg-docs/lib/functions/parenting.js b/rpg-docs/lib/functions/parenting.js new file mode 100644 index 00000000..c29c04e9 --- /dev/null +++ b/rpg-docs/lib/functions/parenting.js @@ -0,0 +1,112 @@ +var childSchema = new SimpleSchema({ + parent: { type: Object }, + 'parent.collection': { type: String }, + 'parent.id': { type: String, regEx: SimpleSchema.RegEx.Id } +}); + +var joinWithDefaultKeys = function(keys){ + var defaultKeys = [ + 'charId', + 'enabled', + 'removed', + 'removedAt', + 'removedBy', + 'restoredAt', + 'restoredBy' + ]; + return _.union(keys, defaultKeys); +} + +var childCollections = []; + +makeChild = function(collection, inheritedKeys){ + collection.inheritedKeys = joinWithDefaultKeys(inheritedKeys); + + collection.helpers({ + //returns the parent even if it's removed + getParent: function(){ + var parentCol = Meteor.isClient? + window[this.parent.collection] : global[this.parent.collection]; + if (parentCol) + return parentCol.findOne(this.parent.id, {removed: true}); + }, + getParentCollection: function(){ + return Meteor.isClient? + window[this.parent.collection] : global[this.parent.collection]; + } + }); + + //when we change parents, inherit its properties + collection.after.update(function (userId, doc, fieldNames, modifier, options) { + if(modifier && modifier.$set && modifier.$set.parent){ + var parent = doc.getParent(); + if(!parent) throw new Meteor.Error('Parenting Error', + 'Document\'s parent does not exist'); + var handMeDowns = _.pick(parent, collection.inheritedKeys); + collection.update(doc._id, {$set: handMeDowns}); + } + }); + + collection.attachSchema(childSchema); + + childCollections.push(collection); +}; + +makeParent = function(collection, donatedKeys){ + collection.donatedKeys = joinWithDefaultKeys(donatedKeys); + + //after changing, push the changes to all children + collection.after.update(function (userId, doc, fieldNames, modifier, options) { + if(!modifier) return; + modifier = _.pick(modifier, ['$set', '$unset']); + modifier.$set = _.pick(modifier.$set, donatedKeys); + modifier.$unset = _.pick(modifier.$unset, donatedKeys); + doc = _.pick(doc, ['_id','charId']); + Meteor.call('updateChildren', doc, modifier); + }); + + collection.after.remove(function (userId, doc) { + doc = _.pick(doc, ['_id','charId']); + Meteor.call('removeChildren', doc); + }); +}; + +var checkPermission = function(userId, charId){ + var char = Characters.findOne( charId, { fields: {owner: 1, writers: 1} } ); + if(!char) + throw new Meteor.Error('Access Denied', + 'Character '+charId+' does not exist'); + if (!userId) + throw new Meteor.Error('Access Denied', + 'No UserId set when trying to update character asset.'); + if (char.owner !== userId && !_.contains(char.writers, userId)) + throw new Meteor.Error('Access Denied', + 'Not permitted to update assets of this character.'); + return true; +}; + +Meteor.methods({ + updateChildren: function (parent, modifier) { + check(parent, {_id: String, charId: String}); + check(modifier, Object); + checkPermission(this.userId, parent.charId); + + _.each(childCollections, function(collection){ + collection.update( + {charId: parent.charId, 'parent.id': parent._id}, + modifier, + {multi: true} + ); + }); + }, + removeChildren: function (parent) { + check(parent, {_id: String, charId: String}); + checkPermission(this.userId, parent.charId); + + _.each(childCollections, function(collection){ + collection.remove( + {charId: parent.charId, 'parent.id': parent._id} + ); + }); + } +}); diff --git a/rpg-docs/server/publications/characterList.js b/rpg-docs/server/publications/characterList.js index 1469989d..413512eb 100644 --- a/rpg-docs/server/publications/characterList.js +++ b/rpg-docs/server/publications/characterList.js @@ -1,4 +1,10 @@ Meteor.publish("characterList",function(userId){ if(!userId) return; - return Characters.find({$or: [ {readers: userId}, {writers: userId}, {owner: userId} ] }); + return Characters.find({ + $or: [ + {readers: userId}, + {writers: userId}, + {owner: userId} + ] + }); }); diff --git a/rpg-docs/server/publications/singleCharacter.js b/rpg-docs/server/publications/singleCharacter.js index 78e5468a..823478cd 100644 --- a/rpg-docs/server/publications/singleCharacter.js +++ b/rpg-docs/server/publications/singleCharacter.js @@ -1,19 +1,29 @@ Meteor.publish("singleCharacter", function(characterId, userId){ - //TODO check if this characer can be viewed by this user - return [ - Characters.find({_id: characterId}), - - Actions.find({charId: characterId}), - Attacks.find({charId: characterId}), - Classes.find({charId: characterId}), - Containers.find({charId: characterId}), - Effects.find({charId: characterId}), - Experiences.find({charId: characterId}), - Features.find({charId: characterId}), - Items.find({charId: characterId}), - Notes.find({charId: characterId}), - Spells.find({charId: characterId}), - SpellLists.find({charId: characterId}), - TemporaryHitPoints.find({charId: characterId}), - ]; + if( + Characters.findOne({ + _id: characterId, + $or: [ + {readers: userId}, + {writers: userId}, + {owner: userId} + ] + }) + ){ + return [ + Characters.find({_id: characterId}), + //get all the assets for this character including soft deleted ones + Actions.find ({charId: characterId}, {removed: true}), + Attacks.find ({charId: characterId}, {removed: true}), + Classes.find ({charId: characterId}, {removed: true}), + Containers.find ({charId: characterId}, {removed: true}), + Effects.find ({charId: characterId}, {removed: true}), + Experiences.find ({charId: characterId}, {removed: true}), + Features.find ({charId: characterId}, {removed: true}), + Items.find ({charId: characterId}, {removed: true}), + Notes.find ({charId: characterId}, {removed: true}), + Spells.find ({charId: characterId}, {removed: true}), + SpellLists.find ({charId: characterId}, {removed: true}), + TemporaryHitPoints.find({charId: characterId}, {removed: true}), + ]; + } });