├── .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 | [](https://standardjs.com)
2 | [](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 |
62 |
70 |
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 |
--------------------------------------------------------------------------------