├── .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 |