├── .gitignore
├── .npmignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── package.json
├── src
├── ReactiveQuery.js
├── __tests__
│ └── reduceStore.ts
├── defs.ts
├── index.ts
└── reduceStore.ts
├── tsconfig.json
└── tslint.json
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .vscode
3 | coverage
4 | dist
5 | node_modules
6 | npm-debug.log
7 | package-lock.json
8 | typings
9 | yarn-error.log
10 | yarn.lock
11 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src
2 | node_modules
3 | typings
4 | tsconfig.json
5 | typings.json
6 | tslint.json
7 | dist/test
8 | yarn.lock
9 | coverage
10 | .vscode
11 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "8"
4 | - "6"
5 | - "4"
6 | install:
7 | - npm install -g coveralls
8 | - npm install
9 |
10 | script:
11 | - npm test
12 | - npm run coverage
13 | - coveralls < ./coverage/lcov.info || true # ignore coveralls error
14 |
15 | # Allow Travis tests to run in containers.
16 | # sudo: false
17 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ### 0.1.1
4 |
5 | * Add support for Render Prop Function
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 Theodor Diaconu
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 all
13 | 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 THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Apollo Live Client
2 |
3 | This project is sponsored by [Cult of Coders](https://www.cultofcoders.com)
4 |
5 | This package provides an easy way to work with [`apollo-live-server`](https://github.com/cult-of-coders/apollo-live-server) subscriptions.
6 |
7 | ## Usage
8 |
9 | ```js
10 | import gql from 'graphql-tag';
11 | import { ReactiveQuery } from 'apollo-live-client';
12 |
13 | const GET_MESSAGES = gql`
14 | query {
15 | messages(threadId: String) {
16 | _id
17 | text
18 | createdAt
19 | }
20 | }
21 | `;
22 |
23 | const SUBSCRIBE_MESSAGES = gql`
24 | subscription {
25 | messages(threadId: String) {
26 | event
27 | doc {
28 | _id
29 | text
30 | createdAt
31 | }
32 | }
33 | }
34 | `;
35 | ```
36 |
37 | For this system to work, the subscription root (`messages`) needs to be the same as the query . And you can query for more data from other fields in the RootQuery.
38 |
39 | ```js
40 | const MessagesWithData = () => (
41 |
46 | {({ data: { notifications }, loading, error }) => {
47 | if (loading) return ;
48 | if (error) return ;
49 |
50 | return ;
51 | }}
52 |
53 | );
54 | ```
55 |
56 | Any other prop passed to `ReactiveQuery` that is not `subscription` is going to be passed to the actual `Query` object behind the scenes.
57 |
58 | ## Customisability
59 |
60 | You can customise the behavior of how you handle incomming data from subscriptions, by rolling out your
61 | own `subscribeToMore` and custom `updateQuery` method.
62 |
63 | Read more about this here:
64 | https://www.apollographql.com/docs/react/advanced/subscriptions.html
65 |
66 | ```js
67 | import { reduceStore } from 'apollo-live-client';
68 |
69 | // And in your updateQuery handler of subscribeToMore:
70 | subscribeToMore({
71 | document: SUBSCRIBE_MESSAGES,
72 | variables: {},
73 | updateQuery: (prev, {subscriptionData}) {
74 | const reactiveEvent = subscriptionData.data.notifications;
75 | return Object.assign({}, prev, {
76 | notifications: reduceStore(reactiveEvent, prev.notifications)
77 | })
78 | },
79 | })
80 | ```
81 |
82 | ## Bare-bones subscription
83 |
84 | To provide you with even more flexibility, you can just roll your own subscription handler and reducer:
85 |
86 | ```js
87 | // client = Apollo Client
88 | const observable = client.subscribe({ query: SUBSCRIBE_NEWSFEED });
89 | const subscription = observable.subscribe({
90 | next({ data }) {
91 | // data is your payload
92 | // do something with it
93 | },
94 | });
95 | subscription.unsubscribe();
96 | ```
97 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "apollo-live-client",
3 | "version": "0.2.1",
4 | "description": "Handles reactive events to easily work with Live Queries",
5 | "main": "dist/index.js",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/cult-of-coders/apollo-live-client.git"
9 | },
10 | "scripts": {
11 | "clean": "rimraf dist coverage",
12 | "compile": "tsc",
13 | "pretest": "npm run compile",
14 | "test": "npm run testonly --",
15 | "posttest": "npm run lint",
16 | "lint": "tslint --type-check --project ./tsconfig.json ./src/**/*",
17 | "watch": "tsc -w",
18 | "testonly": "mocha --reporter spec --full-trace ./dist/__tests__/*.js",
19 | "testonly-watch": "mocha --reporter spec --full-trace ./dist/__tests__/*.js --watch",
20 | "coverage": "node ./node_modules/istanbul/lib/cli.js cover _mocha -- --full-trace ./dist/__tests__/*.js",
21 | "postcoverage": "remap-istanbul --input coverage/coverage.raw.json --type lcovonly --output coverage/lcov.info",
22 | "prepublishOnly": "npm run clean && npm run compile"
23 | },
24 | "peerDependencies": {
25 | "react": "16.x",
26 | "react-apollo": "2.x",
27 | "prop-types": "15.x"
28 | },
29 | "devDependencies": {
30 | "@types/graphql": "^0.11.3",
31 | "@types/mocha": "^2.2.39",
32 | "@types/node": "^8.0.28",
33 | "@types/react": "16.3.5",
34 | "chai": "^4.1.2",
35 | "chai-as-promised": "^7.1.1",
36 | "graphql": "^0.13.0",
37 | "istanbul": "^1.0.0-alpha.2",
38 | "mocha": "^6.1.1",
39 | "remap-istanbul": "^0.9.1",
40 | "rimraf": "^2.6.2",
41 | "sinon": "^7.3.1",
42 | "sinon-chai": "^2.9.0",
43 | "tslint": "^5.2.0",
44 | "typescript": "^3.4.2"
45 | },
46 | "typings": "dist/index.d.ts",
47 | "typescript": {
48 | "definition": "dist/index.d.ts"
49 | },
50 | "license": "MIT"
51 | }
52 |
--------------------------------------------------------------------------------
/src/ReactiveQuery.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as PropTypes from 'prop-types';
3 | import { reduceStore } from './reduceStore';
4 | import { Query } from 'react-apollo';
5 |
6 | export default class ReactiveQuery extends React.Component {
7 | static propTypes = {
8 | children: PropTypes.func.isRequired,
9 | query: PropTypes.object.isRequired,
10 | subscription: PropTypes.object.isRequired,
11 | };
12 |
13 | render() {
14 | const { children, subscription, query, variables, ...rest } = this.props;
15 |
16 | return (
17 |
18 | {props => {
19 | return (
20 |
25 | {() => {
26 | return children(props);
27 | }}
28 |
29 | );
30 | }}
31 |
32 | );
33 | }
34 | }
35 |
36 | class Subscription extends React.Component {
37 | componentDidMount = () => {
38 | const { subscribeToMore, subscription, variables } = this.props;
39 |
40 | subscribeToMore({
41 | document: subscription,
42 | variables: variables,
43 | updateQuery: (prev, { subscriptionData }) => {
44 | if (!subscriptionData.data) return prev;
45 |
46 | const storeName = Object.keys(subscriptionData.data)[0];
47 |
48 | const newStore = Object.assign({}, prev, {
49 | [storeName]: reduceStore(
50 | subscriptionData.data[storeName],
51 | prev[storeName]
52 | ),
53 | });
54 |
55 | return newStore;
56 | },
57 | });
58 | };
59 |
60 | render() {
61 | const { children, ...rest } = this.props;
62 |
63 | return children(rest);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/__tests__/reduceStore.ts:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai';
2 | import { reduceStore } from '../reduceStore';
3 | import { ReactiveEvent, StoreObject, Event } from '../defs';
4 |
5 | describe('reduceStore', function() {
6 | it('Should run an update correctly', function() {
7 | const newStore = reduceStore(
8 | {
9 | event: Event.CHANGED,
10 | doc: {
11 | _id: '123',
12 | __typename: 'update',
13 | newField: 'new',
14 | },
15 | },
16 | [
17 | {
18 | _id: '123',
19 | __typename: 'current',
20 | title: 'abc',
21 | },
22 | {
23 | _id: '124',
24 | __typename: 'current',
25 | title: 'abcd',
26 | },
27 | ]
28 | );
29 |
30 | assert.lengthOf(newStore, 2);
31 | assert.isObject(newStore[0]);
32 | assert.isDefined(newStore[0].title);
33 | assert.isDefined(newStore[0].newField);
34 | // assert.equal('current', newStore[0].__typename);
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/src/defs.ts:
--------------------------------------------------------------------------------
1 | export enum Event {
2 | ADDED = 'added',
3 | CHANGED = 'changed',
4 | REMOVED = 'removed',
5 | }
6 |
7 | export type ReactiveEvent = {
8 | event: Event;
9 | doc: StoreObject;
10 | };
11 |
12 | export interface StoreObject {
13 | _id: string | number;
14 | __typename: string;
15 | [key: string]: any;
16 | }
17 |
18 | export type SubscribeMoreParam = {
19 | type: string;
20 | value: any;
21 | };
22 |
23 | export interface SubscribeMoreConfig {
24 | name: string;
25 | params: {
26 | [key: string]: SubscribeMoreParam;
27 | };
28 | }
29 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { reduceStore } from './reduceStore';
2 | export { default as ReactiveQuery } from './ReactiveQuery';
3 |
--------------------------------------------------------------------------------
/src/reduceStore.ts:
--------------------------------------------------------------------------------
1 | import { ReactiveEvent, StoreObject, Event } from './defs';
2 |
3 | export function reduceStore(
4 | reactiveEvent: ReactiveEvent,
5 | store: StoreObject[] | StoreObject
6 | ) {
7 | if (store instanceof Array) {
8 | return reduceStoreArray(reactiveEvent, store);
9 | }
10 | return reduceStoreObject(reactiveEvent, store);
11 | }
12 |
13 | export function reduceStoreObject(
14 | reactiveEvent: ReactiveEvent,
15 | store: StoreObject
16 | ) {
17 | const { event, doc } = reactiveEvent;
18 | const { __typename, ...rest } = doc;
19 |
20 | if (event === Event.ADDED) {
21 | // check if it exists
22 |
23 | if (store) {
24 | return Object.assign({}, store, rest);
25 | } else {
26 | return doc;
27 | }
28 | }
29 |
30 | if (event === Event.CHANGED) {
31 | return Object.assign({}, store || {}, rest);
32 | }
33 |
34 | if (event === Event.REMOVED) {
35 | return null;
36 | }
37 | }
38 |
39 | export function reduceStoreArray(
40 | reactiveEvent: ReactiveEvent,
41 | store: StoreObject[]
42 | ): StoreObject[] {
43 | const { event, doc } = reactiveEvent;
44 |
45 | if (!doc._id) {
46 | throw new Error(
47 | 'The document does not have _id set, is it present in the subscription?'
48 | );
49 | }
50 |
51 | if (event === Event.ADDED) {
52 | // check if it exists
53 | const { idx, found } = findIndexInStore(store, doc._id);
54 |
55 | if (found) {
56 | return [...store.slice(0, idx), found, ...store.slice(idx + 1)];
57 | }
58 |
59 | return [doc, ...store];
60 | }
61 |
62 | if (event === Event.CHANGED) {
63 | const { idx, found } = findIndexInStore(store, doc._id);
64 |
65 | if (!found) {
66 | return store;
67 | }
68 |
69 | const newFound = Object.assign({}, found, doc);
70 | return [...store.slice(0, idx), newFound, ...store.slice(idx + 1)];
71 | }
72 |
73 | if (event === Event.REMOVED) {
74 | return store.filter(item => item._id !== doc._id);
75 | }
76 | }
77 |
78 | /**
79 | * @param store
80 | * @param _id
81 | */
82 | function findIndexInStore(store, _id) {
83 | let foundIdx;
84 | const found = store.find((item, idx) => {
85 | if (item._id === _id) {
86 | foundIdx = idx;
87 | return true;
88 | }
89 | });
90 |
91 | return { idx: foundIdx, found };
92 | }
93 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "allowJs": true,
5 | "module": "commonjs",
6 | "moduleResolution": "node",
7 | "sourceMap": true,
8 | "lib": ["es6", "dom", "esnext"],
9 | "noImplicitAny": false,
10 | "rootDir": "./src",
11 | "outDir": "./dist",
12 | "allowSyntheticDefaultImports": true,
13 | "pretty": true,
14 | "jsx": "react",
15 | "removeComments": true,
16 | "typeRoots": ["node_modules/@types"]
17 | },
18 |
19 | "include": ["**/*.ts", "**/*.tsx", "**/*.jsx"],
20 | "exclude": ["node_modules", "dist"]
21 | }
22 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "align": [
4 | false,
5 | "parameters",
6 | "arguments",
7 | "statements"
8 | ],
9 | "ban": false,
10 | "class-name": true,
11 | "curly": true,
12 | "eofline": true,
13 | "forin": true,
14 | "indent": [
15 | true,
16 | "spaces"
17 | ],
18 | "interface-name": false,
19 | "jsdoc-format": true,
20 | "label-position": true,
21 | "max-line-length": [
22 | true,
23 | 140
24 | ],
25 | "member-access": true,
26 | "member-ordering": [
27 | true,
28 | "public-before-private",
29 | "static-before-instance",
30 | "variables-before-functions"
31 | ],
32 | "no-any": false,
33 | "no-arg": true,
34 | "no-bitwise": true,
35 | "no-conditional-assignment": true,
36 | "no-consecutive-blank-lines": false,
37 | "no-console": [
38 | true,
39 | "log",
40 | "debug",
41 | "info",
42 | "time",
43 | "timeEnd",
44 | "trace"
45 | ],
46 | "no-construct": true,
47 | "no-debugger": true,
48 | "no-duplicate-variable": true,
49 | "no-empty": true,
50 | "no-eval": true,
51 | "no-inferrable-types": false,
52 | "no-internal-module": true,
53 | "no-null-keyword": false,
54 | "no-require-imports": false,
55 | "no-shadowed-variable": true,
56 | "no-switch-case-fall-through": true,
57 | "no-trailing-whitespace": true,
58 | "no-unused-expression": true,
59 | "no-unused-variable": true,
60 | "no-use-before-declare": true,
61 | "no-var-keyword": true,
62 | "no-var-requires": true,
63 | "object-literal-sort-keys": false,
64 | "one-line": [
65 | true,
66 | "check-open-brace",
67 | "check-catch",
68 | "check-else",
69 | "check-finally",
70 | "check-whitespace"
71 | ],
72 | "quotemark": [
73 | true,
74 | "single",
75 | "avoid-escape"
76 | ],
77 | "radix": true,
78 | "semicolon": [
79 | true,
80 | "always"
81 | ],
82 | "switch-default": true,
83 | "trailing-comma": [
84 | true,
85 | {
86 | "multiline": "always",
87 | "singleline": "never"
88 | }
89 | ],
90 | "triple-equals": [
91 | true,
92 | "allow-null-check"
93 | ],
94 | "typedef": [
95 | false,
96 | "call-signature",
97 | "parameter",
98 | "arrow-parameter",
99 | "property-declaration",
100 | "variable-declaration",
101 | "member-variable-declaration"
102 | ],
103 | "typedef-whitespace": [
104 | true,
105 | {
106 | "call-signature": "nospace",
107 | "index-signature": "nospace",
108 | "parameter": "nospace",
109 | "property-declaration": "nospace",
110 | "variable-declaration": "nospace"
111 | },
112 | {
113 | "call-signature": "space",
114 | "index-signature": "space",
115 | "parameter": "space",
116 | "property-declaration": "space",
117 | "variable-declaration": "space"
118 | }
119 | ],
120 | "variable-name": [
121 | true,
122 | "check-format",
123 | "allow-leading-underscore",
124 | "ban-keywords",
125 | "allow-pascal-case"
126 | ],
127 | "whitespace": [
128 | true,
129 | "check-branch",
130 | "check-decl",
131 | "check-operator",
132 | "check-separator",
133 | "check-type"
134 | ]
135 | }
136 | }
--------------------------------------------------------------------------------