├── .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 |
--------------------------------------------------------------------------------