diff --git a/app/imports/api/creature/archive/ArchivedCreatures.js b/app/imports/api/creature/archive/ArchivedCreatures.js new file mode 100644 index 00000000..4a572482 --- /dev/null +++ b/app/imports/api/creature/archive/ArchivedCreatures.js @@ -0,0 +1,57 @@ +import SimpleSchema from 'simpl-schema'; + +// Archived creatures is an immutable collection of creatures that are no longer +// in use and can be safely archived by the mongoDB hosting service. +// It keeps the working datasets like creatureProperties much smaller +// than they would otherwise be. +let ArchivedCreatures = new Mongo.Collection('archivedCreatures'); + +// We use blackbox objects for everything: +// - saves time checking every object against a schema +// - doesn't accidentaly create indices defined in subschemas +// - The objects we are archiving have already been checked against their +// own schemas +let ArchivedCreatureSchema = new SimpleSchema({ + owner: { + type: String, + regEx: SimpleSchema.RegEx.Id, + // The primary index on this collection + index: 1, + }, + archiveDate: { + type: Date, + // Indexed so the archiving system can archive documents when they + // get to a certain age + index: 1, + }, + creature: { + type: Object, + blackbox: true, + }, + properties: { + type: Array, + }, + 'properties.$': { + type: Object, + blackbox: true, + }, + experiences: { + type: Array, + }, + 'experiences.$': { + type: Object, + blackbox: true, + }, + logs: { + type: Array, + }, + 'logs.$': { + type: Object, + blackbox: true, + }, +}); + +ArchivedCreatures.attachSchema(ArchivedCreatureSchema); + +import '/imports/api/creature/archive/methods/index.js'; +export default ArchivedCreatures; diff --git a/app/imports/api/creature/archive/methods/archiveCreatures.js b/app/imports/api/creature/archive/methods/archiveCreatures.js new file mode 100644 index 00000000..a63c6563 --- /dev/null +++ b/app/imports/api/creature/archive/methods/archiveCreatures.js @@ -0,0 +1,66 @@ +import SimpleSchema from 'simpl-schema'; +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; +import { assertOwnership } from '/imports/api/creature/creaturePermissions.js'; +import Creatures from '/imports/api/creature/Creatures.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; +import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js'; +import Experiences from '/imports/api/creature/experience/Experiences.js'; +import { removeCreatureWork } from '/imports/api/creature/removeCreature.js'; +import ArchivedCreatures from '/imports/api/creature/archive/ArchivedCreatures.js'; + +function archiveCreature(creatureId){ + // Build the archive document + const creature = Creatures.findOne(creatureId); + const properties = CreatureProperties.find({'ancestors.id': creatureId}); + const experiences = Experiences.find({creatureId}); + const logs = CreatureLogs.find({creatureId}); + let archiveCreature = { + owner: creature.owner, + archiveDate: new Date(), + creature, + properties, + experiences, + logs, + }; + + // Insert it + let id = ArchivedCreatures.insert(archiveCreature); + + // Remove the original creature + removeCreatureWork(creatureId); + + return id; +} + +const archiveCreatures = new ValidatedMethod({ + name: 'Creatures.methods.archiveCreatures', + validate: new SimpleSchema({ + creatureIds: { + type: Array, + max: 10, + }, + 'creatureIds.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + }).validator(), + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 1, + timeInterval: 5000, + }, + run({creatureIds}) { + for (let id in creatureIds){ + assertOwnership(id, this.userId) + } + let archivedIds = []; + for (let id in creatureIds){ + let archivedId = archiveCreature(id); + archivedIds.push(archivedId); + } + return archivedIds; + }, +}); + +export default archiveCreatures; diff --git a/app/imports/api/creature/archive/methods/index.js b/app/imports/api/creature/archive/methods/index.js new file mode 100644 index 00000000..62d8bd8d --- /dev/null +++ b/app/imports/api/creature/archive/methods/index.js @@ -0,0 +1,2 @@ +import '/imports/api/creature/archive/methods/archiveCreatures.js'; +import '/imports/api/creature/archive/methods/restoreCreatures.js'; diff --git a/app/imports/api/creature/archive/methods/restoreCreatures.js b/app/imports/api/creature/archive/methods/restoreCreatures.js new file mode 100644 index 00000000..6a84ccf8 --- /dev/null +++ b/app/imports/api/creature/archive/methods/restoreCreatures.js @@ -0,0 +1,63 @@ +import SimpleSchema from 'simpl-schema'; +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; +import { assertOwnership } from '/imports/api/sharing/sharingPermissions.js'; +import Creatures from '/imports/api/creature/Creatures.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; +import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js'; +import Experiences from '/imports/api/creature/experience/Experiences.js'; +import ArchivedCreatures from '/imports/api/creature/archive/ArchivedCreatures.js'; + +function restoreCreature(archiveId){ + // Get the archive + const archivedCreature = ArchivedCreatures.findOne(archiveId); + + // Insert the creature sub documents + // They still have their original _id's + Creatures.insert(archivedCreature.creature); + CreatureProperties.batchInsert(archivedCreature.properties); + Experiences.batchInsert(archivedCreature.experiences); + CreatureLogs.batchInsert(archivedCreature.logs); + // Do not recompute. The creature was in a computed and ordered state when + // we archived it, just restore everything as-is + + // Remove the archived creature + ArchivedCreatures.remove(archiveId); + + return archivedCreature.creature._id; +} + +const restoreCreatures = new ValidatedMethod({ + name: 'Creatures.methods.restoreCreatures', + validate: new SimpleSchema({ + archiveIds: { + type: Array, + max: 10, + }, + 'archiveIds.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + }).validator(), + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 1, + timeInterval: 5000, + }, + run({archiveIds}) { + for (let id in archiveIds){ + let archivedCreature = ArchivedCreatures.findOne(id, { + fields: {owner: 1} + }); + assertOwnership(archivedCreature, this.userId) + } + let creatureIds = []; + for (let id in archiveIds){ + let creatureId = restoreCreature(id); + creatureIds.push(creatureId); + } + return creatureIds; + }, +}); + +export default restoreCreatures; diff --git a/app/imports/server/publications/archivedCreatures.js b/app/imports/server/publications/archivedCreatures.js new file mode 100644 index 00000000..155a887b --- /dev/null +++ b/app/imports/server/publications/archivedCreatures.js @@ -0,0 +1,18 @@ +import ArchivedCreatures from '/imports/api/creature/archive/ArchivedCreatures.js'; + +Meteor.publish('archivedCreatures', function(){ + this.autorun(function (){ + var userId = this.userId; + if (!userId) { + return []; + } + return ArchivedCreatures.find({ + owner: userId, + }, { + fields: { + creature: 1, + } + } + ); + }); +}); diff --git a/app/imports/server/publications/index.js b/app/imports/server/publications/index.js index c11cb255..adff4e49 100644 --- a/app/imports/server/publications/index.js +++ b/app/imports/server/publications/index.js @@ -6,5 +6,6 @@ import '/imports/server/publications/experiences.js'; import '/imports/server/publications/users.js'; import '/imports/server/publications/icons.js'; import '/imports/server/publications/tabletops.js'; -import '/imports/server/publications/slotFillers.js' -import '/imports/server/publications/ownedDocuments.js' +import '/imports/server/publications/slotFillers.js'; +import '/imports/server/publications/ownedDocuments.js'; +import '/imports/server/publications/archivedCreatures.js';