├── testDB
├── LOCK
├── CURRENT
├── LOG
├── 000003.log
└── MANIFEST-000002
├── .babelrc
├── .DS_Store
├── .gitignore
├── .npmignore
├── src
├── index.js
├── utils
│ ├── metaDBUtils.js
│ ├── fetchUtils.js
│ ├── configUtils.js
│ ├── 64to8.js
│ ├── urls.js
│ ├── cushionWorkerUtils.js
│ └── swUtils.js
├── cushion.js
├── cushion_worker
│ ├── cushionWorker.js
│ └── cushionWorkerIndex.js
├── metaDB.js
├── store.js
├── databaseAuth.js
└── account.js
├── index.html
├── .defaultCushionConfig.json
├── sw.js
├── tests
├── account.test.js
└── store.test.js
├── package.json
├── webpack.config.js
└── README.md
/testDB/LOCK:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/testDB/CURRENT:
--------------------------------------------------------------------------------
1 | MANIFEST-000002
2 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/testDB/LOG:
--------------------------------------------------------------------------------
1 | 2019/08/15-09:23:59.123026 7fd26d7fa700 Delete type=3 #1
2 |
--------------------------------------------------------------------------------
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CushionDB/CushionClient/HEAD/.DS_Store
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 | cushionDB*/
4 | cushionMeta/
5 | testDB/
--------------------------------------------------------------------------------
/testDB/000003.log:
--------------------------------------------------------------------------------
1 | TN ÿmeta-storeÿ_local_uuid&"b031f368-de96-40ef-aeff-fcfc624d3a7d"
--------------------------------------------------------------------------------
/testDB/MANIFEST-000002:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CushionDB/CushionClient/HEAD/testDB/MANIFEST-000002
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src/
2 | cushionDB/
3 | cushionMeta/
4 | webpack.config.js
5 | tests/
6 | testDB/
7 | .gitignore
8 | .babelrc
9 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import Cushion from './cushion';
2 |
3 | export default Cushion;
4 |
5 | // global.cushion = new Cushion();
6 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/utils/metaDBUtils.js:
--------------------------------------------------------------------------------
1 | export const getDefaultMetaDBDoc = (localDBName, remoteDBAddress, username, subscribedToPush) => {
2 | return {
3 | _id: 'cushionMeta',
4 | localDBName,
5 | remoteDBAddress,
6 | username,
7 | subscribedToPush
8 | };
9 | }
10 |
--------------------------------------------------------------------------------
/.defaultCushionConfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "couchBaseURL": "http://localhost:5984",
3 | "cushionServerBaseURL": "http://localhost:3001",
4 | "publicVapid": "BP_QrGDbKkoFatPv0iiMEfl5XINAZSknNz171ID_uNXmmA5_bRnmGgt7zqfyDOgzwptrG9w0lqEU94ru34teLQU",
5 | "appname": "CushionDB",
6 | "appPushIcons": false
7 | }
--------------------------------------------------------------------------------
/src/utils/fetchUtils.js:
--------------------------------------------------------------------------------
1 | export const getFetchOpts = ({ method, data }) => {
2 | let fetchOpts = {
3 | method,
4 | headers: {
5 | 'Content-Type': 'application/json',
6 | Accept: 'application/json',
7 | }
8 | }
9 |
10 | return data ? { ...fetchOpts, body: JSON.stringify(data) } : fetchOpts;
11 | }
12 |
--------------------------------------------------------------------------------
/src/utils/configUtils.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const rootDir = path.dirname(require.main.filename);
3 | let configObj = require('../../.defaultCushionConfig.json');
4 |
5 | export const getConfigObj = () => {
6 | let userConfig;
7 |
8 | try {
9 | userConfig = require (rootDir + 'cushionConfig.json');
10 | } catch {
11 | userConfig = undefined;
12 | }
13 |
14 | return userConfig ? {...configObj, ...userConfig} : configObj;
15 | }
16 |
--------------------------------------------------------------------------------
/src/utils/64to8.js:
--------------------------------------------------------------------------------
1 | const urlB64ToUint8Array = (base64String) => {
2 | const padding = '='.repeat((4 - base64String.length % 4) % 4);
3 | const base64 = (base64String + padding)
4 | .replace(/\-/g, '+')
5 | .replace(/_/g, '/');
6 |
7 | const rawData = window.atob(base64);
8 | const outputArray = new Uint8Array(rawData.length);
9 |
10 | for (let i = 0; i < rawData.length; ++i) {
11 | outputArray[i] = rawData.charCodeAt(i);
12 | }
13 | return outputArray;
14 | }
15 |
16 | export default urlB64ToUint8Array;
17 |
--------------------------------------------------------------------------------
/sw.js:
--------------------------------------------------------------------------------
1 | try {
2 | importScripts('node_modules/cushiondb-client/dist/cushionWorker.js');
3 | } catch {
4 | importScripts('assets/cushionWorker.js');
5 | }
6 |
7 | self.addEventListener('sync', evt => {
8 | evt.waitUntil(cushionWorker.syncEventTriggered(evt));
9 | });
10 |
11 | self.addEventListener('message', evt => {
12 | evt.waitUntil(cushionWorker.messageEventTriggered(evt));
13 | });
14 |
15 | self.addEventListener('push', evt => {
16 | console.log('pusheventTriggered');
17 | evt.waitUntil(cushionWorker.pushEventTriggered(evt));
18 | });
19 |
--------------------------------------------------------------------------------
/src/utils/urls.js:
--------------------------------------------------------------------------------
1 | import { getConfigObj } from './configUtils';
2 |
3 | const configObj = getConfigObj();
4 |
5 | export const subscribeDeviceToPush = () => `${configObj.cushionServerBaseURL}/subscribe_device_to_notifications`;
6 | export const signup = () => `${configObj.cushionServerBaseURL}/signup`;
7 | export const isSubscribedToPush = (username) => `${configObj.cushionServerBaseURL}/is_subscribed_to_push/${username}`;
8 | export const changePassword = () => `${configObj.cushionServerBaseURL}/updatePassword`;
9 | export const triggerUpdateDevices = () => `${configObj.cushionServerBaseURL}/trigger_update_user_devices`;
10 |
--------------------------------------------------------------------------------
/src/cushion.js:
--------------------------------------------------------------------------------
1 | import Store from './store';
2 | import Account from './account';
3 | import DatabaseAuth from './databaseAuth';
4 |
5 | import { registerServiceWorker } from './utils/swUtils';
6 | import { getConfigObj } from './utils/configUtils';
7 |
8 | const TESTING = process.env.NODE_ENV === 'testing';
9 | const CONFIG = getConfigObj();
10 |
11 | class Cushion {
12 | constructor() {
13 | if (!TESTING) registerServiceWorker();
14 |
15 | const dbAuth = new DatabaseAuth(CONFIG.couchBaseURL);
16 |
17 | this.ready = dbAuth.ready;
18 |
19 | dbAuth.ready.then(() => {
20 | this.store = new Store(dbAuth);
21 | this.account = new Account(dbAuth);
22 | });
23 | }
24 | };
25 |
26 | export default Cushion;
27 |
--------------------------------------------------------------------------------
/src/utils/cushionWorkerUtils.js:
--------------------------------------------------------------------------------
1 | import PouchDB from 'pouchdb';
2 |
3 | export const pouchSync = (fromDB, toDB) => {
4 | return new PouchDB(fromDB).replicate.to(toDB);
5 | }
6 |
7 | export const addEventToArr = (arr, id, evt) => {
8 | if (arr.some(e => e.id === id)) throw new Error('Event ID taken');
9 |
10 | return [ ...arr, { id, evt } ];
11 | }
12 |
13 | export const removeEventFromArr = (arr, id) => {
14 | const len = arr.length;
15 | const filteredArr = arr.filter(evt => evt.id !== id);
16 |
17 | if (len === filteredArr.length) throw new Error('Event ID not found');
18 |
19 | return filteredArr;
20 | }
21 |
22 | export const triggerEvents = (arr, id, evt) => {
23 | return new Promise((res, rej) => {
24 | const event = arr.find(e => e.id === id).evt;
25 |
26 | event(evt);
27 |
28 | res(true);
29 | rej('Event with this ID not found');
30 | });
31 | }
32 |
33 | export const triggerUpdateUsersDevices = () => {
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/tests/account.test.js:
--------------------------------------------------------------------------------
1 | import Cushion from '../src/cushion';
2 | import PouchDB from 'pouchdb';
3 |
4 | const account = new Cushion().account;
5 | const user = { username: 'foo', password: 'secret' };
6 |
7 | describe('Test sign in', () => {
8 |
9 | test('Sign in foo user with secret password', () => {
10 | expect.assertions(1);
11 | return account.signIn(user).then( res =>
12 | expect(res.name).toBe('foo')
13 | )
14 | });
15 | });
16 |
17 | describe('Get session information', () => {
18 | test('Get user foo session information' , () => {
19 | expect.assertions(1);
20 | return account.getSession().then( res => {
21 | expect(res.ok).toBe(true);
22 | } );
23 | });
24 | });
25 |
26 | describe('Change foo password', () => {
27 | test('Change user foo password from secret to secret2', () => {
28 | expect.assertions(1);
29 | return account.changePassword(user.username, 'secret2')
30 | .then( res => expect(res.ok).toBe(true) );
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cushiondb-client",
3 | "version": "0.1.2",
4 | "description": "CushionDB's database on the client side",
5 | "main": "./dist/main.js",
6 | "scripts": {
7 | "prod": "webpack --mode production",
8 | "dev": "webpack-dev-server --mode development --open",
9 | "build": "webpack --config ./webpack.config.js --mode=production",
10 | "test": "jest"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/CushionDB/CushionClient.git"
15 | },
16 | "keywords": [],
17 | "author": "",
18 | "license": "ISC",
19 | "bugs": {
20 | "url": "https://github.com/CushionDB/issues"
21 | },
22 | "homepage": "https://github.com/drote/cushion#readme",
23 | "devDependencies": {
24 | "@babel/core": "^7.5.5",
25 | "@babel/preset-env": "^7.5.5",
26 | "babel-loader": "^8.0.6",
27 | "jes": "^0.6.1",
28 | "jest": "^24.8.0",
29 | "webpack": "^4.36.1",
30 | "webpack-cli": "^3.3.6",
31 | "webpack-dev-server": "^3.7.2"
32 | },
33 | "dependencies": {
34 | "fs": "0.0.1-security",
35 | "pouchdb": "^7.1.1",
36 | "pouchdb-authentication": "^1.1.3",
37 | "pouchdb-find": "^7.1.1",
38 | "utf-8": "^2.0.0"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = [{
4 | output: {
5 | path: path.resolve(__dirname, 'dist'),
6 | filename: '[name].js',
7 | publicPath: '/assets',
8 | library: 'cushiondb-client',
9 | libraryExport: 'default',
10 | libraryTarget: 'umd'
11 | },
12 | entry: {
13 | main: './src/index.js',
14 | },
15 | module: {
16 | rules: [
17 | {
18 | test: /\.js$/,
19 | exclude: /node_modules/,
20 | use: {
21 | loader: "babel-loader"
22 | }
23 | }
24 | ]
25 | },
26 | node: {
27 | fs: 'empty'
28 | }
29 | },
30 | {
31 | output: {
32 | path: path.resolve(__dirname, 'dist'),
33 | filename: '[name].js',
34 | publicPath: '/assets'
35 | },
36 | entry: {
37 | cushionWorker: './src/cushion_worker/cushionWorkerIndex.js'
38 | },
39 | module: {
40 | rules: [
41 | {
42 | test: /\.js$/,
43 | exclude: /node_modules/,
44 | use: {
45 | loader: "babel-loader"
46 | }
47 | }
48 | ]
49 | },
50 | node: {
51 | fs: 'empty'
52 | }
53 | }
54 | ];
55 |
--------------------------------------------------------------------------------
/src/cushion_worker/cushionWorker.js:
--------------------------------------------------------------------------------
1 | import PouchDB from 'pouchdb';
2 | import path from 'path';
3 | import * as utils from '../utils/cushionWorkerUtils';
4 |
5 | class CushionWorker {
6 | constructor() {
7 | this.pushEvents = [];
8 | this.messageEvents = [];
9 | this.syncEvents = [];
10 | }
11 |
12 | getMetaDB() {
13 | const cushionMeta = new PouchDB('cushionMeta');
14 | return cushionMeta.get('cushionMeta');
15 | }
16 |
17 | pushEventTriggered(evt) {
18 | const eventData = JSON.parse(evt.data.text());
19 |
20 | return utils.triggerEvents(this.pushEvents, eventData.action, evt);
21 | }
22 |
23 | messageEventTriggered(evt) {
24 | const eventData = evt.data;
25 |
26 | return utils.triggerEvents(this.messageEvents, eventData.id, evt);
27 | }
28 |
29 | syncEventTriggered(evt) {
30 | return utils.triggerEvents(this.syncEvents, evt.tag, evt);
31 | }
32 |
33 | addPushEvent(id, evt) {
34 | this.pushEvents = utils.addEventToArr(this.pushEvents, id, evt);
35 | }
36 |
37 | addMessageEvent(id, evt) {
38 | this.messageEvents = utils.addEventToArr(this.messageEvents, id, evt);
39 | }
40 |
41 | addSyncEvent(id, evt) {
42 | this.syncEvents = utils.addEventToArr(this.syncEvents, id, evt);
43 | }
44 |
45 | removePushEvent(id) {
46 | this.pushEvents = utils.removeEventFromArr(this.pushEvents, id);
47 | }
48 |
49 | removeSyncEvent(id) {
50 | this.syncEvents = utils.removeEventFromArr(this.syncEvents, id);
51 | }
52 |
53 | removeMessageEvent(id) {
54 | this.messageEvents = utils.removeEventFromArr(this.messageEvents, id);
55 | }
56 | }
57 |
58 | export default CushionWorker;
59 |
--------------------------------------------------------------------------------
/src/utils/swUtils.js:
--------------------------------------------------------------------------------
1 | import urlB64ToUint8Array from './64to8';
2 | import { getConfigObj } from './configUtils';
3 |
4 | const configObj = getConfigObj();
5 |
6 | const getServiceWorker = () => {
7 | if (navigator.serviceWorker.controller) {
8 | return Promise.resolve(navigator.serviceWorker);
9 | }
10 |
11 | return new Promise((resolve) => {
12 | function onControllerChange() {
13 | navigator.serviceWorker.removeEventListener('controllerchange', onControllerChange);
14 | resolve(navigator.serviceWorker);
15 | }
16 |
17 | navigator.serviceWorker.addEventListener('controllerchange', onControllerChange);
18 | });
19 | }
20 |
21 | const postMessage = (id, payload, sw) => {
22 | return new Promise((res, rej) => {
23 | const msgChannel = new MessageChannel();
24 | msgChannel.port1.onmessage = (evt) => {
25 | if (evt.data.error) {
26 | rej(evt.data.error);
27 | } else {
28 | res(evt.data);
29 | }
30 | }
31 |
32 | sw.controller.postMessage({ id, payload }, [msgChannel.port2]);
33 | });
34 | }
35 |
36 | export const subscribeDeviceToNotifications = () => {
37 | return getServiceWorker()
38 |
39 | .then(sw => sw.ready)
40 | .then(reg => {
41 | return reg.pushManager.subscribe({
42 | userVisibleOnly: true,
43 | applicationServerKey: urlB64ToUint8Array(configObj.publicVapid),
44 | });
45 | })
46 |
47 | .catch(err => Promise.reject(err));
48 | }
49 |
50 | export const scheduleSyncPush = () => {
51 | getServiceWorker().then(sw => {
52 | postMessage('SCHEDULE_PUSH', {}, sw);
53 | });
54 | }
55 |
56 | export const scheduleSyncPull = () => {
57 | getServiceWorker().then(sw => {
58 | postMessage('SCHEDULE_PULL', {}, sw);
59 | });
60 | }
61 |
62 | export const registerServiceWorker = () => {
63 | if ('serviceWorker' in navigator) {
64 | navigator.serviceWorker.register('../../sw.js');
65 | }
66 | }
--------------------------------------------------------------------------------
/src/metaDB.js:
--------------------------------------------------------------------------------
1 | import PouchDB from 'pouchdb';
2 | import { getFetchOpts } from './utils/fetchUtils';
3 | import * as utils from './utils/metaDBUtils';
4 | import * as urls from './utils/urls';
5 |
6 | let metaDB;
7 |
8 | class MetaDB {
9 | constructor() {
10 | this.localName = 'cushionDB';
11 | this.remoteAddress;
12 | this.pushSubscribed;
13 | this.username;
14 | this.ready = this.assignMetaVars();
15 | }
16 |
17 | localDBName() {
18 | return this.localName;
19 | }
20 |
21 | remoteDBName() {
22 | return this.remoteAddress;
23 | }
24 |
25 | subscribedToPush() {
26 | return this.pushSubscribed;
27 | }
28 |
29 | getUsername() {
30 | return this.username;
31 | }
32 |
33 | getMetaDB() {
34 | const cushionMeta = new PouchDB('cushionMeta');
35 | return cushionMeta.get('cushionMeta');
36 | }
37 |
38 | assignMetaVars() {
39 | return this.getMetaDB()
40 |
41 | .then(dbDoc => {
42 | this.remoteAddress = dbDoc.remoteDBAddress;
43 | this.pushSubscribed = dbDoc.subscribedToPush;
44 | this.username = dbDoc.username;
45 | return Promise.resolve();
46 | })
47 |
48 | .catch(_ => {
49 | this.remoteAddress = null;
50 | this.pushSubscribed = null;
51 | this.username = null;
52 | return Promise.resolve();
53 | });
54 | }
55 |
56 | start(remoteDBAddress, username) {
57 | return fetch(
58 | urls.isSubscribedToPush(username),
59 | getFetchOpts({
60 | method: 'GET'
61 | })
62 | )
63 |
64 | .then(res => res.json())
65 | .then(json => {
66 |
67 | const cushionDBDoc = utils.getDefaultMetaDBDoc(
68 | this.localDBName(),
69 | remoteDBAddress,
70 | username,
71 | json.subscribed
72 | )
73 |
74 | metaDB = new PouchDB('cushionMeta');
75 |
76 | return metaDB.put(cushionDBDoc);
77 | })
78 |
79 | .catch(err => Promise.reject(err));
80 | }
81 |
82 | subscribeToNotifications() {
83 | return this.getMetaDB()
84 |
85 | .then(userDoc => {
86 | const metaDB = new PouchDB('cushionMeta');
87 | const newDoc = {
88 | ...userDoc,
89 | subscribedToPush: true,
90 | }
91 |
92 | return metaDB.put(newDoc);
93 | });
94 | }
95 |
96 | destroy() {
97 | return new PouchDB('cushionMeta').destroy();
98 | }
99 | }
100 |
101 | export default MetaDB;
102 |
--------------------------------------------------------------------------------
/src/cushion_worker/cushionWorkerIndex.js:
--------------------------------------------------------------------------------
1 | import CushionWorker from './cushionWorker';
2 | import path from 'path';
3 | import { getFetchOpts } from '../utils/fetchUtils';
4 | import { getConfigObj } from '../utils/configUtils';
5 | import * as urls from '../utils/urls';
6 | import MetaDB from '../metaDB';
7 | import * as utils from '../utils/cushionWorkerUtils';
8 |
9 | const configObj = getConfigObj();
10 | const useIcons = configObj.appPushIcons;
11 | const ROOT_DIR = path.dirname(require.main.filename);
12 |
13 | global.cushionWorker = new CushionWorker();
14 |
15 | cushionWorker.addPushEvent('SYNC', (event) => {
16 | const metaDB = new MetaDB();
17 | const title = configObj.appname;
18 | const options = {
19 | body: "is updating in the background.",
20 | silent: true,
21 | renotify: false
22 | };
23 |
24 | if (useIcons) {
25 | options = {
26 | ...options,
27 | icon: '.\icons\logo-icon.png',
28 | badge: '.\icons\logo-badge.png'
29 | }
30 | }
31 |
32 | return metaDB.ready.then(_ => {
33 | return Promise.all([
34 | utils.pouchSync(metaDB.remoteDBName(), metaDB.localDBName()),
35 | self.registration.showNotification(title, options)
36 | ]);
37 | });
38 | });
39 |
40 | cushionWorker.addMessageEvent('SCHEDULE_PUSH', () => {
41 | self.registration.sync.register('REPLICATE_TO_SERVER');
42 | return Promise.resolve();
43 | });
44 |
45 | cushionWorker.addMessageEvent('SCHEDULE_PULL', () => {
46 | self.registration.sync.register('REPLICATE_FROM_SERVER');
47 | return Promise.resolve();
48 | });
49 |
50 | cushionWorker.addSyncEvent('REPLICATE_TO_SERVER', () => {
51 | const metaDB = new MetaDB();
52 | let userDoc;
53 |
54 | return metaDB.ready.then(_ => {
55 | return utils.pouchSync(metaDB.localDBName(), metaDB.remoteDBName());
56 | })
57 |
58 | .then(() => {
59 | if (metaDB.subscribedToPush()) {
60 |
61 | const data = {
62 | username: metaDB.username,
63 | device: navigator.platform
64 | };
65 |
66 | return fetch(
67 | urls.triggerUpdateDevices(),
68 | getFetchOpts({
69 | method: 'POST',
70 | data
71 | })
72 | );
73 | }
74 | })
75 |
76 | .catch(err => Promise.reject(err));
77 | });
78 |
79 | cushionWorker.addSyncEvent('REPLICATE_FROM_SERVER', () => {
80 | const metaDB = new MetaDB();
81 |
82 | return metaDB.ready.then(_ => {
83 | if (!metaDB.remoteDBName()) return;
84 |
85 | return utils.pouchSync(metaDB.remoteDBName(), metaDB.localDBName());
86 | })
87 |
88 | .catch(err => Promise.reject(err));
89 | });
90 |
--------------------------------------------------------------------------------
/tests/store.test.js:
--------------------------------------------------------------------------------
1 | import Cushion from '../src/cushion';
2 | import PouchDB from 'pouchdb';
3 |
4 | const store = new Cushion().store;
5 | store.localDB = new PouchDB('testDB');
6 | store.deleteAll();
7 |
8 | describe('starting an unregistered new cushion DB instance', () => {
9 | test('starts with default pouchDB if name not provided', () => {
10 | // expect(store.localDB.name).toBe('testDB');
11 | });
12 |
13 | test('starts with an empty listeners list', () => {
14 | expect(store.listeners.length).toEqual(0);
15 | });
16 | });
17 |
18 | describe('Store functionality', () => {
19 | const doc = { todo: 'Task 1', due: 'today', completed: false };
20 | let docID;
21 |
22 | test('Get All docs - no document exist', () => {
23 | expect.assertions(1);
24 | return store.getAll().then( docs => {
25 | expect(docs.length).toEqual(0);
26 | });
27 | });
28 |
29 | test('Add document to cushionDB', () => {
30 | expect.assertions(1);
31 | return store.set(doc).then( id => {
32 | docID = id ;
33 | expect(id).toEqual(expect.anything())
34 | });
35 | });
36 |
37 | test('Retrieve document from cushionDB', () => {
38 | expect.assertions(1);
39 | return store.get(docID).then( doc => {
40 | expect(doc).toHaveProperty('_id', docID );
41 | });
42 | });
43 |
44 | test('Update document', () => {
45 | const attributes = { length: 3 };
46 | expect.assertions(1);
47 | return store.update(docID, attributes).then( id => {
48 | expect(id).toEqual(docID)
49 | });
50 | });
51 |
52 | test('Update document - check new field added', () => {
53 | expect.assertions(1);
54 | return store.get(docID).then ( doc => {
55 | expect(doc).toHaveProperty('length', 3);
56 | });
57 | });
58 |
59 | test('Add second document to cushionDB', () => {
60 | const doc2 = { todo: 'Task 2', due: 'tomorrow', completed: false };
61 | expect.assertions(1);
62 | return store.set(doc2).then( id => {
63 | return store.get(id).then(doc =>
64 | expect(doc).toHaveProperty('todo', 'Task 2')
65 | )
66 | });
67 | });
68 |
69 | test('Find a document by attribute', () => {
70 | // creates a index document within the local database
71 | expect.assertions(1);
72 | return store.find('todo', 'Task 2').then( docs =>
73 | expect(docs[0]).toHaveProperty('todo', 'Task 2')
74 | )
75 | });
76 |
77 | test('Get All docs', () => {
78 | expect.assertions(1);
79 | return store.getAll().then( docs => {
80 | // 2 docs plus index document
81 | expect(docs.length).toEqual(3);
82 | });
83 | });
84 |
85 | test('Delete a document', () => {
86 | expect.assertions(1);
87 | return store.delete(docID).then(id => {
88 | expect(id).toEqual(docID)
89 | });
90 | });
91 |
92 | test('Destroy DB ', () => {
93 | expect.assertions(1) ;
94 | return store.destroy().then( res => {
95 | expect(res).toHaveProperty('ok', true);
96 | });
97 | });
98 | });
99 |
--------------------------------------------------------------------------------
/src/store.js:
--------------------------------------------------------------------------------
1 | import PouchDB from 'pouchdb';
2 | import PouchDBFind from 'pouchdb-find';
3 | PouchDB.plugin(PouchDBFind);
4 |
5 | import { scheduleSyncPush, scheduleSyncPull } from './utils/swUtils';
6 | import urlB64ToUint8Array from './utils/64to8.js';
7 |
8 | let listeners = [];
9 | let dbAuth;
10 |
11 | const notifyListeners = () => {
12 | listeners.forEach(l => l());
13 | }
14 |
15 | class Store {
16 | constructor(dataAuth) {
17 | dbAuth = dataAuth;
18 | dbAuth.bindToLocalDBChange(notifyListeners);
19 | }
20 |
21 | subscribe(listener) {
22 | listeners = [
23 | ...listeners,
24 | listener
25 | ];
26 |
27 | return () => {
28 | listeners = listeners.filter(l => l !== listener);
29 | }
30 | }
31 |
32 | set(document) {
33 | return dbAuth.localDB.post(document)
34 | .then(doc => doc.id)
35 | .catch(err => Promise.reject(err));
36 | }
37 |
38 | get(id) {
39 | return dbAuth.localDB.get(id)
40 | .then(doc => {
41 | const { _rev, ...docWithoutRev } = doc;
42 |
43 | return docWithoutRev;
44 | })
45 |
46 | .catch(err => Promise.reject(err));
47 | }
48 |
49 | getAll() {
50 | return dbAuth.localDB.allDocs({
51 | 'include_docs': true,
52 | })
53 |
54 | .then(docs => {
55 | return docs.rows.map(doc => {
56 | const { _rev, ...rest } = doc.doc;
57 | return rest;
58 | });
59 | })
60 |
61 | .catch(err => Promise.reject(err));
62 | }
63 |
64 | update(id, attrs) {
65 | return dbAuth.localDB.get(id)
66 | .then(doc => {
67 | return dbAuth.localDB.put({
68 | ...doc,
69 | ...attrs
70 | });
71 | })
72 |
73 | .then(doc => doc.id)
74 | .catch(err => Promise.reject(err));
75 | }
76 |
77 | delete(id) {
78 | return dbAuth.localDB.get(id)
79 | .then(doc => {
80 | doc._deleted = true;
81 |
82 | return dbAuth.localDB.put(doc)
83 | })
84 |
85 | .then(doc => doc.id)
86 | .catch(err => Promise.reject(err));
87 | }
88 |
89 | deleteAll() {
90 | return dbAuth.localDB.allDocs({
91 | include_docs: true
92 | })
93 |
94 | .then(docs => {
95 | const deletedDocs = docs.rows.map(row => {
96 | return {
97 | _id: row.doc._id,
98 | _rev: row.doc._rev,
99 | _deleted: true
100 | }
101 | });
102 |
103 | return dbAuth.localDB.bulkDocs(deletedDocs);
104 | })
105 |
106 | .catch(err => Promise.reject(err));
107 | }
108 |
109 | find(attribute, value) {
110 | return dbAuth.localDB.createIndex({
111 | index: {
112 | fields: [attribute]
113 | }
114 | })
115 |
116 | .then(res => {
117 | return dbAuth.localDB.find({
118 | selector: {
119 | [attribute]: value
120 | }
121 | });
122 | })
123 |
124 | .then(r => r.docs)
125 | .catch(err => Promise.reject(err));
126 | }
127 |
128 | destroy() {
129 | return dbAuth.destroyLocal()
130 | .then(_ => {
131 | return { status: 'success' };
132 | })
133 |
134 | .catch(err => Promise.reject(err));
135 | }
136 | }
137 |
138 | export default Store;
139 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # Overview
4 |
5 | CushionDB is an open source, easy-to-use data management framework for building small, offline-first, PWA compliant applications. It simplifies the process of managing and persisting single-user data without writing any server-side database code and provides user authentication capabilities that are crucial for efficient client-side data management.
6 |
7 | CushionDB expands on current offline-first data models by employing different PWA tools that ensure data integrity regardless of network connectivity, and abstracts many of the complexities around utilizing these tools for native-like client side data management.
8 |
9 | CushionClient is designed to work with [CushionSever](https://github.com/CushionDB/CushionServer) and a pre-configured CouchDB we call [CushionCouch](https://github.com/CushionDB/CushionCouchDocker). Both CushionServer and CushionCouch are setup as Docker images and can easily be built, configured and run using a simple script provided by CushionDB.
10 |
11 | ## CushionDB Architecture
12 |
13 | 
14 |
15 | # Getting Started
16 |
17 | ## Prerequisites
18 |
19 | * Node.js >= 10.16.3 - *CushionClient*
20 | * Docker - *CushionServer and CushionCouch*
21 |
22 | ## Install
23 |
24 | ```
25 | npm i cushiondb-client
26 | mv node_modules/cushiondb-client/sw.js .
27 | ```
28 |
29 | ## Setup
30 |
31 | CushionClient is an npm package. It can either be added to the projects node modules by running `npm i cushion-client` from within your project's root directory, or `cushiondb-client` can simply be added as a dependency inside your project's `package.json` file.
32 |
33 | Once CushionClient has been added as a dependency, changes can be made to the `.couchConfig.json` file within the `node_modules/cushiondb-client` directory. This is only necessary if the default server configurations are changed while setting up the CushionDB backend.
34 |
35 | 
36 |
37 | The URLs in this config file will be used for networking with the two backend containers and the Public VAPID key is needed for PWA Push Notifications to work.
38 |
39 | Finally, a Service Worker file (`sw.js`) is needed in the project's root directory. This file comes packaged with the `cushion-client` node module and can be moved to your current project's root directory by running `mv node_modules/cushion-client/sw.js .` from the command line.
40 |
41 | 
42 |
43 | ## Sample Usage
44 |
45 | ```js
46 | import Cushion from 'cushiondb-client';
47 | // or
48 | const Cushion = require('cushiondb-client');
49 | ```
50 |
51 | ```js
52 | // instantiate a new CushionDB object
53 | const cushion = new Cushion();
54 |
55 | // sign up a new user
56 | cushion.account.signUp({
57 | username: 'jDoe',
58 | password: 'secret'
59 | });
60 |
61 | // add a document to the database
62 | cushion.store.set({
63 | title: 'todo',
64 | completed: false
65 | }).then(id => {
66 | // do something
67 | }).catch(err => {
68 | // handle error
69 | });
70 | ```
71 |
72 | # The Team
73 |
74 | [Avshar Kirksall](https://avshrk.github.io/
75 | ) *Software Engineer* Brooklyn, NY
76 |
77 | [Jaron Truman](https://jtruman88.github.io/
78 | ) *Software Engineer* Las Vegas, NV
79 |
80 | [Daniel Rote](https://drote.github.io) *Software Engineer* Seattle, WA
81 |
--------------------------------------------------------------------------------
/src/databaseAuth.js:
--------------------------------------------------------------------------------
1 | import PouchDB from 'pouchdb';
2 | import PouchAuth from 'pouchdb-authentication';
3 | PouchDB.plugin(PouchAuth);
4 |
5 | import MetaDB from './metaDB';
6 | import { scheduleSyncPush, scheduleSyncPull } from './utils/swUtils';
7 |
8 | class DatabaseAuth {
9 | constructor(couchBaseURL) {
10 | this.metaDB = new MetaDB();
11 | this.couchBaseURL = couchBaseURL;
12 | this.ready = this.metaDB.ready;
13 |
14 | this.metaDB.ready
15 |
16 | .then(() => {
17 | this.localDB = new PouchDB(this.metaDB.localDBName());
18 |
19 | if (this.metaDB.remoteDBName()) {
20 | this.remoteDB = new PouchDB(this.metaDB.remoteDBName());
21 | scheduleSyncPull();
22 | }
23 |
24 | this.bindToLocalDBChange(() => {
25 | this.isSignedIn().then(res => {
26 | if (res) {
27 | scheduleSyncPush();
28 | }
29 | })
30 | });
31 | })
32 |
33 | .catch(err => Promise.reject(err));
34 | }
35 |
36 | bindToLocalDBChange(callback) {
37 | this.localDB.changes({
38 | live: true,
39 | since: 'now'
40 | }).on('change', callback);
41 | }
42 |
43 | isSignedIn() {
44 | if (!this.remoteDB) return Promise.resolve(false);
45 |
46 | return this.getSession(this.remoteDB)
47 |
48 | .then(res => !!res)
49 | .catch(err => Promise.reject(err));
50 | }
51 |
52 | getSession(remoteDB) {
53 | return this.remoteDB.getSession()
54 |
55 | .then(res => {
56 | if (!res.userCtx.name) return null;
57 |
58 | return res;
59 | })
60 | }
61 |
62 | getPassword(remoteDB) {
63 | if (!this.remoteDB) return undefined;
64 |
65 | return this.remoteDB.__opts.auth.password;
66 | }
67 |
68 | signIn(username, password) {
69 | const couchUserDBName = DatabaseAuth.createCouchUserDBName(this.couchBaseURL, username);
70 | const fakeRemoteDB = this.createRemoteCouchDBHandle(couchUserDBName, username, password);
71 |
72 | return fakeRemoteDB.logIn(username, password)
73 |
74 | .then(res => this.metaDB.start(couchUserDBName, username))
75 | .then(res => {
76 | this.remoteDB = fakeRemoteDB;
77 | scheduleSyncPull();
78 | scheduleSyncPush();
79 | return true;
80 | })
81 |
82 | .catch(err => {
83 | if (err.name === 'unauthorized' || err.name === 'forbidden') {
84 | return Promise.reject('Username or password incorrect');
85 | }
86 |
87 | return Promise.reject(err);
88 | });
89 | }
90 |
91 | signOut(options = {}) {
92 | if (!this.remoteDB) return Promise.reject('No remote database to sign out with');
93 |
94 | return this.remoteDB.logOut()
95 |
96 | .then(res => {
97 | if (!res.ok) return Promise.reject('Sign out failed');
98 |
99 | if (options.destroyLocal) {
100 | return this.destroyLocal()
101 | .then(() => {
102 | this.localDB = new PouchDB(this.metaDB.localDBName());
103 | this.remoteDB = null;
104 | return this.metaDB.destroy();
105 | });
106 | }
107 |
108 | this.remoteDB = null;
109 | return this.metaDB.destroy();
110 | })
111 |
112 | .catch(err => Promise.reject(err));
113 | }
114 |
115 | destroyLocal() {
116 | return this.localDB.destroy()
117 | .then(res => res)
118 | .catch(err => Promise.reject(err));
119 | }
120 |
121 | createRemoteCouchDBHandle(remoteName, username, password) {
122 | return new PouchDB(
123 | remoteName,
124 | {
125 | skip_setup: true,
126 | auth: {
127 | username,
128 | password
129 | }
130 | }
131 | );
132 | }
133 |
134 | changePassword(username, newPassword) {
135 | return this.signOut()
136 |
137 | .then(_ => this.signIn(username, newPassword));
138 | }
139 |
140 | destroyUser(username) {
141 | return this.remoteDB.deleteUser(username)
142 |
143 | .then(res => {
144 | this.remoteDB = null;
145 | return this.localDB.destroy();
146 | })
147 |
148 | .then(res => {
149 | return this.metaDB.destroy();
150 | })
151 |
152 | .catch(err => Promise.reject(err));
153 | }
154 |
155 | getUsername() {
156 | if (!this.remoteDB) return undefined;
157 |
158 | return this.metaDB.getUsername();
159 | }
160 |
161 | subscribeToNotifications() {
162 | this.metaDB.subscribeToNotifications();
163 | }
164 |
165 | getUserDoc(username) {
166 | return this.remoteDB.getUser(username);
167 | }
168 |
169 | static createCouchUserDBName(couchBaseURL, username) {
170 | const hexUsername = Buffer.from(username, 'utf8').toString('hex');
171 |
172 | return `${couchBaseURL}/cushion_${hexUsername}`;
173 | }
174 | }
175 |
176 | export default DatabaseAuth;
177 |
--------------------------------------------------------------------------------
/src/account.js:
--------------------------------------------------------------------------------
1 | import PouchDB from 'pouchdb';
2 | import PouchAuth from 'pouchdb-authentication';
3 | PouchDB.plugin(PouchAuth);
4 |
5 | import * as urls from './utils/urls';
6 | import * as fetchUtils from './utils/fetchUtils';
7 | import * as swUtils from './utils/swUtils';
8 |
9 | let dbAuth;
10 |
11 | class Account {
12 | constructor(dataAuth) {
13 | dbAuth = dataAuth;
14 | }
15 |
16 | isSignedIn() {
17 | return dbAuth.isSignedIn();
18 | }
19 |
20 | signUp({ username, password }) {
21 | if (!username || !password) {
22 | throw new Error('username and password are required.');
23 | }
24 |
25 | let errMessage;
26 |
27 | return fetch(
28 | urls.signup(),
29 | fetchUtils.getFetchOpts({
30 | method: 'POST',
31 | data: {
32 | username,
33 | password
34 | }
35 | })
36 | )
37 |
38 | .then(response => {
39 | if (response.ok) {
40 | return { status: 'success' };
41 | }
42 |
43 | switch (response.status) {
44 | case 401:
45 | errMessage = 'CouchDB admin or password incorrect';
46 | break;
47 | case 409:
48 | errMessage = 'Username is taken';
49 | break;
50 | default:
51 | errMessage = 'Something went wrong';
52 | }
53 |
54 | return Promise.reject(errMessage)
55 | })
56 |
57 | .catch(err => Promise.reject(err));
58 | }
59 |
60 | signIn({ username, password }) {
61 | if (!username || !password) {
62 | return Promise.reject('username and password are required.');
63 | }
64 |
65 | return dbAuth.signIn(username, password)
66 |
67 | .then(res => {
68 | return { status: 'success' }
69 | })
70 |
71 | .catch(err => Promise.reject(err));
72 | }
73 |
74 | signOut(options) {
75 | return dbAuth.signOut(options)
76 | .then(res => {
77 | return { status: 'success' }
78 | })
79 | .catch(err => Promise.reject(err));
80 | }
81 |
82 | getUserDoc(username) {
83 | return this.isSignedIn()
84 |
85 | .then(res => {
86 | if (!res) return Promise.reject('User is not signed in');
87 |
88 | return dbAuth.remoteDB.getUserDoc(username)
89 | })
90 |
91 | .catch(err => Promise.reject(err));
92 | }
93 |
94 | changePassword(username, newPassword) {
95 | return fetch(
96 | urls.changePassword(),
97 | fetchUtils.getFetchOpts({
98 | method: 'POST',
99 | data: {
100 | name: username,
101 | roles: [],
102 | type: 'user',
103 | password: newPassword
104 | }
105 | })
106 | )
107 |
108 | .then(res => res.json())
109 | .then(json => {
110 | if (json.ok) {
111 | dbAuth.changePassword(username, newPassword);
112 | }
113 | })
114 |
115 | .catch(err => Promise.reject(err));
116 | }
117 |
118 | destroy(username) {
119 | return this.isSignedIn()
120 |
121 | .then(res => {
122 | if (!res) return Promise.reject('User is not signed in');
123 |
124 | return dbAuth.destroyUser(username);
125 | })
126 |
127 | .then(res => {
128 | if (res.ok) {
129 | return { status: 'success' };
130 | }
131 | })
132 |
133 | .catch(err => Promise.reject(err));
134 | }
135 |
136 | // changeUsername({curUsername, password, newUsername}){
137 | // let url = `${TEMP_CONFIG.remoteBaseURL}updateUser`;
138 | // let options = {
139 | // method: 'PUT',
140 | // data: {
141 | // name: curUsername,
142 | // password: password,
143 | // newName: newUsername
144 | // },
145 | // headers: {
146 | // "Content-Type": "application/json",
147 | // "Accept": "application/json"
148 | // },
149 | // };
150 | // request(url, options);
151 | // }
152 |
153 | subscribeToNotifications() {
154 | let subscription;
155 |
156 | return this.isSignedIn()
157 |
158 | .then(res => {
159 | if (!res) return Promise.reject('User is not signed in');
160 |
161 | return swUtils.subscribeDeviceToNotifications();
162 | })
163 |
164 | .then(sub => {
165 | subscription = sub;
166 | return dbAuth.getUsername();
167 | })
168 |
169 | .then(username => {
170 | return fetch(
171 | urls.subscribeDeviceToPush(),
172 | fetchUtils.getFetchOpts({
173 | method: 'POST',
174 | data: {
175 | username,
176 | subscription,
177 | device: navigator.platform
178 | }
179 | })
180 | );
181 | })
182 |
183 | .then(_ => {
184 | dbAuth.subscribeToNotifications();
185 | return { status: 'success' }
186 | })
187 |
188 | .catch(err => Promise.reject(err));
189 | }
190 | }
191 |
192 | export default Account;
193 |
--------------------------------------------------------------------------------