├── .babelrc ├── .circleci └── config.yml ├── .github └── workflows │ ├── main.yml │ └── package-update.yml ├── .gitignore ├── README.md ├── __snapshots__ └── index.test.js.snap ├── async.js ├── async.test.js ├── index.js ├── index.test.js ├── package.json ├── react ├── __snapshots__ │ └── index.test.js.snap ├── index.js └── index.test.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"], 3 | "plugins": ["@babel/plugin-transform-runtime"] 4 | } 5 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:lts 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/repo 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | 29 | - run: yarn install 30 | 31 | - save_cache: 32 | paths: 33 | - node_modules 34 | key: v1-dependencies-{{ checksum "package.json" }} 35 | 36 | # run tests! 37 | - run: yarn test 38 | - run: npx semantic-release 39 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v1 16 | - uses: actions/setup-node@v1 17 | with: 18 | node-version: 12 19 | - run: yarn 20 | - run: yarn test 21 | -------------------------------------------------------------------------------- /.github/workflows/package-update.yml: -------------------------------------------------------------------------------- 1 | on: 2 | schedule: 3 | - cron: 0 0 * * 3 4 | name: Update 5 | jobs: 6 | package-update: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@master 10 | - name: set remote url 11 | run: git remote set-url --push origin https://$GITHUB_ACTOR:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY 12 | - name: package-update 13 | uses: taichi/actions-package-update@master 14 | env: 15 | AUTHOR_EMAIL: brysgo@gmail.com 16 | AUTHOR_NAME: Bryan Goldstein 17 | EXECUTE: "true" 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | UPDATE_COMMAND: yarn 20 | with: 21 | args: upgrade --latest 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | ██████╗ ██████╗ █████╗ ██████╗ ██╗ ██╗ ██████╗ ██╗ ██████╗ ██╗ ██╗███╗ ██╗ 3 | ██╔════╝ ██╔══██╗██╔══██╗██╔══██╗██║ ██║██╔═══██╗██║ ██╔════╝ ██║ ██║████╗ ██║ 4 | ██║ ███╗██████╔╝███████║██████╔╝███████║██║ ██║██║ █████╗██║ ███╗██║ ██║██╔██╗ ██║ 5 | ██║ ██║██╔══██╗██╔══██║██╔═══╝ ██╔══██║██║▄▄ ██║██║ ╚════╝██║ ██║██║ ██║██║╚██╗██║ 6 | ╚██████╔╝██║ ██║██║ ██║██║ ██║ ██║╚██████╔╝███████╗ ╚██████╔╝╚██████╔╝██║ ╚████║ 7 | ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚══▀▀═╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝ 8 | ``` 9 | 10 | ![Test](https://github.com/brysgo/graphql-gun/workflows/Test/badge.svg) 11 | 12 | Augmented query interface for the graph universal database http://gun.js.org/ 13 | 14 | `npm install graphql-gun` 15 | 16 | ## With React 17 | 18 | Say you want to attach offline first, realtime data to the Color component. 19 | ```javascript 20 | const gql = require("graphql-tag"); 21 | const Gun = require("gun"); 22 | const React = require("react"); 23 | const ReactDOM = require("react-dom"); 24 | const gun = Gun(); 25 | const { createContainer, graphqlGun } = require('graphql-gun/react')({React, gun}); 26 | 27 | const Color = ({color, data}) => ( 28 | // data will be passed in by the container with all the data you asked for 29 | // component will also redraw when your subscriptions update 30 |
{JSON.stringify(data, null, 2)}
31 | ) 32 | ``` 33 | 34 | You can use a relay inspired high order component to decorate it with live data: 35 | 36 | 37 | ```javascript 38 | let ColorContainer = createContainer(Color, { 39 | fragments: { 40 | data: gql`{ 41 | fish @live { 42 | red { 43 | name 44 | } 45 | 46 | blue { 47 | _chain 48 | } 49 | 50 | friends(type: Set) { 51 | name 52 | favoriteColor 53 | } 54 | } 55 | }` 56 | } 57 | }); 58 | ``` 59 | 60 | ...or if you prefer apollo client: 61 | 62 | ```javascript 63 | ColorContainer = graphqlGun(gql`{ 64 | fish @live { 65 | red { 66 | name 67 | } 68 | 69 | blue { 70 | _chain 71 | } 72 | 73 | friends(type: Set) { 74 | name 75 | favoriteColor 76 | } 77 | } 78 | }`)(Color); 79 | ``` 80 | 81 | Then just render like normal. 82 | 83 | ```javascript 84 | ReactDOM.render( 85 | , 86 | document.getElementById('root') 87 | ); 88 | 89 | ``` 90 | 91 | ## Without React 92 | 93 | Not using react? 94 | 95 | You can use `graphqlGun` with a more traditional imperative approach: 96 | 97 | ```javascript 98 | const graphqlGun = require('graphql-gun'); 99 | const Gun = require('gun'); 100 | const gql = require('graphql-tag') 101 | 102 | const gun = Gun(); 103 | 104 | const fish = gun.get('fish'); 105 | fish.put({red: {name: 'Frank'}}); 106 | fish.put({blue: {name: 'John'}}); 107 | const friends = fish.get('friends'); 108 | const dori = fish.get('dori') 109 | const martin = fish.get('martin') 110 | const nemo = fish.get('nemo') 111 | dori.put({ name: 'Dori', favoriteColor: 'blue' }); 112 | martin.put({ name: 'Martin', favoriteColor: 'orange' }); 113 | nemo.put({ name: 'Nemo', favoriteColor: 'gold' }); 114 | friends.set(dori); 115 | friends.set(martin); 116 | friends.set(nemo); 117 | 118 | const myQuery = gql`{ 119 | fish { 120 | red { 121 | name 122 | } 123 | 124 | blue { 125 | _chain 126 | } 127 | 128 | friends(type: Set) { 129 | name 130 | favoriteColor 131 | } 132 | } 133 | }`; 134 | 135 | graphqlGun(myQuery, gun).then(function(results) { 136 | console.log('results: ', results); 137 | }); 138 | ``` 139 | 140 | and it will print... 141 | 142 | ```javascript 143 | { 144 | fish: { 145 | red: { 146 | name: 'Frank' // the name you set on the red fish 147 | }, 148 | blue: { 149 | _chain: // reference to gun chain at blue node 150 | }, 151 | friends: [ 152 | { name: 'Dori', favoriteColor: 'blue' }, 153 | { name: 'Martin', favoriteColor: 'orange' }, 154 | { name: 'Nemo', favoriteColor: 'gold' } 155 | ] 156 | } 157 | } 158 | ``` 159 | 160 | Use the live directive to subscribe via an promise/iterator combo. 161 | 162 | 163 | ```javascript 164 | const myQuery = gql`{ 165 | fish { 166 | red @live { 167 | name 168 | } 169 | } 170 | }`; 171 | 172 | const { next } = graphqlGun(myQuery, gun); 173 | 174 | console.log(await next()); 175 | ``` 176 | 177 | Will print... 178 | 179 | ```javascript 180 | { 181 | fish: { 182 | red: { 183 | name: 'Frank' // the name you set on the red fish 184 | } 185 | } 186 | } 187 | ``` 188 | 189 | Then try: 190 | 191 | ```javascript 192 | gun.get('fish').get('red').put({name: 'bob'}); 193 | 194 | console.log(await next()); 195 | ``` 196 | 197 | And you will get... 198 | 199 | ```javascript 200 | { 201 | fish: { 202 | red: { 203 | name: 'bob' // the updated name 204 | } 205 | } 206 | } 207 | ``` 208 | 209 | Take a look at the tests to learn more. 210 | 211 | 212 | ## Credits 213 | 214 | Special thanks to [@amark](https://github.com/amark/) for creating [Gun](https://github.com/amark/gun) and answering all my noob questions. 215 | 216 | Shout out to [@stubailo](https://github.com/stubailo/) for putting up with my late night [graphql-anywhere](https://github.com/amark/) PRs. 217 | 218 | Also a shout out to everyone on the Gun [gitter](https://gitter.im/amark/gun) chat for talking through things. 219 | -------------------------------------------------------------------------------- /__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`graphqlGun can do the basics 1`] = ` 4 | Object { 5 | "gGcdtb": Object { 6 | "bar": "baz", 7 | }, 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /async.js: -------------------------------------------------------------------------------- 1 | function toPromiseFactory(consumer) { 2 | const results = []; 3 | const resolves = []; 4 | consumer(function(result) { 5 | if (resolves.length > 0) { 6 | resolves.shift()(result); 7 | } else { 8 | results.push(result); 9 | } 10 | }); 11 | 12 | return function() { 13 | if (results.length > 0) { 14 | return Promise.resolve(results.shift()); 15 | } else { 16 | return new Promise(resolve => resolves.push(resolve)); 17 | } 18 | }; 19 | } 20 | 21 | function thunkish(cb) { 22 | const subscribers = []; 23 | const results = []; 24 | 25 | cb(function(updatedObj) { 26 | results.push(updatedObj); 27 | subscribers.forEach(sub => sub(updatedObj)); 28 | }); 29 | 30 | const result = function(subscriber) { 31 | results.forEach(res => subscriber(res)); 32 | subscribers.push(subscriber); 33 | }; 34 | result.toPromiseFactory = toPromiseFactory.bind(null, result); 35 | result.isThunk = true; 36 | return result; 37 | } 38 | 39 | module.exports = { 40 | thunkish, 41 | deferrableOrImmediate(obj, fn) { 42 | if (obj && obj.isThunk) { 43 | return thunkish(function(sendUpdate) { 44 | obj(function(updatedObj) { 45 | fn(updatedObj); 46 | sendUpdate(updatedObj); 47 | }); 48 | }); 49 | } else { 50 | return fn(obj); 51 | } 52 | }, 53 | arrayOrDeferrable(arr) { 54 | const thunks = []; 55 | const thunkResults = {}; 56 | const result = arr.map(function(obj, i) { 57 | if (obj && obj.isThunk) { 58 | thunks.push({ obj, i }); 59 | return undefined; 60 | } else { 61 | return obj; 62 | } 63 | }); 64 | if (thunks.length === 0) { 65 | return result; 66 | } else { 67 | return thunkish(function(updateArray) { 68 | thunks.forEach(({ obj: t, i }) => { 69 | t(function(obj) { 70 | thunkResults[i] = obj; 71 | result[i] = obj; 72 | 73 | if (Object.values(thunkResults).length === thunks.length) { 74 | updateArray(result); 75 | } 76 | }); 77 | }); 78 | }); 79 | } 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /async.test.js: -------------------------------------------------------------------------------- 1 | import { thunkish } from "./async"; 2 | describe("async", () => { 3 | describe("thunkish", () => { 4 | it("starts with a producer and can have many consumers", () => { 5 | let producer; 6 | const consumeA = jest.fn(); 7 | const consumeB = jest.fn(); 8 | const consumer = thunkish(function(produce) { 9 | producer = produce; 10 | }); 11 | producer("foo"); 12 | producer("bar"); 13 | consumer(consumeA); 14 | expect(consumeA).toHaveBeenCalledTimes(2); 15 | producer("baz"); 16 | expect(consumeA).toHaveBeenCalledTimes(3); 17 | expect(consumeB).not.toHaveBeenCalled(); 18 | consumer(consumeB); 19 | expect(consumeB).toHaveBeenCalledTimes(3); 20 | producer("done"); 21 | expect(consumeA).toHaveBeenCalledTimes(4); 22 | expect(consumeB).toHaveBeenCalledTimes(4); 23 | }); 24 | describe("toPromiseFactory", () => { 25 | it("makes the thunkish behave like a promise factory", async () => { 26 | let producer; 27 | const consumer = thunkish(function(produce) { 28 | producer = produce; 29 | }); 30 | const next = consumer.toPromiseFactory(); 31 | producer("testing promise factory"); 32 | expect(await next()).toEqual("testing promise factory"); 33 | }); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const Gun = require("gun/gun"); 2 | const graphql = require("graphql-anywhere").default; 3 | const { 4 | thunkish, 5 | deferrableOrImmediate, 6 | arrayOrDeferrable 7 | } = require("./async"); 8 | const tryGet = require("try-get"); 9 | 10 | module.exports = function graphqlGun(query, gun) { 11 | gun = gun || Gun(); 12 | let resultValue = {}; 13 | let subscriptions = {}; 14 | const resolver = (fieldName, container, args, context, info) => { 15 | let key = info.resultKey; 16 | const { 17 | subscribe: parentSubscribed, 18 | index: indexInList, 19 | ref: parentRef, 20 | path, 21 | chain 22 | } = container; 23 | let ref = parentRef; 24 | let subscribe = 25 | (parentSubscribed || "live" in tryGet(info, "directives", {})) && 26 | !("unlive" in tryGet(info, "directives", {})); 27 | 28 | if (info.isLeaf) { 29 | if (key === "_chain") { 30 | ref[key] = chain; 31 | return chain; 32 | } else { 33 | return thunkish(resolve => { 34 | const updater = val => { 35 | if (!!val && val[key]) { 36 | ref[key] = val[key]; 37 | resolve(val[key]); 38 | } else { 39 | ref[key] = val; 40 | resolve(val); 41 | } 42 | }; 43 | const stringPath = [...path, key].join("."); 44 | if (subscribe && subscriptions[stringPath] === undefined) { 45 | subscriptions[stringPath] = chain.get(key).on(updater, true); 46 | } else { 47 | chain.get(key).once(updater); 48 | } 49 | }); 50 | } 51 | } else if (args && args.type === "Set") { 52 | ref[key] = ref[key] || []; 53 | ref = ref[key]; 54 | const keyValueSet = {}; 55 | const resultSet = {}; 56 | 57 | const t = thunkish(function(rerunChild) { 58 | const updater = function(data, _key, at) { 59 | var gunRef = this; // also `at.gun` 60 | Gun.obj.map(data, function(val, field) { 61 | // or a for in 62 | if (field === "_") return; 63 | keyValueSet[field] = keyValueSet[field] || {}; 64 | resultSet[field] = { 65 | chain: gunRef.get(field), 66 | subscribe, 67 | ref: keyValueSet[field], 68 | path: [...path, key, field] 69 | }; 70 | }); 71 | ref.splice(0, ref.length, ...Object.values(keyValueSet)); 72 | rerunChild(Object.values(resultSet)); 73 | }; 74 | chain.get(key).on(updater, true); 75 | }); 76 | return t; 77 | } else { 78 | ref[key] = ref[key] || {}; 79 | return { 80 | chain: chain.get(key), 81 | subscribe, 82 | path: [...path, key], 83 | ref: ref[key] 84 | }; 85 | } 86 | }; 87 | 88 | const graphqlOut = graphql( 89 | resolver, 90 | query, 91 | { path: [], ref: resultValue, chain: gun }, 92 | null, 93 | null, 94 | { 95 | deferrableOrImmediate, 96 | arrayOrDeferrable 97 | } 98 | ); 99 | const thunk = thunkish(function(triggerUpdate) { 100 | triggerUpdate(resultValue); 101 | if (graphqlOut.isThunk) { 102 | graphqlOut(function(actualRes) { 103 | triggerUpdate(resultValue); // TODO: Figure out how to use actualRes instead of tracking resultValue 104 | }); 105 | } 106 | }); 107 | const result = thunk.toPromiseFactory()(); 108 | result.next = thunk.toPromiseFactory(); 109 | result[Symbol.asyncIterator] = function() { 110 | const factory = thunk.toPromiseFactory(); 111 | return { 112 | next: () => 113 | factory().then(value => ({ 114 | value, 115 | done: false 116 | })) 117 | }; 118 | }; 119 | result[Symbol.iterator] = result[Symbol.asyncIterator]; // TODO: Depricate this usage 120 | return result; 121 | }; 122 | -------------------------------------------------------------------------------- /index.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, expect */ 2 | const gql = require("graphql-tag"); 3 | const Gun = require("gun/gun"); 4 | const graphqlGun = require("./"); 5 | 6 | const gun = Gun(); 7 | 8 | describe("graphqlGun", () => { 9 | it("can do the basics", async () => { 10 | gun.get("gGcdtb").put({ bar: "baz" }); 11 | 12 | const { next } = graphqlGun( 13 | gql` 14 | { 15 | gGcdtb { 16 | bar 17 | } 18 | } 19 | `, 20 | gun 21 | ); 22 | await next(); 23 | expect(await next()).toMatchSnapshot(); 24 | }); 25 | 26 | it("lets you grab the chain at any point", async () => { 27 | gun.get("gGlygtcaap").put({ bar: "pop" }); 28 | 29 | const results = await graphqlGun( 30 | gql` 31 | { 32 | gGlygtcaap { 33 | bar { 34 | _chain 35 | hello 36 | } 37 | } 38 | } 39 | `, 40 | gun 41 | ); 42 | 43 | expect(results.gGlygtcaap.bar._chain).not.toBeUndefined() 44 | expect(results.gGlygtcaap.bar._chain).toEqual(gun.get("gGlygtcaap").get("bar")) 45 | 46 | await new Promise(resolve => { 47 | results.gGlygtcaap.bar._chain.on( 48 | (value, key) => { 49 | expect(key).toEqual("bar"); 50 | expect(value).toEqual("pop"); 51 | resolve(); 52 | }, 53 | { changed: true } 54 | ); 55 | 56 | gun 57 | .get("gGlygtcaap") 58 | .get("bar") 59 | .put({ some: "stuff" }); 60 | }); 61 | }); 62 | 63 | it("iterates over sets", async () => { 64 | const chain = gun.get("gGios"); 65 | await new Promise(resolve => { 66 | const thing1 = chain.get("thing1"); 67 | thing1.put({ stuff: "b", more: "ok" }); 68 | chain.get("things").set(thing1, resolve); 69 | }); 70 | await new Promise(resolve => { 71 | const thing2 = chain.get("thing2"); 72 | thing2.put({ stuff: "c", more: "ok" }); 73 | chain.get("things").set(thing2, resolve); 74 | }); 75 | 76 | const { next } = graphqlGun( 77 | gql` 78 | { 79 | gGios { 80 | things(type: Set) { 81 | stuff 82 | } 83 | } 84 | } 85 | `, 86 | gun 87 | ); 88 | 89 | await next(); 90 | 91 | expect(await next()).toEqual({ 92 | gGios: { things: [{ stuff: "b" }, { stuff: "c" }] } 93 | }); 94 | }); 95 | 96 | it("lets you subscribe to updates", async () => { 97 | const chain = gun.get("gGlystu"); 98 | const thing1 = chain.get("thing1"); 99 | const thing2 = chain.get("thing2"); 100 | thing1.put({ stuff: "b", more: "ok" }); 101 | thing2.put({ stuff: "c", more: "ok" }); 102 | chain.get("things").set(thing1); 103 | chain.get("things").set(thing2); 104 | 105 | let { next } = graphqlGun( 106 | gql` 107 | { 108 | gGlystu { 109 | things(type: Set) { 110 | stuff @live 111 | } 112 | } 113 | } 114 | `, 115 | gun 116 | ); 117 | 118 | expect(await next()).toEqual({ 119 | gGlystu: { things: [{ stuff: "b" }, { stuff: "c" }] } 120 | }); 121 | 122 | chain.get("thing1").put({ stuff: "changed" }); 123 | 124 | expect(await next()).toEqual({ 125 | gGlystu: { 126 | things: [{ stuff: "changed" }, { stuff: "c" }] 127 | } 128 | }); 129 | }); 130 | 131 | it("supports mad nesting", async () => { 132 | const chain = gun.get("Ggsmn"); 133 | const thing1 = chain.get("thing1"); 134 | const thing2 = chain.get("thing2"); 135 | const moreThings1 = chain.get("moreThing1"); 136 | const moreThings2 = chain.get("moreThing2"); 137 | const moreThings3 = chain.get("moreThing3"); 138 | const moreThings4 = chain.get("moreThing4"); 139 | moreThings1.put({ otherStuff: "one fish" }); 140 | moreThings2.put({ otherStuff: "two fish" }); 141 | moreThings3.put({ otherStuff: "red fish" }); 142 | moreThings4.put({ otherStuff: "blue fish" }); 143 | thing1.put({ stuff: "b", more: "ok" }); 144 | thing2.put({ stuff: "c", more: "ok" }); 145 | thing1.get("moreThings").set(moreThings1); 146 | thing1.get("moreThings").set(moreThings2); 147 | thing2.get("moreThings").set(moreThings3); 148 | thing2.get("moreThings").set(moreThings4); 149 | chain.get("things").set(thing1); 150 | chain.get("things").set(thing2); 151 | 152 | let { next } = graphqlGun( 153 | gql` 154 | { 155 | things(type: Set) { 156 | stuff 157 | moreThings(type: Set) { 158 | otherStuff 159 | } 160 | } 161 | } 162 | `, 163 | chain 164 | ); 165 | 166 | expect(await next()).toEqual({ 167 | things: [ 168 | { 169 | stuff: "b", 170 | moreThings: [{ otherStuff: "one fish" }, { otherStuff: "two fish" }] 171 | }, 172 | { 173 | stuff: "c", 174 | moreThings: [{ otherStuff: "red fish" }, { otherStuff: "blue fish" }] 175 | } 176 | ] 177 | }); 178 | }); 179 | 180 | it("supports mad nesting with subscriptions", async () => { 181 | const chain = gun.get("gGsmnws"); 182 | const thing1 = chain.get("thing1"); 183 | const thing2 = chain.get("thing2"); 184 | const moreThings1 = chain.get("moreThing1"); 185 | const moreThings2 = chain.get("moreThing2"); 186 | const moreThings3 = chain.get("moreThing3"); 187 | const moreThings4 = chain.get("moreThing4"); 188 | moreThings1.put({ otherStuff: "one fish" }); 189 | moreThings2.put({ otherStuff: "two fish" }); 190 | moreThings3.put({ otherStuff: "red fish" }); 191 | moreThings4.put({ otherStuff: "blue fish" }); 192 | thing1.put({ stuff: "b", more: "ok" }); 193 | thing2.put({ stuff: "c", more: "ok" }); 194 | thing1.get("moreThings").set(moreThings1); 195 | thing1.get("moreThings").set(moreThings2); 196 | thing2.get("moreThings").set(moreThings3); 197 | thing2.get("moreThings").set(moreThings4); 198 | chain.get("things").set(thing1); 199 | chain.get("things").set(thing2); 200 | 201 | let { next } = graphqlGun( 202 | gql` 203 | { 204 | things(type: Set) @live { 205 | stuff 206 | moreThings(type: Set) { 207 | otherStuff 208 | } 209 | } 210 | } 211 | `, 212 | chain 213 | ); 214 | 215 | expect(await next()).toEqual({ 216 | things: [ 217 | { 218 | stuff: "b", 219 | moreThings: [{ otherStuff: "one fish" }, { otherStuff: "two fish" }] 220 | }, 221 | { 222 | stuff: "c", 223 | moreThings: [{ otherStuff: "red fish" }, { otherStuff: "blue fish" }] 224 | } 225 | ] 226 | }); 227 | 228 | const thing3 = chain.get("thing3"); 229 | thing3.put({ stuff: "d", more: "just added" }); 230 | thing2.put({ stuff: "cc" }); 231 | chain.get("things").set(thing3); 232 | const thing4 = chain.get("thing4"); 233 | thing4.put({ stuff: "e" }); 234 | chain.get("things").set(thing4); 235 | 236 | expect(await next()).toEqual({ 237 | things: [ 238 | { 239 | stuff: "b", 240 | moreThings: [{ otherStuff: "one fish" }, { otherStuff: "two fish" }] 241 | }, 242 | { 243 | stuff: "cc", 244 | moreThings: [{ otherStuff: "red fish" }, { otherStuff: "blue fish" }] 245 | }, 246 | { 247 | moreThings: [], 248 | stuff: "d" 249 | }, 250 | { 251 | moreThings: [], 252 | stuff: "e" 253 | } 254 | ] 255 | }); 256 | }); 257 | 258 | it("works with a simple case of two properties and a promise interface", async () => { 259 | const thing = gun.get("thing"); 260 | const fish = thing.get("fish"); 261 | fish.put({ color: "red", fins: 2 }); 262 | 263 | const result = await graphqlGun( 264 | gql` 265 | { 266 | thing { 267 | fish { 268 | color 269 | fins 270 | } 271 | } 272 | } 273 | `, 274 | gun 275 | ); 276 | 277 | expect(result).toEqual({ 278 | thing: { 279 | fish: { 280 | color: "red", 281 | fins: 2 282 | } 283 | } 284 | }); 285 | }); 286 | }); 287 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-gun", 3 | "version": "1.0.2", 4 | "description": "A graphql API for the gun p2p graph database", 5 | "main": "index.js", 6 | "repository": "", 7 | "author": "Bryan Goldstein ", 8 | "license": "MIT", 9 | "scripts": { 10 | "test": "cross-env CI=1 jest", 11 | "test:watch": "jest --watch" 12 | }, 13 | "dependencies": { 14 | "@babel/runtime": "^7.11.2", 15 | "graphql-anywhere": "github:brysgo/graphql-anywhere#graphql-gun", 16 | "gun": "^0.2020.520", 17 | "try-get": "^1.0.0" 18 | }, 19 | "devDependencies": { 20 | "@babel/core": "^7.11.1", 21 | "@babel/plugin-transform-runtime": "^7.11.0", 22 | "@babel/preset-env": "^7.11.0", 23 | "cross-env": "^7.0.2", 24 | "graphql": "^15.3.0", 25 | "graphql-tag": "^2.11.0", 26 | "jest": "^26.3.0", 27 | "prettier": "^2.0.5", 28 | "react": "^16.13.1", 29 | "react-test-renderer": "^16.13.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /react/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`react createContainer reloads your component when new data comes 1`] = ` 4 |
11 | { 12 | "palette": { 13 | "rcCrycwndc": { 14 | "thing": { 15 | "stuff": { 16 | "one": "hello", 17 | "two": "world" 18 | } 19 | } 20 | } 21 | }, 22 | "color": "red" 23 | } 24 |
25 | `; 26 | 27 | exports[`react createContainer reloads your component when new data comes 2`] = ` 28 |
35 | { 36 | "palette": { 37 | "rcCrycwndc": { 38 | "thing": { 39 | "stuff": { 40 | "one": "new", 41 | "two": "world" 42 | } 43 | } 44 | } 45 | }, 46 | "color": "red" 47 | } 48 |
49 | `; 50 | 51 | exports[`react createContainer renders the fragment in the container 1`] = ` 52 |
59 | { 60 | "palette": { 61 | "rcCrtfitc": { 62 | "color": { 63 | "hue": 255, 64 | "saturation": 255, 65 | "value": 255 66 | } 67 | } 68 | }, 69 | "color": "blue" 70 | } 71 |
72 | `; 73 | 74 | exports[`react graphqlGun provides apollo client syntax for container 1`] = ` 75 |
82 | { 83 | "data": { 84 | "color": { 85 | "hue": 255, 86 | "saturation": 255, 87 | "value": 255 88 | } 89 | }, 90 | "color": "blue" 91 | } 92 |
93 | `; 94 | -------------------------------------------------------------------------------- /react/index.js: -------------------------------------------------------------------------------- 1 | const graphqlGunUtil = require("../"); 2 | const tryGet = require("try-get"); 3 | 4 | module.exports = function({ React, gun }) { 5 | function createContainer(Component, { fragments }) { 6 | class Container extends React.Component { 7 | constructor() { 8 | super(); 9 | this.state = {}; 10 | this.resetPromise(); 11 | } 12 | 13 | resetPromise() { 14 | let resolve; 15 | this.promise = new Promise(res => resolve = res); 16 | if (this.resolve) this.resolve(() => this.promise); 17 | this.resolve = resolve; 18 | } 19 | 20 | reloadFragmentOnResolve(fragment, asyncIterator) { 21 | return asyncIterator.next().then(iter => { 22 | this.setState({ data: { [fragment]: iter.value } }); 23 | this.resetPromise(); // for testing 24 | this.reloadFragmentOnResolve(fragment, asyncIterator); 25 | }); 26 | } 27 | 28 | componentDidMount() { 29 | return Object.keys(fragments).map(fragment => { 30 | const asyncIterator = graphqlGunUtil(fragments[fragment], gun)[ 31 | Symbol.iterator 32 | ](); 33 | this.reloadFragmentOnResolve(fragment, asyncIterator); 34 | return asyncIterator; 35 | }); 36 | } 37 | 38 | render() { 39 | return React.createElement( 40 | Component, 41 | Object.assign({}, this.state.data, this.props) 42 | ); 43 | } 44 | } 45 | Container.displayName = `${Component.displayName}DataContainer`; 46 | return Container; 47 | } 48 | 49 | function graphqlGun(data) { 50 | return function wrap(Component) { 51 | return createContainer(Component, { fragments: { data } }); 52 | }; 53 | } 54 | 55 | return { 56 | createContainer, 57 | graphqlGun 58 | }; 59 | }; 60 | -------------------------------------------------------------------------------- /react/index.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, expect */ 2 | const gql = require("graphql-tag"); 3 | const Gun = require("gun/gun"); 4 | const React = require("react"); 5 | const gun = Gun(); 6 | const { createContainer, graphqlGun } = require("./")({ React, gun }); 7 | 8 | const renderer = require("react-test-renderer"); 9 | 10 | const Color = props => { 11 | return React.createElement( 12 | "div", 13 | { style: { color: props.color } }, 14 | JSON.stringify(props, null, 2) 15 | ); 16 | }; 17 | 18 | describe("react", () => { 19 | describe("createContainer", () => { 20 | it("renders the fragment in the container", async () => { 21 | const chain = gun.get("rcCrtfitc"); 22 | chain 23 | .get("color") 24 | .put({ hue: 255, saturation: 255, value: 255, other: "foo" }); 25 | 26 | const ColorContainer = createContainer(Color, { 27 | fragments: { 28 | palette: gql`{ 29 | rcCrtfitc { 30 | color { 31 | hue 32 | saturation 33 | value 34 | } 35 | } 36 | }` 37 | } 38 | }); 39 | 40 | let containerInstance; 41 | const colorContainerElement = React.createElement(ColorContainer, { 42 | color: "blue", 43 | ref: container => containerInstance = container 44 | }); 45 | const component = renderer.create(colorContainerElement); 46 | 47 | await containerInstance.promise; 48 | await containerInstance.promise; 49 | 50 | let tree = component.toJSON(); 51 | expect(tree).toMatchSnapshot(); 52 | }); 53 | 54 | it("reloads your component when new data comes", async () => { 55 | const chain = gun.get("rcCrycwndc"); 56 | chain.get("thing").put({ 57 | stuff: { 58 | one: "hello", 59 | two: "world" 60 | } 61 | }); 62 | 63 | const ColorContainer = createContainer(Color, { 64 | fragments: { 65 | palette: gql`{ 66 | rcCrycwndc { 67 | thing { 68 | stuff @live { 69 | one 70 | two 71 | } 72 | } 73 | } 74 | }` 75 | } 76 | }); 77 | 78 | let containerInstance; 79 | const colorContainerElement = React.createElement(ColorContainer, { 80 | color: "red", 81 | ref: container => containerInstance = container 82 | }); 83 | const component = renderer.create(colorContainerElement); 84 | 85 | const next = await containerInstance.promise; 86 | 87 | let tree = component.toJSON(); 88 | expect(tree).toMatchSnapshot(); 89 | 90 | chain.get("thing").put({ stuff: { one: "new" } }); 91 | 92 | await next(); 93 | 94 | tree = component.toJSON(); 95 | expect(tree).toMatchSnapshot(); 96 | }); 97 | }); 98 | 99 | describe("graphqlGun", () => { 100 | it("provides apollo client syntax for container", async () => { 101 | gun 102 | .get("color") 103 | .put({ hue: 255, saturation: 255, value: 255, other: "foo" }); 104 | 105 | const ColorContainer = graphqlGun( 106 | gql`{ 107 | color { 108 | hue 109 | saturation 110 | value 111 | } 112 | }` 113 | )(Color); 114 | 115 | let containerInstance; 116 | const colorContainerElement = React.createElement(ColorContainer, { 117 | color: "blue", 118 | ref: container => containerInstance = container 119 | }); 120 | const component = renderer.create(colorContainerElement); 121 | 122 | await containerInstance.promise; 123 | 124 | let tree = component.toJSON(); 125 | expect(tree).toMatchSnapshot(); 126 | }); 127 | }); 128 | }); 129 | --------------------------------------------------------------------------------