├── .gitignore ├── Polymer_Gun_electron-starter-Google-Chrome-7-7-2017-21_28_32.gif ├── README.md ├── gun-tag.js ├── index.html ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | bower_components 4 | *.log 5 | npm-debug.log* 6 | *.gif 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directories 28 | node_modules 29 | jspm_packages 30 | 31 | # Optional npm cache directory 32 | .npm 33 | 34 | # Optional REPL history 35 | .node_repl_history 36 | 37 | # ========================= 38 | # Operating System Files 39 | # ========================= 40 | 41 | # OSX 42 | # ========================= 43 | 44 | .DS_Store 45 | .AppleDouble 46 | .LSOverride 47 | 48 | # Thumbnails 49 | ._* 50 | 51 | # Files that might appear in the root of a volume 52 | .DocumentRevisions-V100 53 | .fseventsd 54 | .Spotlight-V100 55 | .TemporaryItems 56 | .Trashes 57 | .VolumeIcon.icns 58 | 59 | # Directories potentially created on remote AFP share 60 | .AppleDB 61 | .AppleDesktop 62 | Network Trash Folder 63 | Temporary Items 64 | .apdisk 65 | 66 | # Windows 67 | # ========================= 68 | 69 | # Windows image file caches 70 | Thumbs.db 71 | ehthumbs.db 72 | 73 | # Folder config file 74 | Desktop.ini 75 | 76 | # Recycle Bin used on file shares 77 | $RECYCLE.BIN/ 78 | 79 | # Windows Installer files 80 | *.cab 81 | *.msi 82 | *.msm 83 | *.msp 84 | 85 | # Windows shortcuts 86 | *.lnk 87 | -------------------------------------------------------------------------------- /Polymer_Gun_electron-starter-Google-Chrome-7-7-2017-21_28_32.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stefdv/gun-tag/ca88379f82e2303b1e1ba302011fede47699c7ed/Polymer_Gun_electron-starter-Google-Chrome-7-7-2017-21_28_32.gif -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gun-tag 2 | ## NOTE: You'll also need https://github.com/Stefdv/gun-synclist . I'm rewriting gun-tag and will include it when it's done. Sorry 3 | ### Intro 4 | 5 | GUN Tag is a plugin for Gun(^0.9.x) using `Gun.chain`. 6 | 7 | If you need to organize your data with tags/labels this might be useful. gun-tag enables you to tag and untag nodes to custom tags/labels. 8 | So for everyone who is struggling with deleting data in Gun ( You can't ) this could be a nice alternative. 9 | 10 | It will give you the following; 11 | * tag nodes 12 | * untag nodes 13 | * scopetag nodes 14 | * proptag nodes 15 | * find intersects (multiple tags) 16 | * filter nodes 17 | 18 | [What you could do with gun-tag](./Polymer_Gun_electron-starter-Google-Chrome-7-7-2017-21_28_32.gif) 19 | 20 | ### Important! 21 | `gun-tag` alters your nodes by adding extra properties to it. ( But so does Gun ) 22 | 23 | ### Setup 24 | 25 | ##### Prerequisites 26 | You'll need Gun ^0.9.x 27 | ``` 28 | npm install gun 29 | npm install gun-tag 30 | ``` 31 | 32 | ### Node.js 33 | ``` 34 | var Gun = require('gun'); 35 | require('gun-tag'); 36 | ``` 37 | 38 | ### Browser 39 | For the browser, it's much simpler, since your version of gun is exported as a global. Just include it as a script tag, and gun-tag takes care of the rest. 40 | ``` 41 | 42 | 43 | ``` 44 | 45 | ## API 46 | Several methods are exposed for your gun instance; 47 | * `.tag` 48 | * `.proptag` 49 | * `.untag` 50 | * `.switchtag` 51 | * `.tagged` 52 | * `.intersect` ( aka filtering) 53 | 54 | 55 | ## gun.get('Bob').tag('programmer') 56 | You can pass `.tag()` multiple names to index a node under. When called, it will try to read out your current context, index it under each tag, and then place each tag under a master list of every tag ever used. 57 | A tag can be a String or an Array (['one','Numbers/two','three']) 58 | 59 | ``` 60 | gun.put({ 61 | name: 'Bob', 62 | profession: 'developer' 63 | }).tag([ 64 | 'members', 65 | 'javascript developers', 66 | 'gunDB developers', 67 | 'examples' 68 | ]) 69 | ``` 70 | 71 | ### Additional : gun.tag('scope/name') 72 | The same as `tag` but when you put a '/' in your tagname the tag will be 'scoped'. 73 | 74 | #### Say what? 75 | Suppose you want to tag your Books and Movies. And you want to categorize them in themes also ( 'Fantasy','Comedy',etc), oh and tag them to the author. 76 | ``` 77 | gun.get('IT') 78 | .put({title:'IT',discription:'Some scary stuff about a clown'}) 79 | .tag([ 80 | 'Books/Horror', 81 | 'Books/Fantasy', 82 | 'Movies/Horror', 83 | 'Movies/'Fantasy', 84 | 'Authors/King', 85 | 'Fantasy/Book', 86 | 'Horror/Movie']); 87 | 88 | ``` 89 | 90 | Now there are several options to retrieve your data... 91 | First of all we can get a list of our 'Book' themes 92 | ``` 93 | gun.tagged('Books', data => { console.log(data) } ) 94 | /* data will be... 95 | { 96 | Fantasy:{'#':'Books/Fantasy'}, 97 | Horror:{'#':'Books/Horror'} 98 | } 99 | 100 | /* basicly the same as */ 101 | gun.get('Books/TAGS').once(Gun.log) 102 | 103 | ``` 104 | Note: It is no longer required to add '/TAGS' to the scoped tags.
105 | eg: `gun.tagged('Books/TAGS')` is the same as `gun.tagged('Books')`.
gun-tag knows its scopes. 106 | 107 | 108 | To get all Fantasy books 109 | 110 | ``` 111 | gun.tagged('Books/Fantasy',cb) 112 | // will return all - full - nodes that are tagged to 'Books/Fantasy' 113 | ``` 114 | 115 | #### Usefull ? 116 | Well yeah... You could create a selector 'Choose Book theme' and let the user select a theme. Upon selection you present all books belonging to that theme. 117 | 118 | ### gun.get().untag('name') 119 | The same 'rules' as tagging but now the nodes get 'untagged'. 120 | > nodes that are untagged will only be filtered out when you use `gun.tagged(tag,cb)`.
121 | Using `gun.tagged(tag).once(cb)` or `gun.get('tagname').once(cb)` will NOT leave out untagged nodes. 122 | 123 | ### gun.get().proptag('name') 124 | A special kind of tag. With 'proptag' the provided tag wil be set as a direct property on the node. This can be usefull if you want to quickly check if a node 'is' or 'has' something. 125 | ``` 126 | gun.get('Bob').put({name:'Bob'}).proptag('has paid') 127 | gun.get('Bob').once(cb) // {name:'Bob','has paid':true} 128 | ``` 129 | 130 | A proptag can be untagged like any other tag. 131 | ``` 132 | gun.get('Bob').untag('has paid'); 133 | gun.get('Bob').once(cb) // {name:'Bob','has paid':false} 134 | ``` 135 | ### gun.get().switchtag('from','to') 136 | The same as doing `gun.get("Bob").untag('married')` and then `gun.get("Bob").tag('single')` 137 | ``` 138 | gun.get('Bob').switchtag('married','single') 139 | ``` 140 | 141 | ### gun.tagged() 142 | When no arguments are provided you get the full tag list.
143 | `gun.tagged().once(cb)` 144 | 145 | ####Changed!!! 146 | You can now do `gun.tagged(cb)` 147 | ``` 148 | gun.tagged(tags=>{ 149 | // {BOOKS:{'#':'BOOKS/TAGS'}}, 150 | // {MOVIES:{'#':'MOVIES/TAGS'}}, 151 | // {MEMBERS:{'#':'MEMBERS'}} 152 | }) 153 | ``` 154 | 155 | ### gun.tagged(tag, cb) 156 | Provide a tagname and a callback to get all valid members of that tag.
157 | The returned nodes are full objects. 158 | ``` 159 | gun.tagged('gunDb',list => console.log(list) ) 160 | 161 | ``` 162 | 163 | ``` 164 | gun.tagged('Books/Fantasy',cb) // all fantasy books 165 | ``` 166 | ### Deleting nodes 167 | Ah yes... a returning question in gitter and stackoverflow.
168 | The short answer "You can't...not really", there are solutions like 'nulling' your node...
169 | At least with `gun-tag` you'll have another option. 170 | 171 | Let's first create a regular `gun.set()` 172 | ``` 173 | let members = gun.get('members'); 174 | 175 | // assume we have an Array with member objects 176 | 177 | allmembers.forEach( member => { 178 | // store the member in Gun 179 | let m = gun.get( Gun.text.random() ).put(member); 180 | // tag member 181 | m.tag( ['MEMBERS','MEMBERS/Paid']) 182 | // put member in `set` 183 | members.set( m) ; 184 | }); 185 | ``` 186 | Great, now instead of building a visual list from our set, we build it from our tagged members. 187 | ``` 188 | gun.tagged('MEMBERS', data => { 189 | // data.list -> create nice member list 190 | }) 191 | ``` 192 | Let's create an extra page in our app, one that shows the members that actually paid there contribution. 193 | 194 | ``` 195 | gun.tagged('MEMBERS/Paid', data => { 196 | // data.list -> create list with members that paid 197 | }) 198 | ``` 199 | Oh.. but Bob didn't pay. 200 | ``` 201 | gun.get('Bob').untag('MEMBERS/Paid'); 202 | ``` 203 | So if we would refresh our list... 204 | >please read the section about subscribing further down. 205 | 206 | ``` 207 | gun.tagged('MEMBERS/Paid', data => { 208 | // data.list -> all members that 'paid'...not Bob! 209 | }); 210 | ``` 211 | Once a month ( or so ) we want to kick the members that didn't pay. 212 | So we get our ( tagged ) members again and filter the list. 213 | >We could use our set ( `gun.get('MEMBERS')` ) but using the tagged list give you some advantages...
214 | Nodes returned from `gun.tagged()` are actually tagged and have an extra '_soul' property. 215 | 216 | 217 | ``` 218 | gun.tagged('MEMBERS', data => { 219 | data.list.forEach(member=>{ 220 | if(member.tags['MEMBERS/Paid] === 0) { 221 | // and there is Bob... 222 | gun.get(member._soul).untag('MEMBER'); 223 | } 224 | }) 225 | }) 226 | ``` 227 | Now Bob is no longer a member...
228 | 229 | ``` 230 | gun.tagged('MEMBERS', data => { 231 | // data.list -> all members ...no more 'Bob' 232 | }) 233 | ``` 234 | >So there you have it...deleted ? No, but Bob won't show up either. 235 | 236 | After a year we want to invite ex-members again.
237 | Luckily we still have our original `set()` 238 | ``` 239 | gun.get('MEMBERS').listonce(data=>{ 240 | data.list.forEach(member=>{ 241 | if(member.tags['MEMBERS'] === 0) { 242 | // Bob !!! -> Invite him 243 | } 244 | }) 245 | }) 246 | 247 | ``` 248 | >You'll notice i use `synclist` and `listonce` in my examples. `synclist` ( includes `listonce`) is one of my other Gun extensions. You can read more about it [here](https://github.com/Stefdv/gun-synclist) 249 | 250 | ### intersect or filter 251 | 252 | An intersect is a list with nodes that are tagged to ALL provided tags. This is a feature of `gun-tagged()`.
253 | 254 | We can achieve this by providing an Array to `gun.tagged()`.
255 | Get all Fantasy books, written by (Stephen) King , published in 1988. 256 | ``` 257 | gun.intersect( ['Books/Fantasy','Authors/King','Published/1988'],cb) 258 | ``` 259 | 260 | ### Subscribing - a word of advise. 261 | Offcourse it is possible to subscribe to changes on a tag like `gun.get('Books/Lent').synclist(cb)` and you will be notified when the status of those books changes. But then you'll have to subscibe to 'Books/borrowed' also...But what if you borrowed a book from one friend and loan it to another? (despite the fact that it is not a nice thing to do ;p) You'll end up with two subscriptions on the same book. 262 | 263 | #### my advise 264 | Store all books in a `set()`, then subscribe to that. 265 | ``` 266 | let books = gun.get('BOOKS') 267 | myBooks.forEach(book=>{ 268 | books.set(book) 269 | }); 270 | books.synclist( data=> { 271 | if(data.list) { // all books } 272 | if( data.node) { // your changed ( tagged maybe ? ) book} 273 | }) 274 | ``` 275 | Now you can go wild with tagging/untagging/retagging your books. Every change will trigger the `synclist()` callback. 276 | 277 | ### Disclaimer 278 | gun-tag is born of necessity, and over time became really powerfull if you fully understand it's potential. That being said... tell me what you want to do and - maybe- i can advise you. I'm not much of an email reader so ping me in [gitter](https://gitter.im/amark/gun) @Stefdv 279 | 280 | ### Credits 281 | Thanks to Mark Nadal for writing Gun and for helping out whenever i need. 282 | -------------------------------------------------------------------------------- /gun-tag.js: -------------------------------------------------------------------------------- 1 | /** 2 | * gun-tag 3 | * @author S.J.J. de Vries ( Stefdv2@hotmail.com) 4 | * @gitter @Stefdv 5 | * @purpose Add tagging capabilities to Gun 6 | * 7 | * @version 3.0.2 8 | * Now up to date for Gun v0.9.x. 9 | * 10 | * @dependencies 11 | * Gun 12 | * gun-synclist 13 | */ 14 | 15 | ;(function(){ 16 | console.log("Also thanks for using - or at least trying - gun-tag.") 17 | if(typeof window !== "undefined"){ 18 | var Gun = window.Gun; 19 | } else { 20 | var Gun = require('gun/gun'); 21 | } 22 | 23 | const _scope = "gun-tag/"; 24 | const _scopes = "_scopes"; 25 | 26 | const invalid = value => { 27 | if (!Gun.obj.is(value) || !value._) { 28 | console.warn('Only nodes can be tagged'); 29 | return true; 30 | }; 31 | }; 32 | 33 | const validateNode = (node,tag) => { 34 | return Object.keys(node).reduce(function(previous, current) { 35 | return (node.tags && node.tags[tag] === 1); 36 | }, {}); 37 | }; 38 | 39 | const serialize = (gun,tags,method) => { 40 | tags = Gun.list.is(tags) ? tags : Array.prototype.slice.call(tags); 41 | tags.forEach( tag => gun[method](tag) ) 42 | }; 43 | 44 | const getScopes = (gun) => { 45 | return new Promise(resolve => { 46 | let root = gun.back(-1); 47 | root.get(_scope + _scopes ).once( sc => { 48 | delete ((sc = Gun.obj.copy(sc))||{})._; 49 | resolve(sc) 50 | }) 51 | }) 52 | 53 | }; 54 | 55 | const isScope = (gun,tag) => { 56 | return new Promise(resolve => { 57 | getScopes(gun) 58 | .then( sc => { 59 | resolve(Object.keys(sc).includes(tag)) 60 | }) 61 | .catch(error => { 62 | console.log(error); 63 | }); 64 | }) 65 | }; 66 | 67 | Gun.chain.tag = function (tag) { 68 | if(!tag || typeof(tag) === "number") { return this}; 69 | if (Gun.list.is(tag) ) { return serialize(this, tag, 'tag');} 70 | 71 | let gun = this.back(-1); 72 | let nodeSoul,scopeSoul,newScope,newTag; 73 | 74 | return this.once(function (node) { 75 | if (invalid(node)) { return this;} 76 | nodeSoul= Gun.node.soul(node); 77 | 78 | // consider a tag with slash ("Books/Fantasy") a scoped 79 | // tag 80 | if(tag.includes('/')) { 81 | newScope = tag.split('/')[0]; 82 | newTag = tag.split('/')[1]; 83 | gun.get(_scope+'TAGS').get(newScope).put({'#': newScope+'/TAGS'}); 84 | gun.get(_scope + _scopes).get(newScope).put(1); 85 | gun.get(newScope+'/TAGS').get(newTag).put({'#':tag}); 86 | gun.get(tag).get(nodeSoul).put({'#':nodeSoul}); 87 | } else { 88 | gun.get(_scope+'TAGS').get(tag).put({'#':_scope + tag}); 89 | gun.get(_scope+tag).get(nodeSoul).put({'#':nodeSoul}); 90 | }; 91 | this.get('tags').get(tag).put(1); 92 | }) 93 | }; 94 | /* Gun.chain.path is in the lib folder of Gun, 95 | but i'm not sure if it's there to stay ;) */ 96 | Gun.chain.path = function(field, opt){ 97 | var back = this, gun = back, tmp; 98 | if(typeof field === 'string'){ 99 | tmp = field.split(opt || '.'); 100 | if(1 === tmp.length){ 101 | gun = back.get(field); 102 | return gun; 103 | } 104 | field = tmp; 105 | } 106 | if(field instanceof Array){ 107 | if(field.length > 1){ 108 | gun = back; 109 | var i = 0, l = field.length; 110 | for(i; i < l; i++){ 111 | gun = gun.get(field[i]); 112 | } 113 | } else { 114 | gun = back.get(field[0]); 115 | } 116 | return gun; 117 | } 118 | if(!field && 0 != field){ 119 | return back; 120 | } 121 | gun = back.get(''+field); 122 | return gun; 123 | }; 124 | 125 | Gun.chain.untag = function (tag) { 126 | if(!Gun.text.is(tag)) { return this; }; 127 | if (arguments.length !== 1 || Gun.list.is(tag)) { 128 | return serialize(this, arguments,'untag'); 129 | }; 130 | return this.once(function (node) { 131 | if (invalid(node)) { return this;} 132 | node[tag] ? this.get(tag).put(false) : this.get('tags').get(tag).put(0); 133 | }); 134 | }; 135 | 136 | Gun.chain.switchtag = function(from,to) { 137 | if(!Gun.text.is(from)||!Gun.text.is(to)) { return this; }; 138 | this.untag(from); 139 | this.tag(to) 140 | }; 141 | 142 | Gun.chain.proptag = function(tag){ 143 | if(!Gun.text.is(tag)) { return this; }; 144 | if (arguments.length > 1 || Gun.list.is(tag) ) { 145 | return serialize(this, tag,'proptag'); 146 | }; 147 | let gun = this.back(-1); 148 | return this.once(function (node) { 149 | if (invalid(node)) { return this;} 150 | let nodeSoul = Gun.node.soul(node) 151 | gun.get(_scope+'TAGS').get(tag).put({'#':_scope+tag}); 152 | gun.get(_scope+tag).get(nodeSoul).put( {'#':nodeSoul} ); 153 | this.get(tag).put(true); 154 | }); 155 | }; 156 | 157 | Gun.chain.tagged = function(tag,cb,opt) { 158 | let gun = this; 159 | cb = cb || function(){}; 160 | opt=opt || {}; 161 | 162 | if(arguments.length === 0) { 163 | return gun.get(_scope + 'TAGS'); // return all tagnames 164 | }; 165 | 166 | if(!Gun.text.is(tag) &&! Gun.list.is(tag)){ 167 | if(typeof(tag)=='function') { 168 | let cb = tag 169 | gun.get(_scope+'TAGS').listonce(data=>{ 170 | delete ((data = Gun.obj.copy(data))||{})._; 171 | cb.call(gun,data) 172 | }) 173 | } else { 174 | console.warn( 'tags must be a String or an Array of Strings!', tag); 175 | return gun; 176 | } 177 | }; 178 | 179 | if(Gun.text.is(tag) && arguments.length === 1) { 180 | if(tag.includes('/') ) { return gun.get(tag); } // tagged as 'books/fantasy' 181 | return gun.get(_scope + tag); 182 | }; 183 | 184 | if(Gun.text.is(tag) && arguments.length > 1) { 185 | 186 | gun.get(_scope + tag ).listonce( data => { 187 | delete ((data = Gun.obj.copy(data))||{})._; 188 | data.lookup ={}; 189 | data.list = data.list.reduce((list, node) => { 190 | if(validateNode(node,tag)){ 191 | data.lookup[node._soul] = list.length; 192 | list.push(node); 193 | } 194 | return list 195 | },[]); 196 | cb.call(gun,data) 197 | }) 198 | } 199 | }; 200 | 201 | Gun.chain.intersect = function(tags,cb) { 202 | if(!Gun.list.is(tags) || arguments.length==1) { 203 | console.warn("for intersects you need to provide an Array with - existing - tags AND a callback") 204 | return this 205 | } 206 | let matchTags = tags; 207 | gun.tagged(matchTags[0],data=>{ 208 | data.list = data.list.reduce(function(result, node) { 209 | let cnt = 0; 210 | matchTags.forEach(tag => { 211 | if(node.tags[tag] && node.tags[tag]===1) { 212 | cnt++; 213 | if(cnt == matchTags.length) { result.push(node)} 214 | }; 215 | }); 216 | return result; 217 | },[]); 218 | cb.call(gun,data.list) 219 | }); 220 | }; 221 | 222 | }()); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gun-tag", 3 | "version": "3.0.2", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "gun-synclist": { 8 | "version": "1.0.2", 9 | "resolved": "https://registry.npmjs.org/gun-synclist/-/gun-synclist-1.0.2.tgz", 10 | "integrity": "sha512-05e0Ar92MY2SLLvWJBiELnLAIAFtwSPSwFSj7n7TpZzpWEnjPkRXMz3GznyKlD+5FyDcHpMeuMJq+s9JjFD6/w==" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gun-tag", 3 | "version": "3.0.2", 4 | "description": "Add tagging capabilities to gun!", 5 | "main": "gun-tag.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Stefdv/gun-tag.git" 12 | }, 13 | "keywords": [ 14 | "gun", 15 | "tag", 16 | "tagging", 17 | "tagger" 18 | ], 19 | "author": "S.J.J. de Vries ( Stefdv2@hotmail.com)", 20 | "license": "(ZLIB | MIT | Apache-2.0)", 21 | "bugs": { 22 | "url": "https://github.com/Stefdv/gun-tag/issues" 23 | }, 24 | "homepage": "https://github.com/Stefdv/gun-tag#readme", 25 | "dependencies": { 26 | "gun-synclist": "^1.0.2" 27 | } 28 | } 29 | --------------------------------------------------------------------------------