├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── examples ├── example.coffee ├── example.es6 ├── example.ts └── filedb.es6 ├── index.d.ts ├── package.json ├── src ├── index.coffee └── with-tv4.coffee └── test ├── test.coffee └── test.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bundle.js 3 | test/test.js 4 | examples/example.js 5 | index.js 6 | with-tv4.js 7 | examples/item.json 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | examples/ 3 | test/ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 mizchi 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StoneSkin 2 | 3 | Isomorphic IndexedDb and in-memory db wrapper with jsonschema validation. 4 | 5 | ``` 6 | $ npm install stone-skin --save 7 | ``` 8 | 9 | Inspired by [mWater/minimongo](https://github.com/mWater/minimongo "mWater/minimongo"). And based on thin indexedDb wrapper [mizchi/idb-wrapper-promisify](https://github.com/mizchi/idb-wrapper-promisify "mizchi/idb-wrapper-promisify") 10 | 11 | ## Features 12 | 13 | - ActiveRecord like API 14 | - Universal indexedDb or in-memory object 15 | - Promisified 16 | - Runnable in shared-worker and service-worker 17 | - (optional) validation by jsonschema(tv4) 18 | - Selectable target 19 | - IndexedDb(browser) 20 | - LocalStorageDb(browser) 21 | - FileDb(node) 22 | - MemoryDb(universal) 23 | 24 | FileDb and LocalStorageDb do just only serialization to json/string. Don't use them with big data. 25 | 26 | ## Example 27 | 28 | with babel(>=4.7.8) async/await (babel --experimental) 29 | 30 | ```js 31 | global.Promise = require('bluebird'); 32 | 33 | import "babel/polyfill"; 34 | import StoneSkin from 'stone-skin/with-tv4'; 35 | 36 | class ItemStore extends StoneSkin.IndexedDb { 37 | storeName: 'Item'; 38 | schema: { 39 | properties: { 40 | title: { 41 | type: 'string' 42 | } 43 | } 44 | } 45 | } 46 | 47 | let itemStore = new ItemStore(); 48 | (async () => { 49 | await itemStore.ready; 50 | await itemStore.save({title: 'foo', _id: 'xxx'}); 51 | let item = await itemStore.find('xxx'); 52 | console.log(item); 53 | let items = await itemStore.all(); 54 | console.log(items); 55 | })(); 56 | ``` 57 | 58 | with coffee 59 | 60 | ```coffee 61 | StoneSkin = require('stone-skin/with-tv4') 62 | 63 | class Item extends StoneSkin.IndexedDb 64 | storeName: 'Item' 65 | schema: 66 | properties: 67 | title: 68 | type: 'string' 69 | body: 70 | type: 'string' 71 | 72 | item = new Item 73 | item.ready 74 | .then -> 75 | item.clear() 76 | .then -> 77 | item.all() 78 | .then (items) -> 79 | console.log items 80 | .then -> 81 | item.save { 82 | _id: 'xxx' 83 | title: 'test2' 84 | body: 'hello' 85 | } 86 | .then -> 87 | item.save [ 88 | { 89 | _id: 'yyy' 90 | title: 'test1' 91 | body: 'hello' 92 | } 93 | ] 94 | .then -> 95 | item.all() 96 | .then (items) -> 97 | console.log items 98 | item.remove 'xxx' 99 | .then -> 100 | item.all() 101 | .then (items) -> 102 | console.log items 103 | ``` 104 | 105 | with TypeScript 106 | 107 | ```js 108 | /// 109 | var StoneSkin = require('stone-skin/with-tv4'); 110 | 111 | interface ItemSchema = { 112 | _id: string; 113 | title: string; 114 | }; 115 | 116 | class Item extends StoneSkin { 117 | // ... 118 | } 119 | ``` 120 | 121 | See detail [stone-skin.d.ts](stone-skin.d.ts)) 122 | 123 | ## Promisified Db API 124 | 125 | `StoneSkin.IndexedDb` and `StoneSkin.MemoryDb` have same API 126 | 127 | - `ready: Promise`: return resolved promise if indexedDb ready. 128 | - `find(id: Id): Promise`: get first item by id 129 | - `select(fn: (t: T) => boolean): Promise`: filtered items by function 130 | - `first(fn: (t: T) => boolean): Promise`: get first item from filtered items 131 | - `last(fn: (t: T) => boolean): Promise`: get last item from filtered items 132 | - `all(): Promise`: return all items 133 | - `clear(): Promise`: remove all items 134 | - `save(t: T): Promise`: save item 135 | - `save(ts: T[]): Promise`: save items 136 | - `remove(id: Id): Promise`: remove item 137 | - `remove(ids: Id[]): Promise`: remove items 138 | 139 | ## `StoneSkin.IndexedDb` 140 | 141 | - `storeName: string;` You need to set this value when you extend it. 142 | - `StoneSkin.IndexedDb.prototype.toMemoryDb(): StoneSkin.MemoryDb`: return memory db by its items 143 | - `StoneSkin.IndexedDb.prototype.toSyncedMemoryDb(): StoneSkin.SyncedMemoryDb`: return synced memory db by its items. 144 | 145 | ## `StoneSkin.FileDb` 146 | 147 | - `filepath: string;` You need to set this value when you extend it. 148 | 149 | ## `StoneSkin.LocalStorageDb` 150 | 151 | - `key: string;` You need to set this value when you extend it. 152 | 153 | ## `StoneSkin.SyncedMemoryDb` 154 | 155 | It has almost same API without Promise wrap. 156 | 157 | ## Migration helper 158 | 159 | - `StoneSkin.utils.setupWithMigrate(currentVersion: number)`; 160 | 161 | ```coffee 162 | StoneSkin.utils.setupWithMigrate 3, 163 | initialize: -> 164 | console.log 'init' # fire at only first 165 | '1to2': -> 166 | console.log 'exec 1 to 2' # fire if last setup version is 1 167 | '2to3': -> 168 | console.log 'exec 2 to 3' # fire it if last setup version is 1 or 2 169 | ``` 170 | 171 | Need localStorage to save last version. It only works on browser. 172 | 173 | ## LICENSE 174 | 175 | MIT 176 | -------------------------------------------------------------------------------- /examples/example.coffee: -------------------------------------------------------------------------------- 1 | global.StoneSkin = require '../src/with-tv4' 2 | global.Promise = require 'bluebird' 3 | 4 | # window.addEventListener 'DOMContentLoaded', -> 5 | # document.body.innerHTML = 'Hello' 6 | 7 | do -> 8 | # class Item extends StoneSkin.IndexedDb 9 | class Item extends StoneSkin.MemoryDb 10 | storeName: 'Item' 11 | schema: 12 | properties: 13 | title: 14 | type: 'string' 15 | body: 16 | type: 'string' 17 | 18 | item = new Item 19 | item.ready 20 | .then -> 21 | item.clear() 22 | .then -> 23 | item.all() 24 | .then (items) -> 25 | console.log items 26 | .then -> 27 | item.save { 28 | _id: 'xxx' 29 | title: 'test2' 30 | body: 'hello' 31 | } 32 | .then -> 33 | item.save [ 34 | { 35 | _id: 'yyy' 36 | title: 'test1' 37 | body: 'hello' 38 | } 39 | ] 40 | .then -> 41 | item.all() 42 | .then (items) -> 43 | console.log items 44 | item.remove 'xxx' 45 | .then -> 46 | item.all() 47 | 48 | .then (items) -> 49 | console.log items 50 | console.log '---------' 51 | item.fetch 'unknown key' 52 | .then (i) -> 53 | console.log 'should not come here', i 54 | 55 | .catch (e) -> 56 | console.log 'should come here' 57 | -------------------------------------------------------------------------------- /examples/example.es6: -------------------------------------------------------------------------------- 1 | global.Promise = require('bluebird'); 2 | 3 | import "babel/polyfill"; 4 | import StoneSkin from '../with-tv4'; 5 | 6 | class ItemStore extends StoneSkin.MemoryDb { 7 | storeName: 'Item'; 8 | schema: { 9 | properties: { 10 | title: { 11 | type: 'string' 12 | } 13 | } 14 | } 15 | } 16 | 17 | let itemStore = new ItemStore(); 18 | (async () => { 19 | await itemStore.ready; 20 | await itemStore.save({title: 'foo', _id: 'xxx'}); 21 | let item = await itemStore.find('xxx'); 22 | console.log(item); 23 | let items = await itemStore.all(); 24 | console.log(items); 25 | })(); 26 | -------------------------------------------------------------------------------- /examples/example.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // run: tsc -t es6 example.ts; babel-node example.js 3 | type Id = StoneSkin.Id; 4 | declare var require: any; 5 | declare var global: any; 6 | 7 | global.StoneSkin = require('../with-tv4'); 8 | 9 | interface FooSchema { 10 | name: string; 11 | } 12 | 13 | class FooStore extends StoneSkin.MemoryDb { 14 | } 15 | 16 | class BarStore extends StoneSkin.MemoryDb<{ 17 | fooId: Id; 18 | name: string; 19 | }> { 20 | } 21 | 22 | const foo = new FooStore(); 23 | const bar = new BarStore(); 24 | foo.save({name: "foo"}) 25 | .then(i => { 26 | console.log(i); 27 | return foo.find(i._id); 28 | }) 29 | .then(foo => { 30 | return bar.save({ 31 | fooId: foo._id, 32 | name: 'it\'s bar' 33 | }) 34 | }) 35 | .then(bar => { 36 | console.log(bar); 37 | }) 38 | -------------------------------------------------------------------------------- /examples/filedb.es6: -------------------------------------------------------------------------------- 1 | // import "babel/polyfill"; 2 | import StoneSkin from '../with-tv4'; 3 | 4 | class ItemStore extends StoneSkin.FileDb { 5 | get filepath() { return process.cwd() + '/item.json';} 6 | } 7 | 8 | let itemStore = new ItemStore(); 9 | itemStore.ready.then(() => { 10 | return itemStore.save({a: 1}); 11 | }) 12 | .then(item => { 13 | console.log("saved", item); 14 | return itemStore.all(); 15 | }) 16 | .then(items => { 17 | console.log(items); 18 | }) 19 | .catch(e =>{ 20 | console.log(e); 21 | }); 22 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module StoneSkin { 2 | interface Id {} 3 | 4 | class Base { 5 | validate(t: T): boolean; 6 | } 7 | 8 | interface __WithId { 9 | _id: Id; 10 | } 11 | 12 | class Async extends Base { 13 | ready: Promise; 14 | find(id: Id>) : Promise<(T & __WithId)>; 15 | find(ids: Id>[]): Promise<(T & __WithId)[]>; 16 | fetch(id: Id>): Promise>; 17 | select(fn: (t: T) => boolean): Promise<(T & __WithId)[]>; 18 | first(fn: (t: T) => boolean): Promise>; 19 | last(fn: (t: T) => boolean): Promise>; 20 | all(): Promise<(T & __WithId)[]>; 21 | clear(): Promise; 22 | save(t: T): Promise>; 23 | save(ts: T[]): Promise<(T & __WithId)[]>; 24 | remove(id: Id>): Promise; 25 | remove(ids: Id>[]): Promise; 26 | } 27 | 28 | class ImmutableLoader extends Base { 29 | find(id: Id): T & __WithId; 30 | fetch(id: Id): T & __WithId; 31 | select(fn: (t: T) => boolean): T & __WithId[]; 32 | all(): (T & __WithId)[]; 33 | } 34 | 35 | class Synced extends Base { 36 | find(id: Id>): T & __WithId; 37 | find(ids: Id>[]): (T & __WithId)[]; 38 | fetch(id: Id>): T & __WithId; 39 | select(fn: (t: T) => boolean): T & __WithId[]; 40 | first(fn: (t: T) => boolean): T & __WithId; 41 | last(fn: (t: T) => boolean): T & __WithId; 42 | all(): (T & __WithId)[]; 43 | clear(): void; 44 | save(t: T): T & __WithId; 45 | save(ts: T[]): (T & __WithId)[]; 46 | remove(id: Id>): void; 47 | remove(ids: Id>[]): void; 48 | } 49 | 50 | export class IndexedDb extends Async { 51 | toMemoryDb(): MemoryDb; 52 | toSyncedMemoryDb(): SyncedMemoryDb; 53 | } 54 | 55 | export class LocalStorageDb extends Async { 56 | key: string; 57 | } 58 | 59 | export class FileDb extends Async { 60 | filename: string; 61 | } 62 | 63 | export class MemoryDb extends Async {} 64 | export class SyncedMemoryDb extends Synced {} 65 | } 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stone-skin", 3 | "version": "0.5.2", 4 | "description": "Isomorphic IndexedDb and Memory data wrapper", 5 | "main": "index", 6 | "scripts": { 7 | "test": "browserify -t coffeeify --extension='.coffee' test/test.coffee -o test/test.js;open test/test.html", 8 | "prepublish": "$(npm bin)/coffee -o . -c src/*.coffee" 9 | }, 10 | "author": "mizchi", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "babel": "^4.7.12", 14 | "bluebird": "^2.9.14", 15 | "chai": "^2.1.0", 16 | "coffee-script": "^1.9.1", 17 | "coffeeify": "^1.0.0", 18 | "mocha": "^2.1.0" 19 | }, 20 | "dependencies": { 21 | "clone": "^1.0.0", 22 | "idb-wrapper-promisify": "^2.1.1", 23 | "node-uuid": "^1.4.2", 24 | "tv4": "^1.1.9" 25 | }, 26 | "directories": { 27 | "example": "examples", 28 | "test": "test" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/mizchi/stone-skin.git" 33 | }, 34 | "bugs": { 35 | "url": "https://github.com/mizchi/stone-skin/issues" 36 | }, 37 | "homepage": "https://github.com/mizchi/stone-skin", 38 | "keywords": [ 39 | "indexeddb" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /src/index.coffee: -------------------------------------------------------------------------------- 1 | Store = require 'idb-wrapper-promisify' 2 | uuid = require 'node-uuid' 3 | clone = require 'clone' 4 | 5 | r = if require? then require else null 6 | 7 | module.exports = StoneSkin = {} 8 | 9 | StoneSkin.validate = (data, schema)-> 10 | console.warn 'StoneSkin.validate is not set' 11 | true 12 | 13 | class StoneSkin.Base 14 | name: null 15 | constructor: -> 16 | save: -> throw new Error 'you should override' 17 | find: -> throw new Error 'you should override' 18 | first: -> throw new Error 'you should override' 19 | last: -> throw new Error 'you should override' 20 | select: -> throw new Error 'you should override' 21 | clear: -> throw new Error 'you should override' 22 | 23 | # Internal 24 | _ensureId: (data) -> 25 | if data._id? 26 | data 27 | else 28 | cloned = clone(data) 29 | cloned._id = uuid() 30 | cloned 31 | 32 | validate: (data) -> 33 | StoneSkin.validate(data, @schema) 34 | 35 | createValidateReason: (data) -> 36 | StoneSkin.createValidateReason(data, @schema) 37 | 38 | class StoneSkin.SyncedMemoryDb extends StoneSkin.Base 39 | constructor: -> 40 | super 41 | @_data = [] 42 | 43 | _pushOrUpdate: (data) -> 44 | found = @_find data._id 45 | if !!found 46 | for k, v of data 47 | found[k] = v 48 | return found 49 | else 50 | ensured = @_ensureId(data) 51 | @_data.push ensured 52 | return ensured 53 | 54 | save: (data) -> 55 | existIds = @_data.map (d) -> d._id 56 | if data instanceof Array 57 | # Validate 58 | if @schema and !!@skipValidate is false 59 | for d in data 60 | reason = @createValidateReason(d) 61 | unless reason.valid 62 | throw reason.error 63 | # Save after validate 64 | result = 65 | for i in data 66 | @_pushOrUpdate(i) 67 | return result 68 | else 69 | valid = @validate(data) 70 | unless valid 71 | reason = @createValidateReason(data) 72 | throw reason.error 73 | # console.log("on save", data) 74 | return @_pushOrUpdate(data) 75 | # console.log("on save", ret, data) 76 | 77 | # raw find 78 | _find: (id) -> 79 | for item in @_data 80 | if item._id is id then return item 81 | undefined 82 | 83 | # will wrap 84 | find: (id) -> @_find(id) 85 | 86 | fetch: (id) -> 87 | ret = @_find(id) 88 | if ret? 89 | return ret 90 | else 91 | throw new Error "#{id} entity does not exist" 92 | 93 | remove: (id) -> 94 | if id instanceof Array 95 | @_data = @_data.filter (i) -> i._id not in id 96 | else 97 | @_data = @_data.filter (i) -> i._id isnt id 98 | undefined 99 | 100 | first: (fn) -> 101 | for item in @_data 102 | if fn(item) then return item 103 | undefined 104 | 105 | last: (fn) -> 106 | for item in @_data.reverse() 107 | if fn(item) then return item 108 | undefined 109 | 110 | select: (fn) -> 111 | result = [] 112 | for i in @_data 113 | if fn(i) then result.push(i) 114 | return clone @_data.filter (i) -> fn(i) 115 | 116 | clear: -> @_data.length = 0 117 | all: -> clone(@_data) 118 | 119 | class StoneSkin.ImmutableLoader extends StoneSkin.SyncedMemoryDb 120 | constructor: -> 121 | super 122 | @save(@initialize()) 123 | 124 | class StoneSkin.MemoryDb extends StoneSkin.SyncedMemoryDb 125 | constructor: -> 126 | super 127 | @ready = Promise.resolve() 128 | 129 | # will cause validation error 130 | save: -> 131 | try 132 | Promise.resolve super 133 | catch e 134 | Promise.reject(e) 135 | remove: -> Promise.resolve super 136 | find: -> Promise.resolve super 137 | first: -> Promise.resolve super 138 | select: -> Promise.resolve super 139 | clear: -> Promise.resolve super 140 | all: -> Promise.resolve super 141 | 142 | class StoneSkin.CommitableDb extends StoneSkin.MemoryDb 143 | commit: => throw 'Override me' 144 | # will cause validation error 145 | save: -> Promise.resolve(super).then @commit 146 | remove: -> Promise.resolve super.then @commit 147 | clear: -> Promise.resolve(super).then @commit 148 | 149 | class StoneSkin.FileDb extends StoneSkin.CommitableDb 150 | filepath: null 151 | constructor: -> 152 | super 153 | unless @filepath? 154 | throw new Error "You have to set filepath in FileDb" 155 | 156 | fs = r 'fs' 157 | if fs.existsSync @filepath 158 | @_data = JSON.parse fs.readFileSync(@filepath) 159 | else 160 | @_data = [] 161 | 162 | commit: (ret) => 163 | new Promise (done) => 164 | unless @filepath? 165 | throw new Error "_data is not serializable." 166 | try 167 | jsonstr = JSON.stringify(@_data) 168 | catch e 169 | throw new Error "" 170 | 171 | fs = r 'fs' 172 | fs.writeFile(@filepath, jsonstr, -> done(ret)) 173 | 174 | class StoneSkin.LocalStorageDb extends StoneSkin.CommitableDb 175 | key: null 176 | constructor: -> 177 | super 178 | unless @key? 179 | throw new Error "You have to set key in LocalStorageDb" 180 | unless localStorage? 181 | throw new Error "This envinronment can't touch localStorage" 182 | 183 | if localStorage[@key]? 184 | @_data = JSON.parse localStorage.getItem(@key) 185 | else 186 | @_data = [] 187 | 188 | commit: (ret) => 189 | new Promise (done) => 190 | unless @key? 191 | throw new Error "You have to set key in LocalStorageDb" 192 | try 193 | jsonstr = JSON.stringify(@_data) 194 | catch e 195 | throw new Error "_data is not serializable." 196 | localStorage.setItem(@key, jsonstr) 197 | done(ret) 198 | 199 | 200 | class StoneSkin.IndexedDb extends StoneSkin.Base 201 | keyPath: '_id' 202 | constructor: -> 203 | super 204 | @_store = new Store 205 | storeName: @storeName 206 | keyPath: @keyPath 207 | @ready = @_store.ready 208 | 209 | clear: -> @_store.clear() 210 | 211 | select: (fn) -> 212 | result = [] 213 | @_store.iterate (i) -> 214 | if fn(i) then result.push(i) 215 | .then -> result 216 | 217 | # TODO: skip when cursor finds first item 218 | first: (fn) -> @select(fn).then (items) => items[0] 219 | 220 | last: (fn) -> @select(fn).then (items) => items[items.length - 1] 221 | 222 | # Internal 223 | _saveBatch: (list) -> 224 | if @schema and !!@skipValidate is false 225 | for data in list 226 | reason = @createValidateReason(data) 227 | unless reason.valid 228 | return Promise.reject(reason.error) 229 | result = list.map (i) => @_ensureId(i) 230 | @_store.putBatch(result).then -> result 231 | 232 | save: (data) -> 233 | if data instanceof Array 234 | return Promise.resolve([]) if data.length is 0 235 | return @_saveBatch(data) 236 | 237 | if @schema and !!@skipValidate is false 238 | # console.log data 239 | isValid = @validate(data) 240 | unless isValid 241 | reason = @createValidateReason(data) 242 | return Promise.reject(reason.error) 243 | result = @_ensureId(data) 244 | @_store.put(result) 245 | .then -> result 246 | 247 | remove: (id) -> 248 | if id instanceof Array 249 | return Promise.resolve() if id.length is 0 250 | @_store.removeBatch(id) 251 | else 252 | @_store.remove(id) 253 | 254 | find: (id) -> 255 | @_store.get(id) 256 | .catch (e) -> undefined 257 | 258 | fetch: (id) -> 259 | @_store.get(id).then (item) -> 260 | return Promise.reject(new Error("#{id} entity does not exist")) unless item? 261 | item 262 | 263 | all: -> @_store.getAll() 264 | 265 | toMemoryDb: -> 266 | @_store.getAll() 267 | .then (items) => 268 | memoryDb = new class extends StoneSkin.MemoryDb 269 | name: @name 270 | schema: @schema 271 | memoryDb._data = items 272 | memoryDb 273 | 274 | toSyncedMemoryDb: -> 275 | @_store.getAll() 276 | .then (items) => 277 | memoryDb = new class extends SyncedMemoryDb 278 | name: @name 279 | schema: @schema 280 | memoryDb._data = items 281 | memoryDb 282 | 283 | class Migrator 284 | # type version: string; 285 | 286 | # version: version; // current version 287 | constructor: (@version, @opts = {}) -> 288 | @lastVersion = @getLastVersion() 289 | @needInitialize = !@lastVersion 290 | 291 | # version? 292 | getLastVersion: -> 293 | localStorage?.getItem('ss-dbVersion') 294 | 295 | # boolean 296 | needUpdated: -> 297 | if @lastVersion? and @lastVersion is @version 298 | false 299 | else 300 | true 301 | 302 | # () => void 303 | _setDbVersionToLocalStorage: -> 304 | localStorage?.setItem 'ss-dbVersion', @version 305 | 306 | # () => Promise 307 | migrate: -> 308 | Promise.resolve( 309 | if @needInitialize 310 | @_setDbVersionToLocalStorage() 311 | @opts.initialize?() 312 | else 313 | null 314 | ) 315 | .then => 316 | if @needUpdated() 317 | @_migrateByVersion @lastVersion, @version 318 | .then => 319 | @_setDbVersionToLocalStorage() 320 | 321 | # (from: version, to: version) => Promise 322 | _migrateByVersion: (from, to) => 323 | from = parseInt from, 10 324 | to = parseInt to, 10 325 | start = Promise.resolve() 326 | while from < to 327 | fnName = "#{from}to#{from + 1}" 328 | fn = @opts[fnName] 329 | start = start.then fn 330 | from++ 331 | start 332 | 333 | ## utils 334 | StoneSkin.utils = {} 335 | 336 | # () => Promise 337 | StoneSkin.utils.setupWithMigrate = (currentVersion, opts = {}) -> 338 | migrator = new Migrator currentVersion, opts 339 | migrator.migrate() 340 | -------------------------------------------------------------------------------- /src/with-tv4.coffee: -------------------------------------------------------------------------------- 1 | StoneSkin = require './index' 2 | tv4 = require 'tv4' 3 | StoneSkin.validate = (data, schema) -> 4 | tv4.validate data, (schema ? {}), true 5 | 6 | StoneSkin.createValidateReason = (data, schema) -> 7 | tv4.validateResult data, (schema ? {}), true 8 | 9 | module.exports = StoneSkin 10 | -------------------------------------------------------------------------------- /test/test.coffee: -------------------------------------------------------------------------------- 1 | global.StoneSkin = require '../src/with-tv4' 2 | 3 | describe 'StoneSkin', -> 4 | crudScenario = (Cls) -> 5 | class Item extends Cls 6 | storeName: 'Item' 7 | item = new Item 8 | item.ready 9 | .then -> 10 | item.clear() 11 | .then -> 12 | item.all() 13 | .then (items) -> 14 | assert items.length is 0 15 | .then -> 16 | item.save { 17 | _id: 'xxx' 18 | title: 'test2' 19 | body: 'hello' 20 | } 21 | .then (saved) -> 22 | assert.ok !!saved.length is false 23 | item.save [ 24 | { 25 | _id: 'yyy' 26 | title: 'test1' 27 | body: 'hello' 28 | } 29 | ] 30 | .then (saved) -> 31 | assert.ok saved.length is 1 32 | item.all() 33 | .then (items) -> 34 | assert items.length is 2 35 | item.remove 'xxx' 36 | .then -> 37 | item.all() 38 | .then (items) -> 39 | assert items.length is 1 40 | 41 | it 'should do crud by MemoryDb', -> 42 | crudScenario(StoneSkin.MemoryDb) 43 | 44 | it 'should do crud by IndexedDb', -> 45 | crudScenario(StoneSkin.IndexedDb) 46 | 47 | updateScenario = (Db) -> 48 | item = new Db 49 | item.ready 50 | .then -> item.clear() 51 | .then -> 52 | item.save { 53 | _id: 'xxx' 54 | title: 'test' 55 | body: 'init' 56 | } 57 | .then -> 58 | item.save { 59 | _id: 'xxx' 60 | title: 'test' 61 | body: 'updated' 62 | } 63 | .then -> item.all() 64 | .then (items) -> 65 | assert.ok items.length is 1 66 | assert.ok items[0].body is 'updated' 67 | .then -> 68 | item.save [ 69 | { 70 | _id: 'xxx' 71 | title: 'test' 72 | body: 'zzz' 73 | } 74 | { 75 | _id: 'yyy' 76 | title: 'test' 77 | body: 'zzz' 78 | } 79 | ] 80 | .then -> item.all() 81 | .then (items) -> 82 | assert.ok items.length is 2 83 | 84 | it 'should update by same id (MemoryDb)', -> 85 | updateScenario(StoneSkin.MemoryDb) 86 | 87 | it 'should update by same id (IndexedDb)', -> 88 | updateScenario(StoneSkin.IndexedDb) 89 | 90 | validationScenario = (Db, done) -> 91 | class Item extends Db 92 | storeName: 'Item' 93 | schema: 94 | required: ['foo'] 95 | properties: 96 | foo: 97 | type: 'string' 98 | item = new Item 99 | willSave = 100 | bar: 'string' 101 | item.ready 102 | .then -> item.clear() 103 | .then -> item.save willSave 104 | .then (saved) -> done('error') 105 | .catch (e) -> 106 | item.save [willSave] 107 | .then -> done('error') 108 | .catch (e) -> 109 | console.log 'catched', e 110 | done() 111 | 112 | it 'validate by IndexedDb', (done) -> 113 | validationScenario(StoneSkin.IndexedDb, done) 114 | 115 | it 'validate by MemoryDb', (done) -> 116 | validationScenario(StoneSkin.MemoryDb, done) 117 | 118 | selectScenario = (Db) -> 119 | class Item extends Db 120 | storeName: 'Item' 121 | item = new Item 122 | item.ready 123 | .then -> item.save [ 124 | {a: 1} 125 | {a: 2} 126 | {a: 3} 127 | ] 128 | .then -> item.select (i) -> i.a >= 2 129 | .then (items) -> 130 | assert.ok items.length is 2 131 | 132 | it 'select by IndexedDb', -> 133 | selectScenario(StoneSkin.IndexedDb) 134 | 135 | it 'select by MemoryDb', -> 136 | selectScenario(StoneSkin.MemoryDb) 137 | -------------------------------------------------------------------------------- /test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mocha 5 | 6 | 7 | 8 |
9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | --------------------------------------------------------------------------------