├── .DS_Store ├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── LICENSE.md ├── README.md ├── dist └── vuex-orm-localforage.js ├── package-lock.json ├── package.json ├── src ├── actions │ ├── Action.js │ ├── Destroy.js │ ├── DestroyAll.js │ ├── Fetch.js │ ├── Get.js │ └── Persist.js ├── common │ └── context.js ├── index.js ├── orm │ └── Model.js ├── support │ └── interfaces.js └── vuex-orm-localforage.js └── webpack.config.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eldomagan/vuex-orm-localforage/3d4b1ddd7ff3aaed4cb4edba9aea396e9962466e/.DS_Store -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"], 3 | "plugins": ["@babel/plugin-transform-runtime"] 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["airbnb-base"], 3 | plugins: ['jest'], 4 | parserOptions: { 5 | parser: "babel-eslint", 6 | }, 7 | env: { 8 | "jest/globals": true, 9 | }, 10 | rules: { 11 | "no-param-reassign": [2, { "props": false }], 12 | "no-underscore-dangle": 0, 13 | "class-methods-use-this": 0 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | yarn-error.log 3 | vuex-orm-axios*.tgz 4 | coverage 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Eldo Magan 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 2 | [![License](https://img.shields.io/npm/l/vuex-orm-localforage.svg)](https://github.com/eldomagan/vuex-orm-localforage/blob/master/LICENSE.md) 3 | 4 | # Vuex ORM Plugin: LocalForage 5 | 6 | VuexORMLocalforage is a plugin for the amazing [VuexORM](https://github.com/vuex-orm/vuex-orm) that let you sync your [Vuex](https://github.com/vuejs/vuex) Store with an IndexedDB database using [LocalForage](https://github.com/localForage/localForage). 7 | 8 | ## Installation 9 | 10 | Add the package to your dependencies 11 | 12 | ```shell 13 | yarn add vuex-orm-localforage 14 | ``` 15 | Or 16 | 17 | ```shell 18 | npm install --save vuex-orm-localforage 19 | ``` 20 | 21 | Then you can setup the plugin 22 | 23 | ``` js 24 | import VuexORM from '@vuex-orm/core' 25 | import VuexORMLocalForage from 'vuex-orm-localforage' 26 | 27 | const database = new VuexORM.Database() 28 | 29 | VuexORM.use(VuexORMLocalForage, { 30 | database 31 | }) 32 | 33 | // ... 34 | 35 | export default () => new Vuex.Store({ 36 | namespaced: true, 37 | plugins: [VuexORM.install(database)] 38 | }) 39 | 40 | ``` 41 | 42 | See https://vuex-orm.github.io/vuex-orm/guide/prologue/getting-started.html#create-modules on how to setup the database 43 | 44 | ## Actions 45 | 46 | This plugin add some vuex actions to load and persist data in an IndexedDB 47 | 48 | | Action | Description | 49 | | ------- | ----------- | 50 | | $fetch | Load data from the IndexedDB store associated to a model and persist them in the Vuex Store | 51 | | $get | Load data by id from the IndexedDB store associated and persist it to Vuex Store | 52 | | $create | Like VuexORM `insertOrUpdate`, but also persist data to IndexedDB | 53 | | $update | Update records using VuexORM `update` or `insertOrUpdate` then persist changes to IndexedDB | 54 | | $replace | Like VuexORM `create`, but also replace all data from IndexedDB | 55 | | $delete | Like VuexORM `delete`, but also remove data from IndexedDB | 56 | | $deleteAll | Like VuexORM `deleteAll`, but also delete all data from IndexedDB | 57 | 58 | ## Example Usage 59 | 60 | ```vue 61 | 71 | 72 | 105 | ``` 106 | ## Configuration Options 107 | 108 | These are options you can pass when calling VuexORM.use() 109 | 110 | ```js 111 | { 112 | // The VuexORM Database instance 113 | database, 114 | 115 | /** 116 | * LocalForage config options 117 | * 118 | * @see https://github.com/localForage/localForage#configuration 119 | */ 120 | localforage: { 121 | name: 'vuex', // Name is required 122 | ... 123 | }, 124 | 125 | /** 126 | * You can override names of actions introduced by this plugin 127 | */ 128 | actions: { 129 | $get: '$get', 130 | $fetch: '$fetch', 131 | $create: '$create', 132 | $update: '$update', 133 | $replace: '$replace', 134 | $delete: '$delete', 135 | $deleteAll: '$deleteAll' 136 | } 137 | } 138 | ``` 139 | 140 | You can also override localforage config per model 141 | 142 | ```js 143 | class Post extends Model { 144 | static localforage = { 145 | driver: localforage.WEBSQL, 146 | storeName: 'my_posts' 147 | } 148 | } 149 | ``` 150 | 151 | ## Using with other VuexORM Plugin 152 | 153 | There may be a conflict when using this plugin along with other VuexORM plugins as they are following the same API (aka they introduced the same actions: $fetch, $create...) 154 | 155 | 156 | Just override actions names like that 157 | 158 | ```js 159 | VuexORM.use(VuexORMLocalForage, { 160 | database, 161 | actions: { 162 | $get: '$getFromLocal', 163 | $fetch: '$fetchFromLocal', 164 | $create: '$createLocally', 165 | $update: '$updateLocally', 166 | $replace: '$replaceLocally', 167 | $delete: '$deleteFromLocal', 168 | $deleteAll: '$deleteAllFromLocal' 169 | } 170 | }) 171 | ``` 172 | 173 | Then 174 | 175 | ```js 176 | Post.$fetchFromLocal() // instead of Post.$fetch() 177 | ... 178 | ``` 179 | -------------------------------------------------------------------------------- /dist/vuex-orm-localforage.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.VuexORMLocalForage=e():t.VuexORMLocalForage=e()}(global,(function(){return function(t){var e={};function n(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)n.d(r,o,function(e){return t[e]}.bind(null,o));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=14)}([function(t,e){t.exports=function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}},function(t,e){function n(t,e){for(var n=0;n=43)}})).catch((function(){return!1}))}(t).then((function(t){return l=t}))}function v(t){var e=d[t.name],n={};n.promise=new a((function(t,e){n.resolve=t,n.reject=e})),e.deferredOperations.push(n),e.dbReady?e.dbReady=e.dbReady.then((function(){return n.promise})):e.dbReady=n.promise}function y(t){var e=d[t.name].deferredOperations.pop();if(e)return e.resolve(),e.promise}function b(t,e){var n=d[t.name].deferredOperations.pop();if(n)return n.reject(e),n.promise}function m(t,e){return new a((function(n,r){if(d[t.name]=d[t.name]||{forages:[],db:null,dbReady:null,deferredOperations:[]},t.db){if(!e)return n(t.db);v(t),t.db.close()}var i=[t.name];e&&i.push(t.version);var a=o.open.apply(o,i);e&&(a.onupgradeneeded=function(e){var n=a.result;try{n.createObjectStore(t.storeName),e.oldVersion<=1&&n.createObjectStore("local-forage-detect-blob-support")}catch(n){if("ConstraintError"!==n.name)throw n;console.warn('The database "'+t.name+'" has been upgraded from version '+e.oldVersion+" to version "+e.newVersion+', but the storage "'+t.storeName+'" already exists.')}}),a.onerror=function(t){t.preventDefault(),r(a.error)},a.onsuccess=function(){n(a.result),y(t)}}))}function g(t){return m(t,!1)}function _(t){return m(t,!0)}function w(t,e){if(!t.db)return!0;var n=!t.db.objectStoreNames.contains(t.storeName),r=t.versiont.db.version;if(r&&(t.version!==e&&console.warn('The database "'+t.name+"\" can't be downgraded from version "+t.db.version+" to version "+t.version+"."),t.version=t.db.version),o||n){if(n){var i=t.db.version+1;i>t.version&&(t.version=i)}return!0}return!1}function S(t){return i([function(t){for(var e=t.length,n=new ArrayBuffer(e),r=new Uint8Array(n),o=0;o0&&(!t.db||"InvalidStateError"===o.name||"NotFoundError"===o.name))return a.resolve().then((function(){if(!t.db||"NotFoundError"===o.name&&!t.db.objectStoreNames.contains(t.storeName)&&t.version<=t.db.version)return t.db&&(t.version=t.db.version+1),_(t)})).then((function(){return function(t){v(t);for(var e=d[t.name],n=e.forages,r=0;r>4,s[u++]=(15&r)<<4|o>>2,s[u++]=(3&o)<<6|63&i;return f}function L(t){var e,n=new Uint8Array(t),r="";for(e=0;e>2],r+=O[(3&n[e])<<4|n[e+1]>>4],r+=O[(15&n[e+1])<<2|n[e+2]>>6],r+=O[63&n[e+2]];return n.length%3==2?r=r.substring(0,r.length-1)+"=":n.length%3==1&&(r=r.substring(0,r.length-2)+"=="),r}var M={serialize:function(t,e){var n="";if(t&&(n=k.call(t)),t&&("[object ArrayBuffer]"===n||t.buffer&&"[object ArrayBuffer]"===k.call(t.buffer))){var r,o="__lfsc__:";t instanceof ArrayBuffer?(r=t,o+="arbf"):(r=t.buffer,"[object Int8Array]"===n?o+="si08":"[object Uint8Array]"===n?o+="ui08":"[object Uint8ClampedArray]"===n?o+="uic8":"[object Int16Array]"===n?o+="si16":"[object Uint16Array]"===n?o+="ur16":"[object Int32Array]"===n?o+="si32":"[object Uint32Array]"===n?o+="ui32":"[object Float32Array]"===n?o+="fl32":"[object Float64Array]"===n?o+="fl64":e(new Error("Failed to get type for BinaryArray"))),e(o+L(r))}else if("[object Blob]"===n){var i=new FileReader;i.onload=function(){var n="~~local_forage_type~"+t.type+"~"+L(this.result);e("__lfsc__:blob"+n)},i.readAsArrayBuffer(t)}else try{e(JSON.stringify(t))}catch(n){console.error("Couldn't convert value into a JSON string: ",t),e(null,n)}},deserialize:function(t){if("__lfsc__:"!==t.substring(0,A))return JSON.parse(t);var e,n=t.substring(N),r=t.substring(A,N);if("blob"===r&&R.test(n)){var o=n.match(R);e=o[1],n=n.substring(o[0].length)}var a=D(n);switch(r){case"arbf":return a;case"blob":return i([a],{type:e});case"si08":return new Int8Array(a);case"ui08":return new Uint8Array(a);case"uic8":return new Uint8ClampedArray(a);case"si16":return new Int16Array(a);case"ur16":return new Uint16Array(a);case"si32":return new Int32Array(a);case"ui32":return new Uint32Array(a);case"fl32":return new Float32Array(a);case"fl64":return new Float64Array(a);default:throw new Error("Unkown type: "+r)}},stringToBuffer:D,bufferToString:L};function P(t,e,n,r){t.executeSql("CREATE TABLE IF NOT EXISTS "+e.storeName+" (id INTEGER PRIMARY KEY, key unique, value)",[],n,r)}function B(t,e,n,r,o,i){t.executeSql(n,r,o,(function(t,a){a.code===a.SYNTAX_ERR?t.executeSql("SELECT name FROM sqlite_master WHERE type='table' AND name = ?",[e.storeName],(function(t,c){c.rows.length?i(t,a):P(t,e,(function(){t.executeSql(n,r,o,i)}),i)}),i):i(t,a)}),i)}function F(t,e,n,r){var o=this;t=f(t);var i=new a((function(i,a){o.ready().then((function(){void 0===e&&(e=null);var c=e,u=o._dbInfo;u.serializer.serialize(e,(function(e,f){f?a(f):u.db.transaction((function(n){B(n,u,"INSERT OR REPLACE INTO "+u.storeName+" (key, value) VALUES (?, ?)",[t,e],(function(){i(c)}),(function(t,e){a(e)}))}),(function(e){if(e.code===e.QUOTA_ERR){if(r>0)return void i(F.apply(o,[t,c,n,r-1]));a(e)}}))}))})).catch(a)}));return c(i,n),i}function T(t){return new a((function(e,n){t.transaction((function(r){r.executeSql("SELECT name FROM sqlite_master WHERE type='table' AND name <> '__WebKitDatabaseInfoTable__'",[],(function(n,r){for(var o=[],i=0;i0}var U={_driver:"localStorageWrapper",_initStorage:function(t){var e={};if(t)for(var n in t)e[n]=t[n];return e.keyPrefix=C(t,this._defaultConfig),z()?(this._dbInfo=e,e.serializer=M,a.resolve()):a.reject()},_support:function(){try{return"undefined"!=typeof localStorage&&"setItem"in localStorage&&!!localStorage.setItem}catch(t){return!1}}(),iterate:function(t,e){var n=this,r=n.ready().then((function(){for(var e=n._dbInfo,r=e.keyPrefix,o=r.length,i=localStorage.length,a=1,c=0;c=0;n--){var r=localStorage.key(n);0===r.indexOf(t)&&localStorage.removeItem(r)}}));return c(n,t),n},length:function(t){var e=this.keys().then((function(t){return t.length}));return c(e,t),e},key:function(t,e){var n=this,r=n.ready().then((function(){var e,r=n._dbInfo;try{e=localStorage.key(t)}catch(t){e=null}return e&&(e=e.substring(r.keyPrefix.length)),e}));return c(r,e),r},keys:function(t){var e=this,n=e.ready().then((function(){for(var t=e._dbInfo,n=localStorage.length,r=[],o=0;o=0;e--){var n=localStorage.key(e);0===n.indexOf(t)&&localStorage.removeItem(n)}})):a.reject("Invalid arguments"),e),r}},q=function(t,e){for(var n,r,o=t.length,i=0;i=0;--o){var i=this.tryEntries[o],a=i.completion;if("root"===i.tryLoc)return r("end");if(i.tryLoc<=this.prev){var c=n.call(i,"catchLoc"),u=n.call(i,"finallyLoc");if(c&&u){if(this.prev=0;--r){var o=this.tryEntries[r];if(o.tryLoc<=this.prev&&n.call(o,"finallyLoc")&&this.prev=0;--e){var n=this.tryEntries[e];if(n.finallyLoc===t)return this.complete(n.completion,n.afterLoc),w(n),f}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var n=this.tryEntries[e];if(n.tryLoc===t){var r=n.completion;if("throw"===r.type){var o=r.arg;w(n)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(t,e,n){return this.delegate={iterator:I(t),resultName:e,nextLoc:n},"next"===this.method&&(this.arg=void 0),f}},t}(t.exports);try{regeneratorRuntime=r}catch(t){Function("r","regeneratorRuntime = r")(r)}},function(t,e){function n(e,r){return t.exports=n=Object.setPrototypeOf||function(t,e){return t.__proto__=e,t},n(e,r)}t.exports=n},function(t,e){t.exports=function(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}},function(t,e,n){"use strict";n.r(e);var r=n(0),o=n.n(r),i=n(1),a=n.n(i),c=n(7),u=n.n(c),f={database:new(n(9).Database),name:"vuex",localforage:{name:"vuex"},actions:{$fetch:"$fetch",$get:"$get",$create:"$create",$update:"$update",$delete:"$delete",$replace:"$replace",$deleteAll:"$deleteAll"},autoFetch:!0},s=function(){function t(e,n){if(o()(this,t),this.components=e,this.options=u()(f,n),this.database=n.database,this.options.localforage||(this.options.localforage={name:this.options.name}),!n.database)throw new Error("database option is required to initialise!")}return a()(t,[{key:"getModelFromState",value:function(t){var e=this.database.entities.find((function(e){return e.name===t.$name}));return e&&e.model}},{key:"getModelByEntity",value:function(t){return _find(this.database.entities,{name:t}).model}}],[{key:"setup",value:function(e,n){return this.instance=new t(e,n),this.instance}},{key:"getInstance",value:function(){return this.instance}}]),t}(),l=n(10),d=n.n(l),h=function(){function t(){o()(this,t)}return a()(t,null,[{key:"transformModel",value:function(t){return t.localforage=u()({storeName:t.entity},t.localforage||{}),t.$localStore=d.a.createInstance(u()(s.getInstance().options.localforage,t.localforage)),t}},{key:"getRecordKey",value:function(t){return"string"==typeof t.$id?t.$id:String(t.$id)}}]),t}(),p=n(2),v=n.n(p),y=n(4),b=n.n(y),m=n(5),g=n.n(m),_=n(6),w=n.n(_),S=n(3),I=n.n(S);function E(t){var e=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],(function(){}))),!0}catch(t){return!1}}();return function(){var n,r=I()(t);if(e){var o=I()(this).constructor;n=Reflect.construct(r,arguments,o)}else n=r.apply(this,arguments);return w()(this,n)}}var x=function(t){g()(r,t);var e,n=E(r);function r(){return o()(this,r),n.apply(this,arguments)}return a()(r,null,[{key:"call",value:(e=b()(v.a.mark((function t(e){var n,r,o,i,a;return v.a.wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return n=e.state,r=e.dispatch,o=s.getInstance(),i=o.getModelFromState(n),a=[],t.abrupt("return",i.$localStore.iterate((function(t){a.push(t)})).then((function(){return r("insertOrUpdate",{data:a})})));case 5:case"end":return t.stop()}}),t)}))),function(t){return e.apply(this,arguments)})}]),r}(h),j=function(){function t(){o()(this,t)}return a()(t,null,[{key:"isFieldAttribute",value:function(t){return t instanceof s.getInstance().components.Attribute}},{key:"getPersistableFields",value:function(e){var n=e.getFields();return Object.keys(n).filter((function(e){return t.isFieldAttribute(n[e])}))}}]),t}();function O(t){var e=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],(function(){}))),!0}catch(t){return!1}}();return function(){var n,r=I()(t);if(e){var o=I()(this).constructor;n=Reflect.construct(r,arguments,o)}else n=r.apply(this,arguments);return w()(this,n)}}var R=function(t){g()(r,t);var e,n=O(r);function r(){return o()(this,r),n.apply(this,arguments)}return a()(r,null,[{key:"call",value:(e=b()(v.a.mark((function t(e){var n,o;return v.a.wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return n=e.state,o=e.dispatch,t.abrupt("return",o("deleteAll").then(function(){var t=b()(v.a.mark((function t(e){return v.a.wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return r.clearDB(n),t.abrupt("return",e);case 2:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}()));case 2:case"end":return t.stop()}}),t)}))),function(t){return e.apply(this,arguments)})},{key:"clearDB",value:function(t){s.getInstance().getModelFromState(t).$localStore.clear()}}]),r}(h);function A(t){var e=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],(function(){}))),!0}catch(t){return!1}}();return function(){var n,r=I()(t);if(e){var o=I()(this).constructor;n=Reflect.construct(r,arguments,o)}else n=r.apply(this,arguments);return w()(this,n)}}var N=function(t){g()(r,t);var e,n=A(r);function r(){return o()(this,r),n.apply(this,arguments)}return a()(r,null,[{key:"call",value:(e=b()(v.a.mark((function t(e,n){var r,o,i,a=this,c=arguments;return v.a.wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return r=e.state,o=e.dispatch,i=c.length>2&&void 0!==c[2]?c[2]:"insertOrUpdate",t.abrupt("return",o(i,n).then((function(t){var e=[];return Array.isArray(t)?e=t:"function"==typeof t.$self?e.push(t):Object.keys(t).forEach((function(n){t[n].forEach((function(t){e.push(t)}))})),"create"===i&&R.clearDB(r),Promise.all(e.map((function(t){var e=t.$self(),n=a.getRecordKey(t),r=j.getPersistableFields(e).reduce((function(e,n){return e[n]=t[n],e}),{});return e.$localStore.setItem(n,r)})))})));case 3:case"end":return t.stop()}}),t)}))),function(t,n){return e.apply(this,arguments)})},{key:"create",value:function(t,e){return this.call(t,e)}},{key:"update",value:function(t,e){var n=e.where?"update":"insertOrUpdate";return this.call(t,e,n)}},{key:"replace",value:function(t,e){return this.call(t,e,"create")}}]),r}(h),k=n(8),D=n.n(k);function L(t){var e=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],(function(){}))),!0}catch(t){return!1}}();return function(){var n,r=I()(t);if(e){var o=I()(this).constructor;n=Reflect.construct(r,arguments,o)}else n=r.apply(this,arguments);return w()(this,n)}}var M=function(t){g()(r,t);var e,n=L(r);function r(){return o()(this,r),n.apply(this,arguments)}return a()(r,null,[{key:"call",value:(e=b()(v.a.mark((function t(e,n){var r,o,i,a,c;return v.a.wrap((function(t){for(;;)switch(t.prev=t.next){case 0:if(r=e.state,o=e.dispatch,i=s.getInstance(),a=i.getModelFromState(r),!(c="object"===D()(n)?n.id:n)){t.next=6;break}return t.abrupt("return",a.$localStore.getItem(c).then((function(t){return o("insertOrUpdate",{data:t})})));case 6:return t.abrupt("return",null);case 7:case"end":return t.stop()}}),t)}))),function(t,n){return e.apply(this,arguments)})}]),r}(h);function P(t){var e=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],(function(){}))),!0}catch(t){return!1}}();return function(){var n,r=I()(t);if(e){var o=I()(this).constructor;n=Reflect.construct(r,arguments,o)}else n=r.apply(this,arguments);return w()(this,n)}}var B=function(t){g()(r,t);var e,n=P(r);function r(){return o()(this,r),n.apply(this,arguments)}return a()(r,null,[{key:"call",value:(e=b()(v.a.mark((function t(e,n){var r,o,i=this;return v.a.wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return r=e.state,o=e.dispatch,t.abrupt("return",o("delete",n).then(function(){var t=b()(v.a.mark((function t(e){var n,o,a;return v.a.wrap((function(t){for(;;)switch(t.prev=t.next){case 0:if(!e){t.next=6;break}return n=s.getInstance(),o=n.getModelFromState(r),a=Array.isArray(e)?e:[e],t.next=6,Promise.all(a.map((function(t){var e=i.getRecordKey(t);return o.$localStore.removeItem(e)})));case 6:return t.abrupt("return",e);case 7:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}()));case 2:case"end":return t.stop()}}),t)}))),function(t,n){return e.apply(this,arguments)})}]),r}(h),F=function(){function t(e,n){o()(this,t),s.setup(e,n),this.setupActions(),this.setupModels()}return a()(t,[{key:"setupActions",value:function(){var t=s.getInstance(),e=t.options.actions;t.components.Actions[e.$get]=M.call.bind(M),t.components.Actions[e.$fetch]=x.call.bind(x),t.components.Actions[e.$create]=N.create.bind(N),t.components.Actions[e.$update]=N.update.bind(N),t.components.Actions[e.$replace]=N.replace.bind(N),t.components.Actions[e.$delete]=B.call.bind(B),t.components.Actions[e.$deleteAll]=R.call.bind(R)}},{key:"setupModels",value:function(){var t=s.getInstance(),e=t.options.actions;t.database.entities.forEach((function(t){t.model=h.transformModel(t.model)})),t.components.Model[e.$fetch]=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return this.dispatch(e.$fetch,t)},t.components.Model[e.$get]=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return this.dispatch(e.$get,t)},t.components.Model[e.$create]=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return this.dispatch(e.$create,t)},t.components.Model[e.$update]=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return this.dispatch(e.$update,t)},t.components.Model[e.$replace]=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return this.dispatch(e.$replace,t)},t.components.Model[e.$delete]=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return this.dispatch(e.$delete,t)},t.components.Model[e.$deleteAll]=function(){return this.dispatch(e.$deleteAll)}}}]),t}(),T=function(){function t(){o()(this,t)}return a()(t,null,[{key:"install",value:function(t,e){return new F(t,e)}}]),t}();e.default=T}]).default})); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuex-orm-localforage", 3 | "version": "0.3.2", 4 | "description": "Vuex-ORM Plugin to sync the data against an indexedDB using localforage.", 5 | "main": "dist/vuex-orm-localforage.js", 6 | "module": "src/index.js", 7 | "scripts": { 8 | "watch": "webpack --watch --mode=development", 9 | "build": "webpack --mode=production", 10 | "lint": "eslint src/**/*.js test/**/*.js", 11 | "test": "jest" 12 | }, 13 | "repository": { 14 | "type": "git+https://github.com/eldomagan/vuex-orm-localforage.git" 15 | }, 16 | "keywords": [ 17 | "vue", 18 | "vuex", 19 | "vuex-plugin", 20 | "vuex-orm", 21 | "vuex-orm-plugin", 22 | "localforage", 23 | "localstorage" 24 | ], 25 | "author": "Eldo Magan ", 26 | "license": "MIT", 27 | "jest": { 28 | "transform": { 29 | "^.+\\.jsx?$": "babel-jest" 30 | } 31 | }, 32 | "dependencies": { 33 | "deepmerge": "^4.2.2", 34 | "localforage": "^1.7.3" 35 | }, 36 | "devDependencies": { 37 | "@babel/core": "^7.5.5", 38 | "@babel/plugin-transform-runtime": "^7.5.5", 39 | "@babel/preset-env": "^7.5.5", 40 | "@babel/runtime": "^7.5.5", 41 | "@vuex-orm/core": "^0.36.3", 42 | "babel-jest": "^24.9.0", 43 | "babel-loader": "^8.0.6", 44 | "babel-preset-env": "^1.7.0", 45 | "eslint": "^6.3.0", 46 | "eslint-config-airbnb": "^18.0.1", 47 | "eslint-plugin-import": "^2.18.2", 48 | "eslint-plugin-jest": "^22.17.0", 49 | "jest": "^24.9.0", 50 | "webpack": "^4.39.3", 51 | "webpack-cli": "^3.3.8", 52 | "webpack-node-externals": "^1.7.2" 53 | }, 54 | "peerDependencies": { 55 | "@vuex-orm/core": "^0.36.3" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/actions/Action.js: -------------------------------------------------------------------------------- 1 | import deepmerge from 'deepmerge'; 2 | import localforage from 'localforage'; 3 | import Context from '../common/context'; 4 | 5 | export default class Action { 6 | /** 7 | * Transform Model to include ModelConfig 8 | * @param {object} model 9 | */ 10 | static transformModel(model) { 11 | model.localforage = deepmerge({ storeName: model.entity }, model.localforage || {}); 12 | 13 | model.$localStore = localforage.createInstance(deepmerge( 14 | Context.getInstance().options.localforage, 15 | model.localforage, 16 | )); 17 | 18 | return model; 19 | } 20 | 21 | static getRecordKey(record) { 22 | return typeof record.$id === 'string' ? record.$id : String(record.$id); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/actions/Destroy.js: -------------------------------------------------------------------------------- 1 | import Action from './Action'; 2 | import Context from '../common/context'; 3 | 4 | export default class Destroy extends Action { 5 | /** 6 | * Is Called after new model deletion from the store 7 | * 8 | * @param {object} record 9 | * @param {string} entityName 10 | */ 11 | static async call({ state, dispatch }, payload) { 12 | return dispatch('delete', payload).then(async (result) => { 13 | if (result) { 14 | const context = Context.getInstance(); 15 | const model = context.getModelFromState(state); 16 | const records = Array.isArray(result) ? result : [result]; 17 | 18 | await Promise.all(records.map((record) => { 19 | const key = this.getRecordKey(record); 20 | return model.$localStore.removeItem(key); 21 | })); 22 | } 23 | 24 | return result; 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/actions/DestroyAll.js: -------------------------------------------------------------------------------- 1 | import Action from './Action'; 2 | import Context from '../common/context'; 3 | 4 | export default class DestroyAll extends Action { 5 | /** 6 | * Is Called after new model deletion from the store 7 | * 8 | * @param {object} record 9 | * @param {string} entityName 10 | */ 11 | static async call({ state, dispatch }) { 12 | return dispatch('deleteAll').then(async (result) => { 13 | DestroyAll.clearDB(state); 14 | return result; 15 | }); 16 | } 17 | 18 | static clearDB(state) { 19 | const context = Context.getInstance(); 20 | const model = context.getModelFromState(state); 21 | model.$localStore.clear(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/actions/Fetch.js: -------------------------------------------------------------------------------- 1 | import Action from './Action'; 2 | import Context from '../common/context'; 3 | 4 | export default class Fetch extends Action { 5 | /** 6 | * Call $fetch method 7 | * @param {object} store 8 | * @param {object} params 9 | */ 10 | static async call({ state, dispatch }) { 11 | const context = Context.getInstance(); 12 | const model = context.getModelFromState(state); 13 | 14 | const records = []; 15 | 16 | return model.$localStore.iterate((record) => { 17 | records.push(record); 18 | }).then(() => dispatch('insertOrUpdate', { 19 | data: records, 20 | })); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/actions/Get.js: -------------------------------------------------------------------------------- 1 | import Action from './Action'; 2 | import Context from '../common/context'; 3 | 4 | export default class Get extends Action { 5 | /** 6 | * Call $fetch method 7 | * @param {object} store 8 | * @param {object} params 9 | */ 10 | static async call({ state, dispatch }, params) { 11 | const context = Context.getInstance(); 12 | const model = context.getModelFromState(state); 13 | const id = typeof params === 'object' ? params.id : params; 14 | 15 | if (id) { 16 | return model.$localStore.getItem(id) 17 | .then(record => dispatch('insertOrUpdate', { 18 | data: record, 19 | })); 20 | } 21 | 22 | return null; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/actions/Persist.js: -------------------------------------------------------------------------------- 1 | import Action from './Action'; 2 | import Model from '../orm/Model'; 3 | import DestroyAll from './DestroyAll'; 4 | 5 | export default class Persist extends Action { 6 | /** 7 | * Is called when an item is inserted or updated in the store 8 | * 9 | * @param {object} store 10 | * @param {object} payload 11 | */ 12 | static async call({ state, dispatch }, payload, action = 'insertOrUpdate') { 13 | return dispatch(action, payload).then((result) => { 14 | let records = []; 15 | 16 | if (Array.isArray(result)) { 17 | records = result; 18 | } else if (typeof result.$self === 'function') { // FIX: instance of Vuex Model is not working 19 | records.push(result); 20 | } else { 21 | Object.keys(result).forEach((entity) => { 22 | result[entity].forEach((record) => { 23 | records.push(record); 24 | }); 25 | }); 26 | } 27 | 28 | if (action === 'create') { 29 | DestroyAll.clearDB(state); 30 | } 31 | 32 | return Promise.all(records.map((record) => { 33 | const model = record.$self(); 34 | const key = this.getRecordKey(record); 35 | const data = Model.getPersistableFields(model).reduce((obj, field) => { 36 | obj[field] = record[field]; 37 | return obj; 38 | }, {}); 39 | 40 | return model.$localStore.setItem(key, data); 41 | })); 42 | }); 43 | } 44 | 45 | static create(context, payload) { 46 | return this.call(context, payload); 47 | } 48 | 49 | static update(context, payload) { 50 | const vuexAction = payload.where ? 'update' : 'insertOrUpdate'; 51 | return this.call(context, payload, vuexAction); 52 | } 53 | 54 | static replace(context, payload) { 55 | return this.call(context, payload, 'create'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/common/context.js: -------------------------------------------------------------------------------- 1 | import deepmerge from 'deepmerge'; 2 | import { VuexOrmPluginConfig } from '../support/interfaces'; 3 | 4 | export default class Context { 5 | /** 6 | * Private constructor, called by the setup method 7 | * 8 | * @constructor 9 | * @param {Components} components The Vuex-ORM Components collection 10 | * @param {VuexOrmPluginConfig} options The options passed to VuexORM.install 11 | */ 12 | constructor(components, options) { 13 | this.components = components; 14 | this.options = deepmerge(VuexOrmPluginConfig, options); 15 | this.database = options.database; 16 | 17 | if (!this.options.localforage) { 18 | this.options.localforage = { 19 | name: this.options.name, 20 | }; 21 | } 22 | 23 | if (!options.database) { 24 | throw new Error('database option is required to initialise!'); 25 | } 26 | } 27 | 28 | /** 29 | * This is called only once and creates a new instance of the Context. 30 | * @param {Components} components The Vuex-ORM Components collection 31 | * @param {VuexOrmPluginConfig} options The options passed to VuexORM.install 32 | * @returns {Context} 33 | */ 34 | static setup(components, options) { 35 | this.instance = new Context(components, options); 36 | return this.instance; 37 | } 38 | 39 | /** 40 | * Get the singleton instance of the context. 41 | * @returns {Context} 42 | */ 43 | static getInstance() { 44 | return this.instance; 45 | } 46 | 47 | /** 48 | * Get Model from State 49 | * @param {object} state 50 | */ 51 | getModelFromState(state) { 52 | const entity = this.database.entities.find((e) => e.name === state.$name); 53 | return entity && entity.model; 54 | } 55 | 56 | /** 57 | * Get model by entity 58 | * @param {Object} entity 59 | */ 60 | getModelByEntity(entity) { 61 | return _find(this.database.entities, { 62 | name: entity, 63 | }).model; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import VuexOrmLocalForage from './vuex-orm-localforage'; 2 | 3 | class VuexOrmLocalForagePlugin { 4 | /** 5 | * This is called, when VuexORM.install(VuexOrmLocalForage, options) is called. 6 | * 7 | * @param {Components} components The Vuex-ORM Components collection 8 | * @param {VuexOrmPluginConfig} options The options passed to VuexORM.install 9 | * @returns {VuexOrmLocalForage} 10 | */ 11 | static install(components, options) { 12 | return new VuexOrmLocalForage(components, options); 13 | } 14 | } 15 | 16 | export default VuexOrmLocalForagePlugin; 17 | -------------------------------------------------------------------------------- /src/orm/Model.js: -------------------------------------------------------------------------------- 1 | import Context from '../common/context'; 2 | 3 | export default class Model { 4 | /** 5 | * Tells if a field is a attribute (and thus not a relation) 6 | * @param {Field} field 7 | * @returns {boolean} 8 | */ 9 | static isFieldAttribute(field) { 10 | const context = Context.getInstance(); 11 | return field instanceof context.components.Attribute; 12 | } 13 | 14 | static getPersistableFields(model) { 15 | const fields = model.getFields(); 16 | return Object.keys(fields).filter((key) => Model.isFieldAttribute(fields[key])); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/support/interfaces.js: -------------------------------------------------------------------------------- 1 | import { Database } from '@vuex-orm/core'; 2 | 3 | export const VuexOrmPluginConfig = { 4 | /** 5 | * Default VuexORM Database 6 | * @param {Database} Instance of VuexORM database 7 | */ 8 | database: new Database(), 9 | 10 | /** 11 | * @param {string} Default DataStore prefix 12 | */ 13 | name: 'vuex', // Keep for backward compatibilities 14 | 15 | /** 16 | * @param {object} localforage config 17 | */ 18 | localforage: { 19 | name: 'vuex', 20 | }, 21 | 22 | /** 23 | * 24 | */ 25 | actions: { 26 | $fetch: '$fetch', 27 | $get: '$get', 28 | $create: '$create', 29 | $update: '$update', 30 | $delete: '$delete', 31 | $replace: '$replace', 32 | $deleteAll: '$deleteAll', 33 | }, 34 | 35 | /** 36 | * @param {boolean} Load data from LocalForage on startup 37 | */ 38 | autoFetch: true, 39 | }; 40 | 41 | export default { 42 | VuexOrmPluginConfig, 43 | }; 44 | -------------------------------------------------------------------------------- /src/vuex-orm-localforage.js: -------------------------------------------------------------------------------- 1 | import Context from './common/context'; 2 | import Action from './actions/Action'; 3 | import Fetch from './actions/Fetch'; 4 | import Persist from './actions/Persist'; 5 | import Get from './actions/Get'; 6 | import Destroy from './actions/Destroy'; 7 | import DestroyAll from './actions/DestroyAll'; 8 | 9 | export default class VuexOrmLocalForage { 10 | /** 11 | * @constructor 12 | * @param {Components} components The Vuex-ORM Components collection 13 | * @param {VuexOrmPluginConfig} options The options passed to VuexORM.install 14 | */ 15 | constructor(components, options) { 16 | Context.setup(components, options); 17 | this.setupActions(); 18 | this.setupModels(); 19 | } 20 | 21 | /** 22 | * This method will setup following Vuex actions: $fetch, $get 23 | */ 24 | setupActions() { 25 | const context = Context.getInstance(); 26 | const { actions } = context.options; 27 | 28 | context.components.Actions[actions.$get] = Get.call.bind(Get); 29 | context.components.Actions[actions.$fetch] = Fetch.call.bind(Fetch); 30 | context.components.Actions[actions.$create] = Persist.create.bind(Persist); 31 | context.components.Actions[actions.$update] = Persist.update.bind(Persist); 32 | context.components.Actions[actions.$replace] = Persist.replace.bind(Persist); 33 | context.components.Actions[actions.$delete] = Destroy.call.bind(Destroy); 34 | context.components.Actions[actions.$deleteAll] = DestroyAll.call.bind(DestroyAll); 35 | } 36 | 37 | /** 38 | * This method will setup following model methods: Model.$fetch, Model.$get, Model.$create, 39 | * Model.$update, Model.$delete 40 | */ 41 | setupModels() { 42 | const context = Context.getInstance(); 43 | const { actions } = context.options; 44 | 45 | /** 46 | * Transform Model and Modules 47 | */ 48 | context.database.entities.forEach((entity) => { 49 | entity.model = Action.transformModel(entity.model); 50 | }); 51 | 52 | context.components.Model[actions.$fetch] = function fetchFromLocalStore(payload = {}) { 53 | return this.dispatch(actions.$fetch, payload); 54 | }; 55 | 56 | context.components.Model[actions.$get] = function getFromLocalStore(payload = {}) { 57 | return this.dispatch(actions.$get, payload); 58 | }; 59 | 60 | context.components.Model[actions.$create] = function insertIntoLocalStore(payload = {}) { 61 | return this.dispatch(actions.$create, payload); 62 | }; 63 | 64 | context.components.Model[actions.$update] = function updateToLocalStore(payload = {}) { 65 | return this.dispatch(actions.$update, payload); 66 | }; 67 | 68 | context.components.Model[actions.$replace] = function replaceLocalStore(payload = {}) { 69 | return this.dispatch(actions.$replace, payload); 70 | }; 71 | 72 | context.components.Model[actions.$delete] = function deleteFromLocalStore(payload = {}) { 73 | return this.dispatch(actions.$delete, payload); 74 | }; 75 | 76 | context.components.Model[actions.$deleteAll] = function clearLocalStore() { 77 | return this.dispatch(actions.$deleteAll); 78 | }; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const nodeExternals = require('webpack-node-externals'); 3 | 4 | module.exports = { 5 | entry: './src/index.js', 6 | target: 'node', 7 | externals: [nodeExternals({ 8 | whitelist: ['deepmerge', 'localforage', /@babel\/runtime/, 'regenerator-runtime'], 9 | })], 10 | output: { 11 | library: 'VuexORMLocalForage', 12 | libraryTarget: 'umd', 13 | path: path.resolve(__dirname, 'dist'), 14 | filename: 'vuex-orm-localforage.js', 15 | libraryExport: 'default', 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.js$/, 21 | exclude: /node_modules/, 22 | use: { 23 | loader: 'babel-loader', 24 | }, 25 | }, 26 | ], 27 | }, 28 | }; 29 | --------------------------------------------------------------------------------