├── .editorconfig ├── .gitattributes ├── .gitignore ├── .versions ├── LICENSE.md ├── README.md ├── package.js └── softremovable.coffee /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | # top-most EditorConfig file 6 | root = true 7 | 8 | # Match all files 9 | [*] 10 | charset = utf-8 11 | end_of_line = lf 12 | indent_size = 2 13 | indent_style = space 14 | insert_final_newline = true 15 | max_line_length = 80 16 | trim_trailing_whitespace = true 17 | 18 | [*.md] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .build* 2 | -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | allow-deny@1.0.5 2 | babel-compiler@6.19.1 3 | babel-runtime@1.0.1 4 | base64@1.0.10 5 | binary-heap@1.0.10 6 | blaze@2.3.2 7 | blaze-tools@1.0.10 8 | boilerplate-generator@1.1.0 9 | callback-hook@1.0.10 10 | check@1.2.5 11 | coffeescript@1.0.6 12 | ddp@1.2.5 13 | ddp-client@1.3.4 14 | ddp-common@1.2.8 15 | ddp-server@1.3.14 16 | deps@1.0.12 17 | diff-sequence@1.0.7 18 | ecmascript@0.8.0 19 | ecmascript-runtime@0.4.1 20 | ecmascript-runtime-client@0.4.1 21 | ecmascript-runtime-server@0.4.1 22 | ejson@1.0.13 23 | geojson-utils@1.0.10 24 | html-tools@1.0.11 25 | htmljs@1.0.11 26 | id-map@1.0.9 27 | jquery@1.11.10 28 | logging@1.1.17 29 | matb33:collection-hooks@0.9.0-rc.1 30 | meteor@1.6.1 31 | minimongo@1.2.0 32 | modules@0.9.0 33 | modules-runtime@0.8.0 34 | mongo@1.1.18 35 | mongo-id@1.0.6 36 | npm-mongo@2.2.24 37 | observe-sequence@1.0.16 38 | ordered-dict@1.0.9 39 | promise@0.8.9 40 | random@1.0.10 41 | reactive-var@1.0.11 42 | retry@1.0.9 43 | routepolicy@1.0.12 44 | spacebars@1.0.15 45 | spacebars-compiler@1.1.2 46 | tracker@1.1.3 47 | ui@1.0.13 48 | underscore@1.0.10 49 | webapp@1.3.16 50 | webapp-hashing@1.0.9 51 | zimme:collection-behaviours@1.0.4 52 | zimme:collection-softremovable@1.0.6-beta.2 53 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2015 Simon Fridlund 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gitter](https://img.shields.io/badge/gitter-join_chat-brightgreen.svg)] 2 | (https://gitter.im/zimme/meteor-collection-softremovable) 3 | [![Code Climate](https://img.shields.io/codeclimate/github/zimme/meteor-collection-softremovable.svg)] 4 | (https://codeclimate.com/github/zimme/meteor-collection-softremovable) 5 | 6 | # Soft remove for collections 7 | 8 | Add soft remove to collections. 9 | 10 | ### Install 11 | ```sh 12 | meteor add zimme:collection-softremovable 13 | ``` 14 | 15 | ### Usage 16 | 17 | Basic usage examples. 18 | 19 | #### Attach 20 | 21 | ```js 22 | Posts = new Mongo.Collection('posts'); 23 | 24 | //Attach behaviour with the default options 25 | Posts.attachBehaviour('softRemovable'); 26 | 27 | //Attach behaviour with custom options 28 | Posts.attachBehaviour('softRemovable', { 29 | removed: 'deleted', 30 | removedBy: false, 31 | restoredAt: 'undeletedAt', 32 | restoredBy: false 33 | }); 34 | ``` 35 | 36 | #### Remove/Restore 37 | 38 | ```js 39 | // Soft remove document by _id 40 | Posts.softRemove({_id: 'BFpDzGuWG8extPwrE'}); 41 | 42 | // Restore document by _id 43 | Posts.restore('BFpDzGuWG8extPwrE'); 44 | 45 | // Actually remove document from collection 46 | Posts.remove({_id: 'BFpDzGuWG8extPwrE'}); 47 | ``` 48 | 49 | #### Find 50 | 51 | ```js 52 | // Find all posts except soft removed posts 53 | Posts.find({}); 54 | 55 | // Find only posts that have been soft removed 56 | Posts.find({removed: true}); 57 | 58 | // Find all posts including removed 59 | Posts.find({}, {removed: true}); 60 | ``` 61 | 62 | #### Publish 63 | 64 | For you to be able to find soft removed documents on the client you will need 65 | to explicitly publish those. The example code below belongs in server-side code. 66 | 67 | ```js 68 | Meteor.publish('posts', function() { 69 | return Posts.find({}); 70 | }); 71 | 72 | Meteor.publish('removedPosts', function() { 73 | return Posts.find({removed: true}); 74 | }); 75 | 76 | Meteor.publish('allPosts', function() { 77 | return Posts.find({}, {removed: true}); 78 | }); 79 | ``` 80 | 81 | ### Options 82 | 83 | The following options can be used: 84 | 85 | * `removed`: Optional. Set to `'string'` to change the fields name. 86 | This field can't be omitted. 87 | 88 | * `removedAt`: Optional. Set to `'string'` to change the fields name. 89 | Set to `false` to omit field. 90 | 91 | * `removedBy`: Optional. Set to `'string'` to change the fields name. 92 | Set to `false` to omit field. 93 | 94 | * `restoredAt`: Optional. Set to `'string'` to change the fields name. 95 | Set to `false` to omit field. 96 | 97 | * `restoredBy`: Optional. Set to `'string'` to change the fields name. 98 | Set to `false` to omit field. 99 | 100 | * `systemId`: Optional. Set to `'string'` to change the id representing the 101 | system. 102 | 103 | ### Global configuration 104 | 105 | ```js 106 | // Configure behaviour globally 107 | // All collection using this behaviour will use these settings as defaults 108 | // The settings below are the package default settings 109 | CollectionBehaviours.configure('softRemovable',{ 110 | removed: 'removed', 111 | removedAt: 'removedAt', 112 | removedBy: 'removedBy', 113 | restoredAt: 'restoredAt', 114 | restoredBy: 'restoredAt', 115 | systemId: '0' 116 | }); 117 | ``` 118 | 119 | ### Notes 120 | 121 | * This package attaches a schema to the collection if `aldeed:simple-schema`, 122 | `aldeed:collection2` and/or `aldeed:autoform` are used in the application. 123 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | git: 'https://github.com/zimme/meteor-collection-softremovable.git', 3 | name: 'zimme:collection-softremovable', 4 | summary: 'Add soft remove to collections', 5 | version: '1.0.6-beta.2' 6 | }); 7 | 8 | Package.onUse(function(api) { 9 | api.versionsFrom('1.0'); 10 | 11 | api.use([ 12 | 'check', 13 | 'coffeescript', 14 | 'underscore' 15 | ]); 16 | 17 | api.use([ 18 | 'matb33:collection-hooks@0.9.0-rc.1', 19 | 'zimme:collection-behaviours@1.0.3' 20 | ]); 21 | 22 | api.use([ 23 | 'aldeed:autoform@4.0.0 || 5.0.0', 24 | 'aldeed:collection2@2.0.0', 25 | 'aldeed:simple-schema@1.0.3' 26 | ], ['client', 'server'], {weak: true}); 27 | 28 | api.imply('zimme:collection-behaviours'); 29 | 30 | api.addFiles('softremovable.coffee'); 31 | }); 32 | -------------------------------------------------------------------------------- /softremovable.coffee: -------------------------------------------------------------------------------- 1 | af = Package['aldeed:autoform'] 2 | c2 = Package['aldeed:collection2'] 3 | SimpleSchema = Package['aldeed:simple-schema']?.SimpleSchema 4 | 5 | defaults = 6 | removed: 'removed' 7 | removedAt: 'removedAt' 8 | removedBy: 'removedBy' 9 | restoredAt: 'restoredAt' 10 | restoredBy: 'restoredBy' 11 | systemId: '0' 12 | 13 | behaviour = (options = {}) -> 14 | check options, Object 15 | 16 | {removed, removedAt, removedBy, restoredAt, restoredBy, systemId} = 17 | _.defaults options, @options, defaults 18 | 19 | if c2? 20 | afDefinition = autoform: 21 | omit: true 22 | 23 | addAfDef = (definition) -> 24 | _.extend definition, afDefinition 25 | 26 | definition = {} 27 | 28 | def = definition[removed] = 29 | optional: true 30 | type: Boolean 31 | 32 | addAfDef def if af? 33 | 34 | if removedAt 35 | def = definition[removedAt] = 36 | denyInsert: true 37 | optional: true 38 | type: Date 39 | 40 | addAfDef def if af? 41 | 42 | regEx = new RegExp "(#{SimpleSchema.RegEx.Id.source})|^#{systemId}$" 43 | 44 | if removedBy 45 | def = definition[removedBy] = 46 | denyInsert: true 47 | optional: true 48 | regEx: regEx 49 | type: String 50 | 51 | addAfDef def if af? 52 | 53 | if restoredAt 54 | def = definition[restoredAt] = 55 | denyInsert: true 56 | optional: true 57 | type: Date 58 | 59 | addAfDef def if af? 60 | 61 | if restoredBy 62 | def = definition[restoredBy] = 63 | denyInsert: true 64 | optional: true 65 | regEx: regEx 66 | type: String 67 | 68 | addAfDef def if af? 69 | 70 | @collection.attachSchema new SimpleSchema definition 71 | 72 | beforeFindHook = (userId = systemId, selector, options = {}) -> 73 | return if not selector 74 | if _.isString selector 75 | selector = 76 | _id: selector 77 | 78 | if Match.test(selector, Object) and not (options.removed or selector[removed]?) 79 | selector = _.clone selector 80 | selector[removed] = 81 | $exists: false 82 | 83 | @args[0] = selector 84 | return 85 | 86 | @collection.before.find beforeFindHook 87 | @collection.before.findOne beforeFindHook 88 | 89 | @collection.before.update (userId = systemId, doc, fieldNames, modifier, 90 | options) -> 91 | 92 | $set = modifier.$set ?= {} 93 | $unset = modifier.$unset ?= {} 94 | 95 | if $set[removed] and doc[removed]? 96 | return false 97 | 98 | if $unset[removed] and not doc[removed]? 99 | return false 100 | 101 | if $set[removed] and not doc[removed]? 102 | $set[removed] = true 103 | 104 | if removedAt 105 | $set[removedAt] = new Date 106 | 107 | if removedBy 108 | $set[removedBy] = userId 109 | 110 | if restoredAt 111 | $unset[restoredAt] = true 112 | 113 | if restoredBy 114 | $unset[restoredBy] = true 115 | 116 | if $unset[removed] and doc[removed]? 117 | $unset[removed] = true 118 | 119 | if removedAt 120 | $unset[removedAt] = true 121 | 122 | if removedBy 123 | $unset[removedBy] = true 124 | 125 | if restoredAt 126 | $set[restoredAt] = new Date 127 | 128 | if restoredBy 129 | $set[restoredBy] = userId 130 | 131 | if _.isEmpty $set 132 | delete modifier.$set 133 | 134 | if _.isEmpty $unset 135 | delete modifier.$unset 136 | 137 | isLocalCollection = @collection._connection is null 138 | 139 | @collection.softRemove = (selector, callback) -> 140 | return 0 unless selector 141 | 142 | modifier = 143 | $set: $set = {} 144 | 145 | $set[removed] = true 146 | 147 | try 148 | if Meteor.isServer or isLocalCollection 149 | ret = @update selector, modifier, multi: true, callback 150 | 151 | else 152 | ret = @update selector, modifier, callback 153 | 154 | catch error 155 | if error.reason.indexOf 'Not permitted.' isnt -1 156 | throw new Meteor.Error 403, 'Not permitted. Untrusted code may only ' + 157 | "softRemove documents by ID." 158 | 159 | if ret is false 160 | 0 161 | else 162 | ret 163 | 164 | @collection.restore = (selector, callback) -> 165 | return 0 unless selector 166 | 167 | modifier = 168 | $unset: $unset = {} 169 | 170 | $unset[removed] = true 171 | 172 | try 173 | if Meteor.isServer or isLocalCollection 174 | selector = _.clone selector 175 | selector[removed] = true 176 | ret = @update selector, modifier, multi: true, callback 177 | 178 | else 179 | ret = @update selector, modifier, callback 180 | 181 | catch error 182 | if error.reason.indexOf 'Not permitted.' isnt -1 183 | throw new Meteor.Error 403, 'Not permitted. Untrusted code may only ' + 184 | "restore documents by ID." 185 | 186 | if ret is false 187 | 0 188 | else 189 | ret 190 | 191 | CollectionBehaviours.define 'softRemovable', behaviour 192 | --------------------------------------------------------------------------------