├── test ├── titanium │ ├── .gitignore │ ├── Resources │ │ ├── lib │ │ ├── test │ │ │ ├── browser │ │ │ └── tests_to_run.js │ │ ├── app.js │ │ ├── runner.js │ │ └── qunit │ │ │ └── titanium_adaptor.js │ ├── manifest │ └── tiapp.xml ├── browser │ ├── tasks.html │ ├── tasks.client.js │ ├── test.migrations.html │ ├── test.sync.html │ ├── test.search.html │ ├── test.mixin.html │ ├── test.persistence.html │ ├── test.jquery-persistence.html │ ├── test.uki-persistence.html │ ├── test.search.js │ ├── uki │ │ └── uki-persistence.js │ ├── util.js │ ├── qunit │ │ └── qunit.css │ ├── test.mixin.js │ ├── test.sync.js │ └── test.migrations.js └── node │ ├── test.store.config.js │ ├── test.memory.store.js │ ├── test.sqlite.store.js │ ├── test.error.handling.js │ ├── test.sqlite3.store.js │ ├── test.sync.server.js │ ├── partial.sync.schema.sql │ └── node-blog.js ├── .gitignore ├── index.js ├── demo └── jquerymobile │ ├── assets │ ├── version.png │ ├── ipad-palm.png │ ├── jqm-sitebg.png │ └── jquery-logo.png │ ├── README.md │ ├── order │ └── form-fake-response.html │ ├── docs │ ├── text.html │ └── text_and_images.html │ └── index.html ├── lib ├── index.js ├── persistence.sync.server.php.sql ├── persistence.store.config.js ├── persistence.pool.js ├── persistence.jquery.js ├── persistence.store.sqlite.js ├── persistence.store.sqlite3.js ├── persistence.store.react-native.js ├── persistence.sync.server.php ├── persistence.store.mysql.js ├── persistence.store.titanium.js ├── persistence.store.websql.js ├── persistence.store.cordovasql.js ├── persistence.sync.server.js ├── persistence.store.memory.js ├── persistence.search.js ├── persistence.migrations.js └── persistence.jquery.mobile.js ├── package.json ├── AUTHORS ├── docs ├── jquery.md ├── search.md ├── DEVELOPMENT.md ├── jquery.mobile.md ├── migrations.md └── sync.md ├── CHANGES └── bower.json /test/titanium/.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | test/titanium/build 2 | -------------------------------------------------------------------------------- /test/titanium/Resources/lib: -------------------------------------------------------------------------------- 1 | ../../../lib -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/'); 2 | -------------------------------------------------------------------------------- /test/titanium/Resources/test/browser: -------------------------------------------------------------------------------- 1 | ../../../browser -------------------------------------------------------------------------------- /demo/jquerymobile/assets/version.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coresmart/persistencejs/HEAD/demo/jquerymobile/assets/version.png -------------------------------------------------------------------------------- /demo/jquerymobile/assets/ipad-palm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coresmart/persistencejs/HEAD/demo/jquerymobile/assets/ipad-palm.png -------------------------------------------------------------------------------- /demo/jquerymobile/assets/jqm-sitebg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coresmart/persistencejs/HEAD/demo/jquerymobile/assets/jqm-sitebg.png -------------------------------------------------------------------------------- /test/titanium/Resources/app.js: -------------------------------------------------------------------------------- 1 | var win = Titanium.UI.createWindow({ 2 | url:'runner.js', 3 | title: 'Unit Test' 4 | }); 5 | win.open(); -------------------------------------------------------------------------------- /demo/jquerymobile/assets/jquery-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coresmart/persistencejs/HEAD/demo/jquerymobile/assets/jquery-logo.png -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./persistence').persistence; 2 | module.exports.StoreConfig = require('./persistence.store.config'); 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "persistencejs", 3 | "version": "0.3.0", 4 | "engine": "node >=0.2.0", 5 | "author": "Zef Hemel", 6 | "directories": {"lib": "./lib"} 7 | } 8 | -------------------------------------------------------------------------------- /test/titanium/Resources/runner.js: -------------------------------------------------------------------------------- 1 | // This file needs to sit in the Resources directory so that when 2 | // it is used as a URL to a window, the include structure doesn't change. 3 | Titanium.include('qunit/titanium_adaptor.js'); -------------------------------------------------------------------------------- /test/titanium/manifest: -------------------------------------------------------------------------------- 1 | #appname: titanium 2 | #publisher: staugaard 3 | #url: https://github.com/zefhemel/persistencejs 4 | #image: appicon.png 5 | #appid: persistencejs.titanium.test 6 | #desc: undefined 7 | #type: mobile 8 | #guid: add78b91-427a-456d-9d6f-e21a272adf95 9 | -------------------------------------------------------------------------------- /demo/jquerymobile/README.md: -------------------------------------------------------------------------------- 1 | To try this demo, you need to run it through a web server. 2 | `index.html` uses relative links to persistence.js (in 3 | `../../lib/persistence.js` to be exact), so this path needs to be 4 | available. 5 | 6 | Example images, design and text used in the demo are copy-pasted 7 | straight from jquerymobile documentation. 8 | -------------------------------------------------------------------------------- /demo/jquerymobile/order/form-fake-response.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Form submission 5 | 6 | 7 | 8 |
9 | 10 |
11 |

Sample form response

12 |
13 | 14 |
15 |

Fake response

16 |

You choose: (your value here if it would be no fake)

17 |
18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /lib/persistence.sync.server.php.sql: -------------------------------------------------------------------------------- 1 | -- This table must exist in the database for synchronization with the php version of the server to run. 2 | -- This is definitely not an efficient, but it's about as simple as it gets 3 | 4 | CREATE TABLE `persistencejs_objects` ( 5 | `id` char(32) NOT NULL, 6 | `bucket` varchar(50) NOT NULL, 7 | `lastUpdated` bigint(20) NOT NULL, 8 | `content` text NOT NULL, 9 | PRIMARY KEY (`id`), 10 | KEY `ix_objects_lastUpdated` (`lastUpdated`), 11 | KEY `ix_bucket` (`bucket`) 12 | ) DEFAULT CHARSET=utf8 -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Authors ordered by first contribution. 2 | 3 | Zef Hemel 4 | Fabio Rehm 5 | Lukas Berns 6 | Roberto Saccon 7 | Wilker Lúcio 8 | Bruno Jouhier 9 | Robin Wenglewski 10 | Matthias Hochgatterer 11 | Chris Chua 12 | Mike Smullin 13 | Masahiro Hayashi 14 | Mick Staugaard 15 | Shane Tomlinson 16 | Eugene Ware 17 | -------------------------------------------------------------------------------- /test/browser/tasks.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

Tasks

10 |

Hello!

11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /docs/jquery.md: -------------------------------------------------------------------------------- 1 | # persistence.jquery.js 2 | 3 | `persistence.jquery.js` is a jquery plugin for `persistence.js` that 4 | allows the usage of jquery notation for crossbrowser-access of 5 | persistencejs entities. 6 | 7 | Example 8 | ------- 9 | 10 | Simple example: 11 | 12 | var User = persistence.define('User', { 13 | firstname: "TEXT", 14 | lastname: "TEXT" 15 | }); 16 | 17 | var user = new User({firstname: "Joe", lastname: "Doo"}); 18 | 19 | // setter 20 | $(user).data('firstname', "Mike") 21 | 22 | // getter 23 | console.log($(user).data('firstname')); // => Mike 24 | 25 | You can find more examples in `test/test.persistence-jquery.js`. -------------------------------------------------------------------------------- /test/browser/tasks.client.js: -------------------------------------------------------------------------------- 1 | 2 | // Data model 3 | var Task = persistence.define('Task', { 4 | name: "TEXT", 5 | done: "BOOL", 6 | lastChange: "DATE" 7 | }); 8 | 9 | persistence.connect('taskdemo', 'database', 5 * 1024 * 1024, '1.0'); 10 | persistence.schemaSync(); 11 | 12 | function syncAll() { 13 | persistence.sync.synchronize('/recentChanges', Task, function(conflicts, updatesToPush, callback) { 14 | console.log(conflicts); 15 | callback(); 16 | }); 17 | } 18 | 19 | 20 | function addTask() { 21 | var t = new Task(); 22 | t.name = "Some new local task"; 23 | t.done = false; 24 | t.lastChange = new Date(); 25 | persistence.add(t); 26 | persistence.flush(); 27 | } 28 | -------------------------------------------------------------------------------- /test/titanium/Resources/test/tests_to_run.js: -------------------------------------------------------------------------------- 1 | //setting up stuff so that the environment kind of looks like a browser 2 | var window = {}; 3 | var console = { 4 | log: function() { 5 | Titanium.API.debug(arguments[0]); 6 | } 7 | }; 8 | var document = null; 9 | $ = function(document) { 10 | return {ready: function(f) {f();}}; 11 | }; 12 | 13 | //requiring persistencejs 14 | Titanium.include('lib/persistence.js', 15 | 'lib/persistence.store.sql.js', 16 | 'lib/persistence.store.titanium.js'); 17 | var persistence = window.persistence; 18 | //allows us to run unmodified browser tests in titanium 19 | persistence.store.websql = persistence.store.titanium; 20 | 21 | 22 | // Tests to run 23 | Titanium.include('test/browser/test.persistence.js'); 24 | //Titanium.include('test/browser/util.js'); 25 | //Titanium.include('test/browser/test.migrations.js'); 26 | -------------------------------------------------------------------------------- /test/node/test.store.config.js: -------------------------------------------------------------------------------- 1 | // $ expresso test.store.config.js 2 | 3 | var assert = require('assert'); 4 | var persistence = require('../../lib/persistence').persistence; 5 | 6 | var config = { 7 | adaptor: '', 8 | database: 'test', 9 | host: 'localhost', 10 | port: 3306, 11 | user: 'root', 12 | password: '' 13 | }; 14 | 15 | module.exports = { 16 | memory: function() { 17 | config.adaptor = 'memory'; 18 | var persistenceStore = require('../../lib/persistence.store.config').init(persistence, config); 19 | var session = persistenceStore.getSession(); 20 | session.close(); 21 | }, 22 | mysql: function() { 23 | config.adaptor = 'mysql'; 24 | var persistenceStore = require('../../lib/persistence.store.config').init(persistence, config); 25 | var session = persistenceStore.getSession(); 26 | session.close(); 27 | }, 28 | default: function() { 29 | var persistenceStore = require('../../lib/persistence.store.config').init(persistence, config); 30 | var session = persistenceStore.getSession(); 31 | session.close(); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /test/browser/test.migrations.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |

persistence.js migrations plugin

18 |

19 |

20 |
    21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /lib/persistence.store.config.js: -------------------------------------------------------------------------------- 1 | exports.init = function(persistence, config) { 2 | var persistenceStore; 3 | switch (config.adaptor) { 4 | case 'memory': 5 | persistenceStore = require('./persistence.store.memory'); 6 | break; 7 | case 'mysql': 8 | persistenceStore = require('./persistence.store.mysql'); 9 | break; 10 | case 'sqlite3': 11 | persistenceStore = require('./persistence.store.sqlite3'); 12 | break; 13 | case 'react-native': 14 | persistenceStore = require('./persistence.store.react-native'); 15 | break; 16 | default: 17 | persistenceStore = require('./persistence.store.mysql'); 18 | break; 19 | } 20 | 21 | if (config.username) config.user = config.username; 22 | if (config.hostname) config.host = config.hostname; 23 | persistenceStore.config(persistence, 24 | config.host, 25 | config.port, 26 | config.database, 27 | config.user, 28 | config.password); 29 | return persistenceStore; 30 | }; 31 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | Changes 2 | ======= 3 | 4 | * Moved all the SQL stuff into persistence.store.sql.js, and WebSQL to 5 | persistence.store.websql.js. So, to use a WebSQL browser database you need 6 | to include 3 files in your HTML now: 7 | 8 | 9 | 10 | 11 | 12 | Then, instead of using `persistence.connect` use: 13 | 14 | persistence.store.websql.config(persistence, 'dbname', 'My db', 5 * 1024 * 1024); 15 | 16 | For node.js and MySQL: 17 | 18 | var persistence = require('./persistence').persistence; 19 | var persistenceStore = require('./persistence.store.mysql'); 20 | 21 | persistenceStore.config(persistence, 'localhost', 'somedb', 'user', 'pw'); 22 | var session = persistenceStore.getSession(); 23 | ... 24 | session.close(); 25 | 26 | * persistence.db.log is now called persistence.debug 27 | 28 | v0.1.1: Last version with only one persistence.js file 29 | -------------------------------------------------------------------------------- /test/browser/test.sync.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |

    persistence.js sync tests

    19 |

    20 |

    21 |
      22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/browser/test.search.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |

      persistence.js search tests

      19 |

      20 |

      21 |
        22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/browser/test.mixin.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |

        persistence.js mixin tests

        19 |

        20 |

        21 |
          22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/browser/test.persistence.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |

          persistence.js core tests

          19 |

          20 |

          21 |
            22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "persistence", 3 | "main": "./lib/persistence.js", 4 | "version": "0.3.0", 5 | "_release": "0.3.0", 6 | "_target": "~0.3.0", 7 | "_source": "git://github.com/zefhemel/persistencejs.git", 8 | "homepage": "http://persistencejs.org", 9 | "authors": [ 10 | "Zef Hemel ", 11 | "Fabio Rehm ", 12 | "Lukas Berns", 13 | "Roberto Saccon ", 14 | "Wilker Lúcio ", 15 | "Bruno Jouhier ", 16 | "Robin Wenglewski ", 17 | "Matthias Hochgatterer ", 18 | "Chris Chua ", 19 | "Mike Smullin ", 20 | "Masahiro Hayashi ", 21 | "Mick Staugaard ", 22 | "Shane Tomlinson ", 23 | "Eugene Ware " 24 | ], 25 | "description": "An asynchronous Javascript database mapper library. You can use it in the browser, as well on the server (and you can share data models between them).", 26 | "license": "MIT", 27 | "ignore": [ 28 | "**/.*", 29 | "node_modules", 30 | "test" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /lib/persistence.pool.js: -------------------------------------------------------------------------------- 1 | var sys = require('sys'); 2 | var mysql = require('mysql'); 3 | 4 | function log(o) { 5 | sys.print(sys.inspect(o) + "\n"); 6 | } 7 | 8 | function ConnectionPool(getSession, initialPoolSize) { 9 | this.newConnection = getSession; 10 | this.pool = []; 11 | for(var i = 0; i < initialPoolSize; i++) { 12 | this.pool.push({available: true, session: getSession()}); 13 | } 14 | } 15 | 16 | ConnectionPool.prototype.obtain = function() { 17 | var session = null; 18 | for(var i = 0; i < this.pool.length; i++) { 19 | if(this.pool[i].available) { 20 | var pool = this.pool[i]; 21 | session = pool.session; 22 | pool.available = false; 23 | pool.claimed = new Date(); 24 | break; 25 | } 26 | } 27 | if(!session) { 28 | session = getSession(); 29 | this.pool.push({available: false, session: session, claimed: new Date() }); 30 | } 31 | }; 32 | 33 | ConnectionPool.prototype.release = function(session) { 34 | for(var i = 0; i < this.pool.length; i++) { 35 | if(this.pool[i].session === session) { 36 | var pool = this.pool[i]; 37 | pool.available = true; 38 | pool.claimed = null; 39 | return; 40 | } 41 | } 42 | return false; 43 | }; 44 | 45 | exports.ConnectionPool = ConnectionPool; 46 | -------------------------------------------------------------------------------- /test/browser/test.jquery-persistence.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |

            persistence.js core tests with jquery-plugin

            20 |

            21 |

            22 |
              23 | 24 | -------------------------------------------------------------------------------- /test/titanium/tiapp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | persistencejs.titanium.test 4 | titanium 5 | 1.0 6 | staugaard 7 | https://github.com/zefhemel/persistencejs 8 | No description provided 9 | 2011 by staugaard 10 | default_app_logo.png 11 | false 12 | false 13 | default 14 | false 15 | false 16 | false 17 | true 18 | add78b91-427a-456d-9d6f-e21a272adf95 19 | 20 | 21 | Ti.UI.PORTRAIT 22 | 23 | 24 | Ti.UI.PORTRAIT 25 | Ti.UI.UPSIDE_PORTRAIT 26 | Ti.UI.LANDSCAPE_LEFT 27 | Ti.UI.LANDSCAPE_RIGHT 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /test/browser/test.uki-persistence.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |

              persistence.js core tests with uki MVC framework

              21 |

              22 |

              23 |
                24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /test/node/test.memory.store.js: -------------------------------------------------------------------------------- 1 | // $ expresso -s test.memory.store.js 2 | 3 | var assert = require('assert'); 4 | var persistence = require('../../lib/persistence').persistence; 5 | var persistenceStore = require('../../lib/persistence.store.memory'); 6 | persistenceStore.config(persistence); 7 | 8 | var Task = persistence.define('Task', { 9 | username: 'TEXT' 10 | }); 11 | 12 | var data = { 13 | username: 'test' 14 | }; 15 | 16 | var task, session; 17 | 18 | module.exports = { 19 | init: function(done) { 20 | persistence.schemaSync(); 21 | session = persistenceStore.getSession(); 22 | done(); 23 | }, 24 | add: function(done) { 25 | task = new Task(data); 26 | session.add(task); 27 | session.flush(function(result, err) { 28 | assert.ifError(err); 29 | done(); 30 | }); 31 | }, 32 | get: function(done) { 33 | Task.findBy(session, 'id', task.id, function(task) { 34 | assert.equal(task.username, data.username); 35 | done(); 36 | }); 37 | }, 38 | update: function(done) { 39 | task.username = 'test2'; 40 | Task.findBy(session, 'id', task.id, function(task) { 41 | assert.equal(task.username, 'test2'); 42 | done(); 43 | }); 44 | }, 45 | remove: function(done) { 46 | session.remove(task); 47 | session.flush(function(result, err) { 48 | assert.ifError(err); 49 | Task.findBy(session, 'id', task.id, function(task) { 50 | assert.equal(task, null); 51 | done(); 52 | }); 53 | }); 54 | }, 55 | afterAll: function(done) { 56 | session.close(); 57 | done(); 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /demo/jquerymobile/docs/text.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Text only 5 | 6 | 7 | 8 |
                9 | 10 |
                11 |

                Text only

                12 |
                13 | 14 |
                15 |

                jQuery’s mobile strategy can be summarized simply: Delivering top-of-the-line JavaScript in a unified User Interface that works across the most-used smartphone web browsers and tablet form factors.

                16 | 17 |

                The critical difference with our approach is the wide variety of mobile platforms we’re targeting with jQuery Mobile. We’ve been working hard at bringing jQuery support to all mobile browsers that are sufficiently-capable and have at least a nominal amount of market share. In this way, we’re treating mobile web browsers exactly how we treat desktop web browsers.

                18 | 19 |

                To make this broad support possible, all pages in jQuery Mobile are built on a foundation of clean, semantic HTML to ensure compatibility with pretty much any web-enabled device. In devices that interpret CSS and JavaScript, jQuery Mobile applies progressive enhancement techniques to unobtrusively transform the semantic page into a rich, interactive experience that leverages the power of jQuery and CSS. Accessibility features such as WAI-ARIA are tightly integrated throughout the framework to provide support for screen readers and other assistive technologies.

                20 |
                21 | 22 |
                23 | 24 | 25 | -------------------------------------------------------------------------------- /docs/search.md: -------------------------------------------------------------------------------- 1 | persistence.search.js 2 | ============== 3 | `persistence.search.js` is a light-weight extension of the 4 | `persistence.js` library that adds full-text search through a simple 5 | API. 6 | 7 | Initialization: 8 | 9 | persistence.search.config(persistence, persistence.store.websql.sqliteDialect); 10 | 11 | Example usage: 12 | 13 | var Note = persistence.define('Note', { 14 | name: "TEXT", 15 | text: "TEXT", 16 | status: "TEXT" 17 | }); 18 | Note.textIndex('name'); 19 | Note.textIndex('text'); 20 | 21 | This sample defines a `Note` entity with three properties of which 22 | `name` and `text` are full-text indexed. For this a new database table 23 | will be created that stores the index. 24 | 25 | Searching is done as follows: 26 | 27 | Note.search("note").list(tx, function(results) { 28 | console.log(results); 29 | }); 30 | 31 | or you can paginate your results using `limit` and `skip` (similar 32 | to `limit` and `skip` in QueryCollections). 33 | 34 | Note.search("note").limit(10).skip(10).list(null, function(results) { 35 | console.log(results); 36 | }); 37 | 38 | Query language 39 | -------------- 40 | 41 | Queries can contain regular words. In addition the `*` wildcard can be 42 | used anywhere with a word. The `property:` notation can be used to 43 | search only a particular field. Examples: 44 | 45 | * `note` 46 | * `name: note` 47 | * `interesting` 48 | * `inter*` 49 | * `important essential` 50 | 51 | Note that currently a result is return when _any_ word matches. 52 | Results are ranked by number of occurences of one of the words in the 53 | text. 54 | -------------------------------------------------------------------------------- /demo/jquerymobile/docs/text_and_images.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Text and image 5 | 6 | 7 | 8 |
                9 | 10 |
                11 |

                Text and image

                12 |
                13 | 14 |
                15 |

                jQuery’s mobile strategy can be summarized simply: Delivering top-of-the-line JavaScript in a unified User Interface that works across the most-used smartphone web browsers and tablet form factors.

                16 |

                The critical difference with our approach is the wide variety of mobile platforms we’re targeting with jQuery Mobile. We’ve been working hard at bringing jQuery support to all mobile browsers that are sufficiently-capable and have at least a nominal amount of market share. In this way, we’re treating mobile web browsers exactly how we treat desktop web browsers.

                17 |

                To make this broad support possible, all pages in jQuery Mobile are built on a foundation of clean, semantic HTML to ensure compatibility with pretty much any web-enabled device. In devices that interpret CSS and JavaScript, jQuery Mobile applies progressive enhancement techniques to unobtrusively transform the semantic page into a rich, interactive experience that leverages the power of jQuery and CSS. Accessibility features such as WAI-ARIA are tightly integrated throughout the framework to provide support for screen readers and other assistive technologies.

                18 | Smartphone and tablet designs 19 |
                20 | 21 |
                22 | 23 | 24 | -------------------------------------------------------------------------------- /test/node/test.sqlite.store.js: -------------------------------------------------------------------------------- 1 | // $ expresso -s test.sqlite.store.js 2 | 3 | var assert = require('assert'); 4 | var persistence = require('../../lib/persistence').persistence; 5 | var persistenceStore = require('../../lib/persistence.store.sqlite'); 6 | 7 | var dbPath = __dirname + '/test.db'; 8 | persistenceStore.config(persistence, dbPath); 9 | 10 | var Task = persistence.define('Task', { 11 | username: 'TEXT' 12 | }); 13 | 14 | var data = { 15 | username: 'test' 16 | }; 17 | 18 | var task, session; 19 | 20 | // remove test database 21 | function removeDb() { 22 | try { 23 | require('fs').unlinkSync(dbPath); 24 | } catch (err) { 25 | } 26 | } 27 | 28 | module.exports = { 29 | init: function(done) { 30 | removeDb(); 31 | session = persistenceStore.getSession(function () { 32 | session.schemaSync(done); 33 | }); 34 | }, 35 | add: function(done) { 36 | task = new Task(session, data); 37 | session.add(task); 38 | session.flush(function(result, err) { 39 | assert.ifError(err); 40 | done(); 41 | }); 42 | }, 43 | get: function(done) { 44 | Task.findBy(session, 'id', task.id, function(task) { 45 | assert.equal(task.username, data.username); 46 | done(); 47 | }); 48 | }, 49 | update: function(done) { 50 | task.username = 'test2'; 51 | Task.findBy(session, 'id', task.id, function(task) { 52 | assert.equal(task.username, 'test2'); 53 | done(); 54 | }); 55 | }, 56 | remove: function(done) { 57 | session.remove(task); 58 | session.flush(function(result, err) { 59 | assert.ifError(err); 60 | Task.findBy(session, 'id', task.id, function(task) { 61 | assert.equal(task, null); 62 | done(); 63 | }); 64 | }); 65 | }, 66 | afterAll: function(done) { 67 | session.close(function() { 68 | removeDb(); 69 | done(); 70 | }); 71 | } 72 | }; 73 | -------------------------------------------------------------------------------- /docs/DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | Documentation for developers 2 | ============================ 3 | 4 | Constructor functions 5 | --------------------- 6 | 7 | var Task = persistence.define('Task', { 8 | name: "TEXT", 9 | done: "BOOL" 10 | }); 11 | var Category = persistence.define('Category', { 12 | name: "TEXT" 13 | }); 14 | Task.hasOne('category', Category); 15 | 16 | `Task` is a constructor function that is used to create new instances of the `Task` entity, but also can be used to retrieve meta data from, using `Task.meta`. This `meta` field provides the following information: 17 | 18 | * `name`, the name of the entity as set as first argument of `define` 19 | * `fields`, the field object passed to the original `define`, 20 | consisting of property names as keys and textual column types as 21 | values. 22 | * `hasOne`, an object with relation names as keys and relationship 23 | objects as values. The relationship object currently has one field: 24 | `type`: which links to the constructor function of the type of the 25 | relation. Example: `Task.hasOne.category.type` will equal the 26 | `Category` constructor. 27 | * `hasMany`, an object with relation anmes as keys and relationship objects as values. The relationship has the following fields: 28 | * `type`: the constructor function of the relationship entity 29 | * `inverseProperty`: the property name of the inverse relation 30 | * `manyToMany`: a boolean value indicating if this is a manyToMany 31 | relationship or not (then it's a one-tomany) 32 | * `tableName`: name of the utility coupling table used for a 33 | many-to-many relationship 34 | 35 | Extension hooks 36 | ---------------- 37 | 38 | * `persistence.entityDecoratorHooks`: a list of functions (with the 39 | constructor function as argument) to be called to decorate. Useful to 40 | add new functionality to constructo functions, such as `Task.index`. 41 | * `persistence.flushHooks`: a list of functions to be called before flushing. 42 | * `persistence.schemaSyncHooks`: a list of functions to be called before syncing the schema. 43 | 44 | -------------------------------------------------------------------------------- /docs/jquery.mobile.md: -------------------------------------------------------------------------------- 1 | # persistence.jquery.mobile.js 2 | 3 | `persistence.jquery.mobile.js` is a plugin for `persistence.js` and [jQuery mobile](http://jquerymobile.com) that 4 | allows ajax request re-routing to persitencejs for: 5 | 6 | * Html text: caches ajax-loaded HTML pages in local DB. 7 | * Images (in `img` tags of ajax-loaded HTML pages): grabs/encodes them via `canvas` and caches them as data-URL strings in local DB. 8 | * Form submission (only POST requests). 9 | 10 | For ajax-loaded HTML pages and images, the content-providing entities get 11 | their name from user-overwritable default values. For form submissions, the entity 12 | is matched according to the following URL pattern: 13 | 14 | entity-name / path1/path2/../pathN 15 | 16 | Ajax re-routing to persitencejs only takes place if the required entities exist. 17 | 18 | Global settings (and it's default values): 19 | 20 | persistence.jquery.mobile.pageEntityName = "Page"; // Html page entity name 21 | persistence.jquery.mobile.imageEntityName = "Image"; // Image entity name 22 | persistence.jquery.mobile.pathField = "path"; // Entity path-field name 23 | persistence.jquery.mobile.dataField = "data"; // Entity data-field name 24 | 25 | 26 | Optional Regular Expression to exclude URLs from re-routing to persistencejs: 27 | 28 | persistence.jquery.mobile.urlExcludeRx 29 | 30 | Example: `persistence.jquery.mobile.urlExcludeRx = /^\/admin\//;` 31 | (all URL paths starting with "/admin/" are excluded) 32 | 33 | 34 | Ajax page loading example: 35 | 36 | URL: "about/intro.html" 37 | => entity name: "Page" 38 | => entity path field: "about/intro.html" 39 | => entity data field: (the HTML content of the page) 40 | Images: (all images contained in the page specified above) 41 | => entity name: "Image" 42 | => entity path field: (src attribute value of IMG tag) 43 | => entity data field: (the imgae data as Base64 encoded dataURL) 44 | 45 | Ajax form submission examples: 46 | 47 | URL (POST): "order/response.html" 48 | => entity name: "Order" 49 | => entity fields (other than path): retrieved from POST data 50 | 51 | You can find a demo at `demo/jquerymobile/index.html` (you must load it from a server). -------------------------------------------------------------------------------- /test/node/test.error.handling.js: -------------------------------------------------------------------------------- 1 | // $ expresso -s test/test.error.handling.js 2 | 3 | var assert = require('assert'); 4 | var persistence = require('../lib/persistence').persistence; 5 | var persistenceStore = require('../lib/persistence.store.mysql'); 6 | 7 | persistenceStore.config(persistence, 'localhost', 3306, 'nodejs_mysql', 'test', 'test'); 8 | 9 | var InexistentTable = persistence.define('inexistent_table', { 10 | name: "TEXT" 11 | }); 12 | 13 | var session = persistenceStore.getSession(); 14 | 15 | var create = function(data, cb) { 16 | var inexistent_table = new InexistentTable(data); 17 | session.add(inexistent_table); 18 | session.flush(function(result, err) { 19 | cb && cb(err, inexistent_table); 20 | }); 21 | }; 22 | 23 | var remove = function(inexistent_table, cb) { 24 | session.remove(inexistent_table); 25 | session.flush(function(result, err) { 26 | cb && cb(err, result); 27 | }); 28 | }; 29 | 30 | var temp; 31 | 32 | module.exports = { 33 | 'beforeAll': function(done) { 34 | session.transaction(function(tx) { 35 | tx.executeSql('FLUSH TABLES WITH READ LOCK;', function() { 36 | done(); 37 | }); 38 | }); 39 | }, 40 | 'schemaSync fail': function(done) { 41 | session.schemaSync(function(tx, err) { 42 | assert.isDefined(err); 43 | done(); 44 | }); 45 | }, 46 | 'create fail': function(done) { 47 | create({ 48 | name: 'test' 49 | }, function(err, result) { 50 | assert.isDefined(err); 51 | temp = result; 52 | done(); 53 | }); 54 | }, 55 | 'remove fail': function(done) { 56 | remove(temp, function(err, result) { 57 | assert.isDefined(err); 58 | done(); 59 | }); 60 | }, 61 | 'destroyAll fail': function(done) { 62 | InexistentTable.all(session).destroyAll(function(result, err) { 63 | assert.isDefined(err); 64 | done(); 65 | }); 66 | }, 67 | 'reset fail': function(done) { 68 | session.reset(function(result, err) { 69 | assert.isDefined(err); 70 | done(); 71 | }); 72 | }, 73 | afterAll: function(done) { 74 | session.transaction(function(tx) { 75 | tx.executeSql('UNLOCK TABLES;', function() { 76 | session.close(); 77 | done(); 78 | }); 79 | }); 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /test/browser/test.search.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | persistence.store.websql.config(persistence, 'searchtest', 'My db', 5 * 1024 * 1024); 3 | persistence.search.config(persistence, persistence.store.websql.sqliteDialect); 4 | persistence.debug = true; 5 | 6 | var Note = persistence.define('Note', { 7 | title: "TEXT", 8 | text: "TEXT" 9 | }); 10 | 11 | Note.textIndex('title'); 12 | Note.textIndex('text'); 13 | 14 | module("Setup"); 15 | 16 | asyncTest("setting up database", 1, function() { 17 | persistence.reset(function() { 18 | persistence.schemaSync(function(tx){ 19 | ok(tx.executeSql, 'schemaSync passed transaction as argument to callback'); 20 | start(); 21 | }); 22 | }); 23 | }); 24 | 25 | module("Search test"); 26 | 27 | asyncTest("Create some sample data", function() { 28 | persistence.add(new Note({title: "My first note", text: "This is my first note. It has a rather high duplication quotient, or whatever."})); 29 | persistence.add(new Note({title: "My second note", text: "This is my second note. Isn't it a cool note? Third, fourth."})); 30 | persistence.add(new Note({title: "My third note", text: "Nothing here yet"})); 31 | persistence.add(new Note({title: "Unrelated", text: "Under contruction."})); 32 | persistence.flush(function() { 33 | start(); 34 | }); 35 | }); 36 | 37 | asyncTest("Searching", function() { 38 | Note.search("note").list(function(results) { 39 | equals(results.length, 3, "returned correct number of results"); 40 | equals(results[0].title, "My second note", "Found most relevant result"); 41 | Note.search("title: third").list(function(results) { 42 | equals(results.length, 1, "returned correct number of results"); 43 | equals(results[0].title, "My third note", "Searched in only title"); 44 | Note.search("thi*").list(function(results) { 45 | equals(results.length, 3, "wildcard search"); 46 | Note.search("thi*").limit(1).list(function(results) { 47 | equals(results.length, 1, "limit number of search results"); 48 | start(); 49 | }); 50 | }); 51 | }); 52 | }); 53 | }); 54 | 55 | }); 56 | -------------------------------------------------------------------------------- /test/node/test.sqlite3.store.js: -------------------------------------------------------------------------------- 1 | // $ expresso -s test.sqlite3.store.js 2 | 3 | var assert = require('assert'); 4 | var persistence = require('../../lib/persistence').persistence; 5 | var persistenceStore = require('../../lib/persistence.store.sqlite3'); 6 | 7 | var dbPath = __dirname + '/test-sqlite3.db'; 8 | persistenceStore.config(persistence, dbPath); 9 | 10 | var Task = persistence.define('Task', { 11 | username: 'TEXT' 12 | }); 13 | 14 | var data = { 15 | username: 'test' 16 | }; 17 | 18 | var data2 = { 19 | username: 'test2' 20 | }; 21 | 22 | var task, task2, session; 23 | 24 | // remove test database 25 | function removeDb() { 26 | try { 27 | require('fs').unlinkSync(dbPath); 28 | } catch (err) { 29 | } 30 | } 31 | 32 | module.exports = { 33 | init: function(done) { 34 | removeDb(); 35 | session = persistenceStore.getSession(function () { 36 | session.schemaSync(done); 37 | }); 38 | }, 39 | add: function(done) { 40 | task = new Task(session, data); 41 | session.add(task); 42 | session.flush(function(result, err) { 43 | assert.ifError(err); 44 | done(); 45 | }); 46 | }, 47 | get: function(done) { 48 | Task.findBy(session, 'id', task.id, function(task) { 49 | assert.equal(task.username, data.username); 50 | done(); 51 | }); 52 | }, 53 | update: function(done) { 54 | task.username = 'test2'; 55 | Task.findBy(session, 'id', task.id, function(task) { 56 | assert.equal(task.username, 'test2'); 57 | done(); 58 | }); 59 | }, 60 | remove: function(done) { 61 | session.remove(task); 62 | session.flush(function(result, err) { 63 | assert.ifError(err); 64 | Task.findBy(session, 'id', task.id, function(task) { 65 | assert.equal(task, null); 66 | done(); 67 | }); 68 | }); 69 | }, 70 | addMultiple: function(done) { 71 | task = new Task(session, data); 72 | session.add(task); 73 | task2 = new Task(session, data2); 74 | session.add(task2); 75 | session.flush(function(result, err) { 76 | assert.ifError(err); 77 | var count = 0; 78 | Task.all(session).order('username', true).each(function(row) { 79 | count++; 80 | if (count == 1) { 81 | assert.equal(row.username, data.username); 82 | } 83 | if (count == 2) { 84 | assert.equal(row.username, data2.username); 85 | done(); 86 | } 87 | }); 88 | }); 89 | }, 90 | afterAll: function(done) { 91 | session.close(function() { 92 | removeDb(); 93 | done(); 94 | }); 95 | } 96 | }; 97 | -------------------------------------------------------------------------------- /test/browser/uki/uki-persistence.js: -------------------------------------------------------------------------------- 1 | // Original file at: http://github.com/rsaccon/uki/tree/master/src/uki-persistence/persistence.js 2 | 3 | 4 | /** 5 | * persistencejs integration (http://www.persistencejs.org) 6 | * 7 | **/ 8 | 9 | // Example 10 | // ======= 11 | // // persistence engine 12 | // include('path/to/persistence.js'); 13 | // include('path/to/persistence.store.sql.js'); 14 | // include('path/to/persistence.store.websql.js'); 15 | // include('path/to/persistence.store.memory.js'); 16 | // include('path/to/persistence.sync.js'); // optional 17 | // include('path/to/persistence.search.js'); // optional 18 | // include('path/to/persistence.migrations.js'); // optional 19 | // include('path/to/uki-data/uki-persistence.js'); 20 | // 21 | // if (window.openDatabase) { 22 | // persistence.store.websql.config(persistence, 'myDbName', 'database', 5 * 1024 * 1024); 23 | // } else { 24 | // persistence.store.memory.config(persistence); 25 | // } 26 | // 27 | // var User = uki.persistence.define('User', { 28 | // firstname: "TEXT", 29 | // lastname: "TEXT" 30 | // }); 31 | // 32 | // var aUser = new User({firstname: "Joe", lastname: "Doo"}); 33 | // 34 | // aUser.firstname("Mike") ; 35 | // 36 | // console.log(aUser.firstname()); // => Mike 37 | // 38 | // persistence.add(aUser); 39 | // 40 | // persistence.flush(); 41 | 42 | 43 | /** 44 | * uki implementation for entity-property 45 | */ 46 | persistence.defineProp = function(scope, field, setterCallback, getterCallback) { 47 | scope[field] = function(value) { 48 | if (value === undefined) { 49 | return getterCallback(); 50 | } else { 51 | setterCallback(value); 52 | return scope; 53 | } 54 | }; 55 | }; 56 | 57 | /** 58 | * uki implementation for entity-property setter 59 | */ 60 | persistence.set = function(scope, fieldName, value) { 61 | if (persistence.isImmutable(fieldName)) throw "immutable field: "+fieldName; 62 | scope[fieldName](value); 63 | return scope; 64 | }; 65 | 66 | /** 67 | * uki implementation for entity-property getter 68 | */ 69 | persistence.get = function(arg1, arg2) { 70 | var val = (arguments.length == 1) ? arg1 : arg1[arg2]; 71 | return (typeof val === "function") ? val() : val; 72 | }; 73 | 74 | /** 75 | * uki ajax implementation 76 | */ 77 | if (persistence.sync) { 78 | uki.extend(persistence.sync, { 79 | getJSON: function(url, callback) { 80 | uki.getJSON(url, null, callback); 81 | }, 82 | postJSON: function(url, data, callback) { 83 | uki.ajax({ 84 | url: url, 85 | type: 'POST', 86 | data: data, 87 | dataType: 'json', 88 | success: function(response) { 89 | callback(JSON.parse(response)); 90 | } 91 | }); 92 | } 93 | }); 94 | } -------------------------------------------------------------------------------- /test/titanium/Resources/qunit/titanium_adaptor.js: -------------------------------------------------------------------------------- 1 | Titanium.include('qunit/qunit.js'); 2 | 3 | // ============================================================================= 4 | // Uncomment the following lines in order to get jsMockito support for mocking 5 | // (after following jsMockito install instructions) 6 | // ============================================================================= 7 | 8 | // Titanium.include('qunit/jshamcrest.js'); 9 | // Titanium.include('qunit/jsmockito-1.0.2.js'); 10 | // JsHamcrest.Integration.QUnit(); 11 | // JsMockito.Integration.importTo(this); 12 | 13 | var logger = function(failures, message) { 14 | if (failures) { 15 | Titanium.API.error(message); 16 | } else { 17 | Titanium.API.info(message); 18 | } 19 | }; 20 | // QUnit.testStart(name) is called whenever a new test batch of assertions starts running. name is the string name of the test batch. 21 | QUnit.testStart = function(name) { 22 | logger(false, '>> >> >>TEST START: '+name); 23 | }; 24 | // QUnit.testDone(name, failures, total) is called whenever a batch of assertions finishes running. name is the string name of the test batch. failures is the number of test failures that occurred. total is the total number of test assertions that occurred. 25 | QUnit.testDone = function(name, failures, total) { 26 | logger(failures, '<< << <> >>MODULE START: '+name); 31 | }; 32 | // QUnit.moduleDone(name, failures, total) is called whenever a module finishes running. name is the string name of the module. failures is the number of module failures that occurred. total is the total number of module assertions that occurred. 33 | QUnit.moduleDone = function(name, failures, total) { 34 | logger(failures, '<< < ', result[0].sql); 44 | ok(result[0].sql.match(regex), column + ' colum exists'); 45 | if (callback) callback(); 46 | }); 47 | }); 48 | } 49 | 50 | function columnNotExists(table, column, type, callback) { 51 | var sql = 'select sql from sqlite_master where type = "table" and name == "'+table+'"'; 52 | type = type.replace('(', '\\(').replace(')', '\\)'); 53 | var regex = "CREATE TABLE \\w+ \\((\\w|[\\(\\), ])*" + column + " " + type + "(\\w|[\\(\\), ])*\\)"; 54 | persistence.transaction(function(tx){ 55 | tx.executeSql(sql, null, function(result){ 56 | ok(!result[0].sql.match(regex), column + ' colum not exists'); 57 | if (callback) callback(); 58 | }); 59 | }); 60 | } 61 | 62 | function indexExists(table, column, callback) { 63 | var sql = 'select sql from sqlite_master where type = "index" and name == "'+table+'_'+column+'"'; 64 | persistence.transaction(function(tx){ 65 | tx.executeSql(sql, null, function(result){ 66 | ok(result.length == 1, 'index ' + table + '_' + column + ' exists'); 67 | if (callback) callback(); 68 | }); 69 | }); 70 | } 71 | 72 | function indexNotExists(table, column, callback) { 73 | var sql = 'select sql from sqlite_master where type = "index" and name == "'+table+'_'+column+'"'; 74 | persistence.transaction(function(tx){ 75 | tx.executeSql(sql, null, function(result){ 76 | ok(result.length == 0, 'index ' + table + '_' + column + ' not exists'); 77 | if (callback) callback(); 78 | }); 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /test/browser/qunit/qunit.css: -------------------------------------------------------------------------------- 1 | 2 | ol#qunit-tests { 3 | font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 4 | margin:0; 5 | padding:0; 6 | list-style-position:inside; 7 | 8 | font-size: smaller; 9 | } 10 | ol#qunit-tests li{ 11 | padding:0.4em 0.5em 0.4em 2.5em; 12 | border-bottom:1px solid #fff; 13 | font-size:small; 14 | list-style-position:inside; 15 | } 16 | ol#qunit-tests li ol{ 17 | box-shadow: inset 0px 2px 13px #999; 18 | -moz-box-shadow: inset 0px 2px 13px #999; 19 | -webkit-box-shadow: inset 0px 2px 13px #999; 20 | margin-top:0.5em; 21 | margin-left:0; 22 | padding:0.5em; 23 | background-color:#fff; 24 | border-radius:15px; 25 | -moz-border-radius: 15px; 26 | -webkit-border-radius: 15px; 27 | } 28 | ol#qunit-tests li li{ 29 | border-bottom:none; 30 | margin:0.5em; 31 | background-color:#fff; 32 | list-style-position: inside; 33 | padding:0.4em 0.5em 0.4em 0.5em; 34 | } 35 | 36 | ol#qunit-tests li li.pass{ 37 | border-left:26px solid #C6E746; 38 | background-color:#fff; 39 | color:#5E740B; 40 | } 41 | ol#qunit-tests li li.fail{ 42 | border-left:26px solid #EE5757; 43 | background-color:#fff; 44 | color:#710909; 45 | } 46 | ol#qunit-tests li.pass{ 47 | background-color:#D2E0E6; 48 | color:#528CE0; 49 | } 50 | ol#qunit-tests li.fail{ 51 | background-color:#EE5757; 52 | color:#000; 53 | } 54 | ol#qunit-tests li strong { 55 | cursor:pointer; 56 | } 57 | h1#qunit-header{ 58 | background-color:#0d3349; 59 | margin:0; 60 | padding:0.5em 0 0.5em 1em; 61 | color:#fff; 62 | font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 63 | border-top-right-radius:15px; 64 | border-top-left-radius:15px; 65 | -moz-border-radius-topright:15px; 66 | -moz-border-radius-topleft:15px; 67 | -webkit-border-top-right-radius:15px; 68 | -webkit-border-top-left-radius:15px; 69 | text-shadow: rgba(0, 0, 0, 0.5) 4px 4px 1px; 70 | } 71 | h2#qunit-banner{ 72 | font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 73 | height:5px; 74 | margin:0; 75 | padding:0; 76 | } 77 | h2#qunit-banner.qunit-pass{ 78 | background-color:#C6E746; 79 | } 80 | h2#qunit-banner.qunit-fail, #qunit-testrunner-toolbar { 81 | background-color:#EE5757; 82 | } 83 | #qunit-testrunner-toolbar { 84 | font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 85 | padding:0; 86 | /*width:80%;*/ 87 | padding:0em 0 0.5em 2em; 88 | font-size: small; 89 | } 90 | h2#qunit-userAgent { 91 | font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 92 | background-color:#2b81af; 93 | margin:0; 94 | padding:0; 95 | color:#fff; 96 | font-size: small; 97 | padding:0.5em 0 0.5em 2.5em; 98 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 99 | } 100 | p#qunit-testresult{ 101 | font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 102 | margin:0; 103 | font-size: small; 104 | color:#2b81af; 105 | border-bottom-right-radius:15px; 106 | border-bottom-left-radius:15px; 107 | -moz-border-radius-bottomright:15px; 108 | -moz-border-radius-bottomleft:15px; 109 | -webkit-border-bottom-right-radius:15px; 110 | -webkit-border-bottom-left-radius:15px; 111 | background-color:#D2E0E6; 112 | padding:0.5em 0.5em 0.5em 2.5em; 113 | } 114 | strong b.fail{ 115 | color:#710909; 116 | } 117 | strong b.pass{ 118 | color:#5E740B; 119 | } 120 | -------------------------------------------------------------------------------- /lib/persistence.jquery.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2010 Roberto Saccon 3 | * 4 | * Permission is hereby granted, free of charge, to any person 5 | * obtaining a copy of this software and associated documentation 6 | * files (the "Software"), to deal in the Software without 7 | * restriction, including without limitation the rights to use, 8 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the 10 | * Software is furnished to do so, subject to the following 11 | * conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be 14 | * included in all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | * OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | 26 | if (!window.jQuery) { 27 | throw new Error("jQuery should be loaded before persistence.jquery.js"); 28 | } 29 | 30 | if (!window.persistence) { 31 | throw new Error("persistence.js should be loaded before persistence.jquery.js"); 32 | } 33 | 34 | persistence.jquery = {}; 35 | 36 | /** 37 | * crossbrowser implementation for entity-property 38 | */ 39 | persistence.defineProp = function(scope, field, setterCallback, getterCallback) { 40 | scope[field] = function(value) { 41 | if (value === undefined) { 42 | return getterCallback(); 43 | } else { 44 | setterCallback(value); 45 | return scope; 46 | } 47 | }; 48 | }; 49 | 50 | /** 51 | * crossbrowser implementation for entity-property setter 52 | */ 53 | persistence.set = function(scope, fieldName, value) { 54 | if (persistence.isImmutable(fieldName)) throw new Error("immutable field: "+fieldName); 55 | scope[fieldName](value); 56 | return scope; 57 | }; 58 | 59 | /** 60 | * crossbrowser implementation for entity-property getter 61 | */ 62 | persistence.get = function(arg1, arg2) { 63 | var val = (arguments.length == 1) ? arg1 : arg1[arg2]; 64 | return (typeof val === "function") ? val() : val; 65 | }; 66 | 67 | 68 | (function($){ 69 | var originalDataMethod = $.fn.data; 70 | 71 | $.fn.data = function(name, data) { 72 | if (this[0] && this[0]._session && (this[0]._session === window.persistence)) { 73 | if (data) { 74 | this[0][name](data); 75 | return this; 76 | } else { 77 | return this[0][name](); 78 | } 79 | } else { 80 | return originalDataMethod.apply(this, arguments); 81 | } 82 | }; 83 | 84 | if (persistence.sync) { 85 | persistence.sync.getJSON = function(url, success) { 86 | $.getJSON(url, null, success); 87 | }; 88 | 89 | persistence.sync.postJSON = function(url, data, success) { 90 | $.ajax({ 91 | url: url, 92 | type: 'POST', 93 | data: data, 94 | dataType: 'json', 95 | success: function(response) { 96 | success(JSON.parse(response)); 97 | } 98 | }); 99 | }; 100 | } 101 | })(jQuery); 102 | -------------------------------------------------------------------------------- /docs/migrations.md: -------------------------------------------------------------------------------- 1 | # persistence.migrations.js 2 | 3 | `persistence.migrations.js` is a plugin for `persistence.js` that provides 4 | a simple API for altering your databases in a structured and organised manner 5 | inspired by [Ruby on Rails migrations](http://guides.rubyonrails.org/migrations.html). 6 | 7 | ## Anatomy of a Migration 8 | 9 | persistence.defineMigration(1, { 10 | up: function() { 11 | this.createTable('Task', function(t){ 12 | t.text('name'); 13 | t.text('description'); 14 | t.boolean('done'); 15 | }); 16 | }, 17 | down: function() { 18 | this.dropTable('Task'); 19 | } 20 | }); 21 | 22 | This migration adds a table called `Task` with a string column called `name`, 23 | a text column called `description` and a boolean column called `done`. 24 | A `id VARCHAR(32) PRIMARY KEY` collumn will also be added, however since 25 | this is the default we do not need to ask for this. Reversing this migration 26 | is as simple as dropping the table. The first argument passed to `defineMigration` 27 | is the migration version which should be incremented when defining following 28 | migrations 29 | 30 | Migrations are not limited to changing the schema. You can also use them to 31 | fix bad data in the database or populate new fields: 32 | 33 | persistence.defineMigration(2, { 34 | up: function() { 35 | this.addColumn('User', 'email', 'TEXT'); 36 | 37 | // You can execute some raw SQL 38 | this.executeSql('UPDATE User SET email = username + "@domain.com"'); 39 | 40 | // OR 41 | 42 | // you can define a custom action to query for objects and manipulate them 43 | this.action(function(tx, nextAction){ 44 | allUsers.list(tx, function(result){ 45 | result.forEach(function(u){ 46 | u.email = u.userName + '@domain.com'; 47 | persistence.add(u); 48 | }); 49 | persistence.flush(tx, function() { 50 | // Please remember to call this when you are done with an action, 51 | // otherwise the system will hang 52 | nextAction(); 53 | }); 54 | }); 55 | }); 56 | } 57 | }); 58 | 59 | This migration adds a `email` column to the `User` table and sets all emails 60 | to `"{userName}@domain.com"`. 61 | 62 | ## API methods 63 | 64 | persistence.defineMigration(3, { 65 | up: function() { 66 | this.addColumn('TableName', 'columnName', 'COLUMN_TYPE'); 67 | this.removeColumn('TableName', 'columnName'); 68 | this.addIndex('TableName', 'columnName'); 69 | this.removeIndex('TableName', 'columnName'); 70 | this.executeSql('RAW SQL'); 71 | this.dropTable('TableName'); 72 | 73 | this.createTable('TableName', function(table){ 74 | table.text('textColumnName'); 75 | table.integer('integerColumnName'); 76 | table.boolean('booleanColumnName'); 77 | table.json('jsonColumnName'); // JSON columns will be mapped to TEXT columns on database 78 | table.date('dateColumnName'); 79 | }); 80 | } 81 | }); 82 | 83 | ## Running Migrations 84 | 85 | First thing you need to do is initialize migrations plugin: 86 | 87 | persistence.migrations.init(function() { 88 | // Optional callback to be executed after initialization 89 | }); 90 | 91 | Then you should load your migrations and run: 92 | 93 | * `persistence.migrate()` to run migrations up to the most recent 94 | * `persistence.migrate(function)` to run migrations up to the most recent and execute some code 95 | * `persistence.migrate(version, function)` to run migrations up / down to the specified version and execute some code 96 | 97 | To load migrations you should use something like [RequireJS](http://requirejs.org/). 98 | -------------------------------------------------------------------------------- /lib/persistence.store.sqlite.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This back-end depends on the node.js asynchronous SQLite driver as found on: 3 | * https://github.com/orlandov/node-sqlite 4 | * Easy install using npm: 5 | * npm install sqlite 6 | * @author Eugene Ware 7 | */ 8 | var sys = require('sys'); 9 | var sql = require('./persistence.store.sql'); 10 | var sqlite = require('sqlite'); 11 | 12 | var db, username, password; 13 | 14 | function log(o) { 15 | sys.print(sys.inspect(o) + "\n"); 16 | } 17 | 18 | 19 | exports.config = function(persistence, dbPath) { 20 | exports.getSession = function(cb) { 21 | var that = {}; 22 | cb = cb || function() { }; 23 | var conn = new sqlite.Database(); 24 | conn.open(dbPath, cb); 25 | 26 | var session = new persistence.Session(that); 27 | session.transaction = function (explicitCommit, fn) { 28 | if (typeof arguments[0] === "function") { 29 | fn = arguments[0]; 30 | explicitCommit = false; 31 | } 32 | var tx = transaction(conn); 33 | if (explicitCommit) { 34 | tx.executeSql("START TRANSACTION", null, function(){ 35 | fn(tx) 36 | }); 37 | } 38 | else 39 | fn(tx); 40 | }; 41 | 42 | session.close = function(cb) { 43 | cb = cb || function() {}; 44 | conn.close(cb); 45 | }; 46 | return session; 47 | }; 48 | 49 | function transaction(conn){ 50 | var that = {}; 51 | // TODO: add check for db opened or closed 52 | that.executeSql = function(query, args, successFn, errorFn){ 53 | function cb(err, result){ 54 | if (err) { 55 | log(err.message); 56 | that.errorHandler && that.errorHandler(err); 57 | errorFn && errorFn(null, err); 58 | return; 59 | } 60 | if (successFn) { 61 | successFn(result); 62 | } 63 | } 64 | if (persistence.debug) { 65 | sys.print(query + "\n"); 66 | args && args.length > 0 && sys.print(args.join(",") + "\n") 67 | } 68 | if (!args) { 69 | conn.execute(query, cb); 70 | } 71 | else { 72 | conn.execute(query, args, cb); 73 | } 74 | } 75 | 76 | that.commit = function(session, callback){ 77 | session.flush(that, function(){ 78 | that.executeSql("COMMIT", null, callback); 79 | }) 80 | } 81 | 82 | that.rollback = function(session, callback){ 83 | that.executeSql("ROLLBACK", null, function() { 84 | session.clean(); 85 | callback(); 86 | }); 87 | } 88 | return that; 89 | } 90 | 91 | ///////////////////////// SQLite dialect 92 | 93 | persistence.sqliteDialect = { 94 | // columns is an array of arrays, e.g. 95 | // [["id", "VARCHAR(32)", "PRIMARY KEY"], ["name", "TEXT"]] 96 | createTable: function(tableName, columns) { 97 | var tm = persistence.typeMapper; 98 | var sql = "CREATE TABLE IF NOT EXISTS `" + tableName + "` ("; 99 | var defs = []; 100 | for(var i = 0; i < columns.length; i++) { 101 | var column = columns[i]; 102 | defs.push("`" + column[0] + "` " + tm.columnType(column[1]) + (column[2] ? " " + column[2] : "")); 103 | } 104 | sql += defs.join(", "); 105 | sql += ')'; 106 | return sql; 107 | }, 108 | 109 | // columns is array of column names, e.g. 110 | // ["id"] 111 | createIndex: function(tableName, columns, options) { 112 | options = options || {}; 113 | return "CREATE "+(options.unique?"UNIQUE ":"")+"INDEX IF NOT EXISTS `" + tableName + "__" + columns.join("_") + 114 | "` ON `" + tableName + "` (" + 115 | columns.map(function(col) { return "`" + col + "`"; }).join(", ") + ")"; 116 | } 117 | }; 118 | 119 | sql.config(persistence, persistence.sqliteDialect); 120 | }; 121 | 122 | -------------------------------------------------------------------------------- /lib/persistence.store.sqlite3.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This back-end depends on the node.js asynchronous SQLite3 driver as found on: 3 | * https://github.com/developmentseed/node-sqlite3 4 | * Easy install using npm: 5 | * npm install sqlite3 6 | * @author Eugene Ware 7 | * @author Jeff Kunkle 8 | * @author Joe Ferner 9 | */ 10 | var sys = require('sys'); 11 | var sql = require('./persistence.store.sql'); 12 | var sqlite = require('sqlite3'); 13 | 14 | var db, username, password; 15 | 16 | function log(o) { 17 | sys.print(sys.inspect(o) + "\n"); 18 | } 19 | 20 | 21 | exports.config = function(persistence, dbPath) { 22 | exports.getSession = function(cb) { 23 | var that = {}; 24 | cb = cb || function() { }; 25 | var conn = new sqlite.Database(dbPath, cb); 26 | 27 | var session = new persistence.Session(that); 28 | session.transaction = function (explicitCommit, fn) { 29 | if (typeof arguments[0] === "function") { 30 | fn = arguments[0]; 31 | explicitCommit = false; 32 | } 33 | var tx = transaction(conn); 34 | if (explicitCommit) { 35 | tx.executeSql("START TRANSACTION", null, function(){ 36 | fn(tx) 37 | }); 38 | } 39 | else 40 | fn(tx); 41 | }; 42 | 43 | session.close = function(cb) { 44 | cb = cb || function() {}; 45 | conn.close(cb); 46 | }; 47 | return session; 48 | }; 49 | 50 | function transaction(conn){ 51 | var that = {}; 52 | // TODO: add check for db opened or closed 53 | that.executeSql = function(query, args, successFn, errorFn){ 54 | function cb(err, result){ 55 | if (err) { 56 | log(err.message); 57 | that.errorHandler && that.errorHandler(err); 58 | errorFn && errorFn(null, err); 59 | return; 60 | } 61 | if (successFn) { 62 | successFn(result); 63 | } 64 | } 65 | if (persistence.debug) { 66 | sys.print(query + "\n"); 67 | args && args.length > 0 && sys.print(args.join(",") + "\n") 68 | } 69 | if (!args) { 70 | conn.all(query, cb); 71 | } 72 | else { 73 | conn.all(query, args, cb); 74 | } 75 | } 76 | 77 | that.commit = function(session, callback){ 78 | session.flush(that, function(){ 79 | that.executeSql("COMMIT", null, callback); 80 | }) 81 | } 82 | 83 | that.rollback = function(session, callback){ 84 | that.executeSql("ROLLBACK", null, function() { 85 | session.clean(); 86 | callback(); 87 | }); 88 | } 89 | return that; 90 | } 91 | 92 | ///////////////////////// SQLite dialect 93 | 94 | persistence.sqliteDialect = { 95 | // columns is an array of arrays, e.g. 96 | // [["id", "VARCHAR(32)", "PRIMARY KEY"], ["name", "TEXT"]] 97 | createTable: function(tableName, columns) { 98 | var tm = persistence.typeMapper; 99 | var sql = "CREATE TABLE IF NOT EXISTS `" + tableName + "` ("; 100 | var defs = []; 101 | for(var i = 0; i < columns.length; i++) { 102 | var column = columns[i]; 103 | defs.push("`" + column[0] + "` " + tm.columnType(column[1]) + (column[2] ? " " + column[2] : "")); 104 | } 105 | sql += defs.join(", "); 106 | sql += ')'; 107 | return sql; 108 | }, 109 | 110 | // columns is array of column names, e.g. 111 | // ["id"] 112 | createIndex: function(tableName, columns, options) { 113 | options = options || {}; 114 | return "CREATE "+(options.unique?"UNIQUE ":"")+"INDEX IF NOT EXISTS `" + tableName + "__" + columns.join("_") + 115 | "` ON `" + tableName + "` (" + 116 | columns.map(function(col) { return "`" + col + "`"; }).join(", ") + ")"; 117 | } 118 | }; 119 | 120 | sql.config(persistence, persistence.sqliteDialect); 121 | }; 122 | 123 | -------------------------------------------------------------------------------- /lib/persistence.store.react-native.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module depends on the react-native asynchronous SQLite3 driver as found on: 3 | * https://github.com/almost/react-native-sqlite 4 | * Easy install using npm: 5 | * npm install react-native-sqlite 6 | * and follow the instructions provided in the README 7 | * @author Lukas Reichart 8 | */ 9 | var sys = {}; 10 | sys.print = console.log; 11 | var sql = require('./persistence.store.sql'); 12 | var sqlite = require('react-native-sqlite'); 13 | 14 | var db, username, password; 15 | 16 | function log(o) { 17 | sys.print(o + "\n"); 18 | } 19 | 20 | 21 | exports.config = function(persistence, dbPath) { 22 | exports.getSession = function(cb) { 23 | var that = {}; 24 | cb = cb || function() { }; 25 | var conn = new sqlite.Database(dbPath, cb); 26 | 27 | var session = new persistence.Session(that); 28 | session.transaction = function (explicitCommit, fn) { 29 | if (typeof arguments[0] === "function") { 30 | fn = arguments[0]; 31 | explicitCommit = false; 32 | } 33 | var tx = transaction(conn); 34 | if (explicitCommit) { 35 | tx.executeSql("START TRANSACTION", null, function(){ 36 | fn(tx) 37 | }); 38 | } 39 | else 40 | fn(tx); 41 | }; 42 | 43 | session.close = function(cb) { 44 | cb = cb || function() {}; 45 | conn.close(cb); 46 | }; 47 | return session; 48 | }; 49 | 50 | function transaction(conn){ 51 | var that = {}; 52 | // TODO: add check for db opened or closed 53 | that.executeSql = function(query, args, successFn, errorFn){ 54 | var queryResult = []; 55 | function cb(err){ 56 | if (err) { 57 | log(err.message); 58 | that.errorHandler && that.errorHandler(err); 59 | errorFn && errorFn(null, err); 60 | return; 61 | } 62 | if (successFn) { 63 | if( !queryResult ) { 64 | queryResult = []; 65 | } 66 | successFn(queryResult); 67 | } 68 | } 69 | function rowCallback(row) { 70 | queryResult.push(row); 71 | } 72 | if (persistence.debug) { 73 | console.log(query + "\n"); 74 | //args && args.length > 0 && sys.print(args.join(",") + "\n") 75 | } 76 | if (!args) { 77 | conn.executeSQL(query, [], rowCallback, cb ); 78 | } 79 | else { 80 | conn.executeSQL(query, args, rowCallback, cb ); 81 | } 82 | } 83 | 84 | that.commit = function(session, callback){ 85 | session.flush(that, function(){ 86 | that.executeSQL("COMMIT", [], function(){}, callback); 87 | }) 88 | } 89 | 90 | that.rollback = function(session, callback){ 91 | that.executeSQL("ROLLBACK", [], function() {}, function() { 92 | session.clean(); 93 | callback(); 94 | }); 95 | } 96 | return that; 97 | } 98 | 99 | ///////////////////////// SQLite dialect 100 | 101 | persistence.sqliteDialect = { 102 | // columns is an array of arrays, e.g. 103 | // [["id", "VARCHAR(32)", "PRIMARY KEY"], ["name", "TEXT"]] 104 | createTable: function(tableName, columns) { 105 | var tm = persistence.typeMapper; 106 | var sql = "CREATE TABLE IF NOT EXISTS `" + tableName + "` ("; 107 | var defs = []; 108 | for(var i = 0; i < columns.length; i++) { 109 | var column = columns[i]; 110 | defs.push("`" + column[0] + "` " + tm.columnType(column[1]) + (column[2] ? " " + column[2] : "")); 111 | } 112 | sql += defs.join(", "); 113 | sql += ')'; 114 | return sql; 115 | }, 116 | 117 | // columns is array of column names, e.g. 118 | // ["id"] 119 | createIndex: function(tableName, columns, options) { 120 | options = options || {}; 121 | return "CREATE "+(options.unique?"UNIQUE ":"")+"INDEX IF NOT EXISTS `" + tableName + "__" + columns.join("_") + 122 | "` ON `" + tableName + "` (" + 123 | columns.map(function(col) { return "`" + col + "`"; }).join(", ") + ")"; 124 | } 125 | }; 126 | 127 | sql.config(persistence, persistence.sqliteDialect); 128 | }; 129 | 130 | -------------------------------------------------------------------------------- /lib/persistence.sync.server.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * Permission is hereby granted, free of charge, to any person 7 | * obtaining a copy of this software and associated documentation 8 | * files (the "Software"), to deal in the Software without 9 | * restriction, including without limitation the rights to use, 10 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the 12 | * Software is furnished to do so, subject to the following 13 | * conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be 16 | * included in all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | * OTHER DEALINGS IN THE SOFTWARE. 26 | * 27 | * 28 | * USAGE: 29 | * Before this code can be used to persist data in the database the file persistence.sync.server.php.sql must be run 30 | * 31 | * This is NOT intended to be used without modification as it implements only the mimimal set of functionality to 32 | * get persistence working. It does not include any kind of security model for example. 33 | */ 34 | 35 | /** 36 | * Requires that the database schema be setup by running: 37 | * 38 | * persistence.sync.server.php.sql 39 | */ 40 | class PersistenceDB { 41 | private $db; 42 | private $persistence_table; 43 | 44 | function __construct(PDO $db, $persistence_table) { 45 | $this->db = $db; 46 | $this->persistence_table = $persistence_table; 47 | } 48 | 49 | public function getObjectChanges($bucket, $since) { 50 | $statement = $this->db->prepare("SELECT content FROM {$this->persistence_table} WHERE bucket=:bucket AND lastUpdated > :since"); 51 | $statement->execute(array(':bucket' => $bucket, ':since' => $since)); 52 | $changes = array(); 53 | foreach ($statement->fetchAll(PDO::FETCH_COLUMN) as $content) { 54 | $change = json_decode($content); 55 | // Don't bother sending removed items to fresh clients 56 | if ($since != 0 || !isset($change->_removed)) { 57 | $changes[] = $change; 58 | } 59 | } 60 | 61 | return $changes; 62 | } 63 | 64 | public function applyObjectChanges($bucket, $now, array $changes) { 65 | $statement = $this->db->prepare(" 66 | INSERT INTO {$this->persistence_table} (id, bucket, lastUpdated, content) 67 | VALUES (:id, :bucket, :lastUpdated, :content) 68 | ON DUPLICATE KEY UPDATE lastUpdated=:lastUpdated, content=:content"); 69 | 70 | foreach ($changes as $change) { 71 | $change->_lastChanged = $now; 72 | $statement->execute(array(':id' => $change->id, ':bucket' => $bucket, ':lastUpdated' => $now, ':content' => json_encode($change))); 73 | } 74 | } 75 | } 76 | 77 | $db = new PersistenceDB(new PDO('mysql:host=localhost;dbname=persistencejs', 'root', ''), 'persistencejs_objects'); 78 | 79 | function http_400() { 80 | header($_SERVER['SERVER_PROTOCOL'] . ' 400 Invalid Request'); 81 | exit(0); 82 | } 83 | 84 | header('Content-Type: applicatin/json'); 85 | 86 | switch (strtoupper($_SERVER['REQUEST_METHOD'])) { 87 | case 'GET': 88 | if (!isset($_GET['bucket']) || !isset($_GET['since'])) 89 | http_400(); 90 | 91 | $bucket = $_GET['bucket']; 92 | $since = isset($_GET['since']) ? $_GET['since'] : 0; 93 | 94 | 95 | $changes = $db->getObjectChanges($bucket, $since); 96 | echo json_encode(array('now' => round(microtime(true) * 1000), "updates" => $changes)); 97 | break; 98 | case 'POST': 99 | $body = file_get_contents('php://input'); 100 | $changes = json_decode($body); 101 | $now = floor(microtime(true)*1000); 102 | $db->applyObjectChanges($bucket, $now, $changes); 103 | echo json_encode(array('now' => $now, "status" => 'ok')); 104 | break; 105 | default: 106 | header($_SERVER['SERVER_PROTOCOL'] . ' 405 Invalid Request'); 107 | } 108 | -------------------------------------------------------------------------------- /demo/jquerymobile/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | jQuery mobile / persistencejs integration 5 | 6 | 7 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 61 | 62 | 63 | 64 |
                65 |
                66 |

                jQuery Mobile Framework

                67 |

                Touch-Optimized Web Framework for Smartphones & Tablets - now with PersistenceJS integration

                68 |

                Alpha Release

                69 |
                70 |
                71 | 77 |
                78 | Reset DB 79 |
                80 | 81 |
                82 |
                83 |

                Form submission

                84 |
                85 |
                86 |
                87 |
                88 |
                89 | 90 | 96 |
                97 | 98 |
                99 |
                100 |
                101 |
                102 | 103 | 104 | -------------------------------------------------------------------------------- /lib/persistence.store.mysql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This back-end depends on the node.js asynchronous MySQL driver as found on: 3 | * http://github.com/felixge/node-mysql/ 4 | * Easy install using npm: 5 | * npm install mysql 6 | */ 7 | var sys = require('sys'); 8 | var sql = require('./persistence.store.sql'); 9 | var mysql = require('mysql'); 10 | 11 | var db, username, password; 12 | 13 | function log(o) { 14 | sys.print(sys.inspect(o) + "\n"); 15 | } 16 | 17 | 18 | exports.config = function(persistence, hostname, port, db, username, password) { 19 | exports.getSession = function(cb) { 20 | var that = {}; 21 | var opts = { 22 | host: hostname, 23 | port: port, 24 | database: db, 25 | user: username, 26 | password: password 27 | }; 28 | var client; 29 | function handleDisconnect() { 30 | connection = mysql.createConnection(opts); 31 | } 32 | if(typeof(mysql.createConnection)=='undefined'){ 33 | client = mysql.createClient(opts); 34 | }else{ 35 | client = new mysql.createConnection(opts); 36 | client.connect(function(err) { 37 | if(err){ 38 | console.error(err); 39 | setTimeout(handleDisconnect, 2000); 40 | } 41 | }); 42 | client.on('error', function(err) { 43 | console.error(err); 44 | if(err.code === 'PROTOCOL_CONNECTION_LOST') { 45 | handleDisconnect(); 46 | } else { 47 | throw err; 48 | } 49 | }); 50 | } 51 | 52 | var session = new persistence.Session(that); 53 | session.transaction = function (explicitCommit, fn) { 54 | if (typeof arguments[0] === "function") { 55 | fn = arguments[0]; 56 | explicitCommit = false; 57 | } 58 | var tx = transaction(client); 59 | if (explicitCommit) { 60 | tx.executeSql("START TRANSACTION", null, function(){ 61 | fn(tx) 62 | }); 63 | } 64 | else 65 | fn(tx); 66 | }; 67 | 68 | session.close = function() { 69 | client.end(); 70 | //conn._connection.destroy(); 71 | }; 72 | session.client = client; 73 | return session; 74 | }; 75 | 76 | function transaction(conn){ 77 | var that = {}; 78 | if(conn.ending) { 79 | throw new Error("Connection has been closed, cannot execute query."); 80 | } 81 | that.executeSql = function(query, args, successFn, errorFn){ 82 | function cb(err, result){ 83 | if (err) { 84 | log(err.message); 85 | that.errorHandler && that.errorHandler(err); 86 | errorFn && errorFn(null, err); 87 | return; 88 | } 89 | if (successFn) { 90 | successFn(result); 91 | } 92 | } 93 | if (persistence.debug) { 94 | sys.print(query + "\n"); 95 | args && args.length > 0 && sys.print(args.join(",") + "\n") 96 | } 97 | if (!args) { 98 | conn.query(query, cb); 99 | } 100 | else { 101 | conn.query(query, args, cb); 102 | } 103 | } 104 | 105 | that.commit = function(session, callback){ 106 | session.flush(that, function(){ 107 | that.executeSql("COMMIT", null, callback); 108 | }) 109 | } 110 | 111 | that.rollback = function(session, callback){ 112 | that.executeSql("ROLLBACK", null, function() { 113 | session.clean(); 114 | callback && callback(); 115 | }); 116 | } 117 | return that; 118 | } 119 | 120 | exports.mysqlDialect = { 121 | // columns is an array of arrays, e.g. 122 | // [["id", "VARCHAR(32)", "PRIMARY KEY"], ["name", "TEXT"]] 123 | createTable: function(tableName, columns) { 124 | var tm = persistence.typeMapper; 125 | var sql = "CREATE TABLE IF NOT EXISTS `" + tableName + "` ("; 126 | var defs = []; 127 | for(var i = 0; i < columns.length; i++) { 128 | var column = columns[i]; 129 | defs.push("`" + column[0] + "` " + tm.columnType(column[1]) + (column[2] ? " " + column[2] : "")); 130 | } 131 | sql += defs.join(", "); 132 | sql += ') ENGINE=InnoDB DEFAULT CHARSET=utf8'; 133 | return sql; 134 | }, 135 | 136 | // columns is array of column names, e.g. 137 | // ["id"] 138 | createIndex: function(tableName, columns, options) { 139 | options = options || {}; 140 | return "CREATE "+(options.unique?"UNIQUE ":"")+"INDEX `" + tableName + "__" + columns.join("_") + 141 | "` ON `" + tableName + "` (" + 142 | columns.map(function(col) { return "`" + col + "`"; }).join(", ") + ")"; 143 | } 144 | }; 145 | 146 | sql.config(persistence, exports.mysqlDialect); 147 | }; 148 | 149 | -------------------------------------------------------------------------------- /docs/sync.md: -------------------------------------------------------------------------------- 1 | persistence.sync.js 2 | =================== 3 | 4 | `persystence.sync.js` is a `persistence.js` plug-in that adds data 5 | synchronization with remote servers. It comes with a client-side 6 | component (`persistence.sync.js`) and a sample server-side component 7 | (`persistence.sync.server.js`) for use with 8 | [node.js](http://nodejs.org). It should be fairly easy to implement 9 | server-components using other languages, any contributions there 10 | are welcome. 11 | 12 | Client-side usage 13 | ----------------- 14 | 15 | After including both `persistence.js` and `persistence.sync.js` in 16 | your page, you can enable syncing on entities individually: 17 | 18 | var Task = persistence.define("Task", { 19 | name: "TEXT", 20 | done: "BOOL" 21 | }); 22 | 23 | Task.enableSync('/taskChanges'); 24 | 25 | The argument passed to `enableSync` is the URI of the sync server 26 | component. 27 | 28 | To initiate a sync, the `EntityName.syncAll(..)` method is used. The method signature 29 | is the following: 30 | 31 | EntityName.syncAll(conflictHandler, successCallback, errorCallback) 32 | 33 | successCallback and errorCallback are optional. successCallback occurs after a 34 | successful sync errorCallback occurs on error (I.E. a non-200 response code). 35 | 36 | conflictHandler is called in the event of a conflict between local and remote data: 37 | 38 | function conflictHandler(conflicts, updatesToPush, callback) { 39 | // Decide what to do with the conflicts here, possibly add to updatesToPush 40 | callback(); 41 | } 42 | 43 | EntityName.syncAll(conflictHandler, function() { 44 | alert('Done!'); 45 | }, errorHandler); 46 | 47 | There are two sample conflict handlers: 48 | 49 | 1. `persistence.sync.preferLocalConflictHandler`, which in case of a 50 | data conflict will always pick the local changes. 51 | 2. `persistence.sync.preferRemoteConflictHandler`, which in case of a 52 | data conflict will always pick the remote changes. 53 | 54 | For instance: 55 | 56 | EntityName.syncAll(persistence.sync.preferLocalConflictHandler, function() { 57 | alert('Done!'); 58 | }, errorCallback); 59 | 60 | Note that you are responsible for syncing all entities and that there 61 | are no database consistencies after a sync, e.g. if you only sync `Task`s that 62 | refer to a `Project` object and that `Project` object has not (yet) been synced, 63 | the database will be (temporarily) inconsistent. 64 | 65 | Server-side (Java, Slim3, AppEngine) 66 | ------------------------------------ 67 | 68 | Roberto Saccon developed a [Java server-side implementation of 69 | persistence sync using the Slim3 70 | framework](http://github.com/rsaccon/Slim3PersistenceSync). 71 | 72 | Server-side (node.js) 73 | --------------------- 74 | 75 | The server must expose a resource located at the given URI that responds to: 76 | 77 | * `GET` requests with a `since=` GET parameter that 78 | will return a JSON object with two properties: 79 | * `now`, the timestamp of the current time at the server (in ms since 1/1/1970) 80 | * `updates`, an array of objects updated since the timestamp 81 | `since`. Each object has at least an `id` and `_lastChange` field 82 | (in the same timestamp format). 83 | 84 | For instance: 85 | 86 | /taskChanges?since=1279888110373 87 | 88 | {"now":1279888110421, 89 | "updates": [ 90 | {"id": "F89F99F7B887423FB4B9C961C3883C0A", 91 | "name": "Main project", 92 | "_lastChange": 1279888110370 93 | } 94 | ] 95 | } 96 | 97 | * `POST` requests with as its body a JSON array of new/updated 98 | objects. Every object needs to have at least an `id` property. 99 | 100 | Example, posting to: 101 | 102 | /taskChanges 103 | 104 | with body: 105 | 106 | [{"id":"BDDF85807155497490C12D6DA3A833F1", 107 | "name":"Locally created project"}] 108 | 109 | The server is supposed to persist these changes (if valid). 110 | Internally the items must be assigned a `_lastChange` timestamp 111 | `TS`. If OK, the server will return a JSON object with "ok" as 112 | `status` and `TS` as `now`. _Note:_ it is important that the 113 | timestamp of all items and the one returned are the same. 114 | 115 | {"status": "ok", 116 | "now": 1279888110797} 117 | 118 | 119 | Server-side filtering 120 | ------------------- 121 | 122 | In certain circumstances, it is not necessary or desired to push all records down to a client. A standard GET URI looks like this: 123 | 124 | app.get('/taskupdates', function(req, res) { 125 | persistenceSync.pushUpdates(req.conn, req.tx, Task, req.query.since, function(updates){ 126 | res.send(updates); 127 | }); 128 | }); 129 | 130 | The third parameter in `pushUpdates` is the Entity model. If you wish to filter, simply pass a Query Collection in its place. 131 | 132 | app.get('/taskupdates', function(req, res) { 133 | var taskCollection = Task.all(req.conn).filter('done','=',false); 134 | persistenceSync.pushUpdates(req.conn, req.tx, taskCollection, req.query.since, function(updates){ 135 | res.send(updates); 136 | }); 137 | }); 138 | 139 | 140 | 141 | Limitations 142 | ----------- 143 | 144 | * This synchronization library synchronizes on a per-object granularity. It 145 | does not keep exact changes on a per-property basis, therefore 146 | conflicts may be introduced that need to be resolved. 147 | * It does not synchronize many-to-many relationships at this point 148 | * There may still be many bugs, I'm not sure. 149 | 150 | -------------------------------------------------------------------------------- /test/node/test.sync.server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2010 Zef Hemel 3 | * 4 | * Permission is hereby granted, free of charge, to any person 5 | * obtaining a copy of this software and associated documentation 6 | * files (the "Software"), to deal in the Software without 7 | * restriction, including without limitation the rights to use, 8 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the 10 | * Software is furnished to do so, subject to the following 11 | * conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be 14 | * included in all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | * OTHER DEALINGS IN THE SOFTWARE. 24 | * 25 | * 26 | * Requirements: 27 | * node.js 28 | * npm install connect 29 | * npm install express 30 | */ 31 | var sys = require('sys'); 32 | var connect = require('connect'); 33 | var express = require('express'); 34 | 35 | var persistence = require('../../lib/persistence').persistence; 36 | var persistenceStore = require('../../lib/persistence.store.mysql'); 37 | var persistenceSync = require('../../lib/persistence.sync.server'); 38 | 39 | // Database configuration 40 | persistenceStore.config(persistence, 'localhost', 3306, 'synctest', 'test', 'test'); 41 | 42 | // Switch off query logging: 43 | //persistence.db.log = false; 44 | 45 | function log(o) { 46 | sys.print(sys.inspect(o) + "\n"); 47 | } 48 | 49 | persistenceSync.config(persistence); 50 | 51 | // Data model 52 | var Project = persistence.define('Project', { 53 | name: "TEXT" 54 | }); 55 | 56 | var Task = persistence.define('Task', { 57 | name: "TEXT", 58 | done: "BOOL" 59 | }); 60 | 61 | var Tag = persistence.define('Tag', { 62 | name: "TEXT" 63 | }); 64 | 65 | Task.hasMany('tags', Tag, 'tasks'); 66 | Tag.hasMany('tasks', Task, 'tags'); 67 | 68 | Project.hasMany('tasks', Task, 'project'); 69 | 70 | Project.enableSync(); 71 | Task.enableSync(); 72 | Tag.enableSync(); 73 | 74 | var app = express.createServer( 75 | //connect.logger(), 76 | connect.bodyDecoder(), 77 | connect.staticProvider('../browser'), 78 | function(req, res, next) { 79 | var end = res.end; 80 | 81 | req.conn = persistenceStore.getSession(); 82 | res.end = function() { 83 | req.conn.close(); 84 | end.apply(res, arguments); 85 | }; 86 | req.conn.transaction(function(tx) { 87 | req.tx = tx; 88 | next(); 89 | }); 90 | } 91 | ); 92 | 93 | function generateDummyData(session) { 94 | var p = new Project(session, {name: "Main project"}); 95 | session.add(p); 96 | for(var i = 0; i < 25; i++) { 97 | var t = new Task(session, {name: "Task " + i, done: false}); 98 | p.tasks.add(t); 99 | } 100 | } 101 | 102 | // Actions 103 | app.get('/reset', function(req, res) { 104 | req.conn.reset(req.tx, function() { 105 | req.conn.schemaSync(req.tx, function() { 106 | generateDummyData(req.conn); 107 | req.conn.flush(req.tx, function() { 108 | res.send({status: "ok"}); 109 | }); 110 | }); 111 | }); 112 | }); 113 | 114 | app.get('/projectUpdates', function(req, res) { 115 | persistenceSync.pushUpdates(req.conn, req.tx, Project, req.query.since, function(updates) { 116 | res.send(updates); 117 | }); 118 | }); 119 | 120 | app.post('/projectUpdates', function(req, res) { 121 | persistenceSync.receiveUpdates(req.conn, req.tx, Project, req.body, function(result) { 122 | res.send(result); 123 | }); 124 | }); 125 | 126 | app.get('/taskUpdates', function(req, res) { 127 | persistenceSync.pushUpdates(req.conn, req.tx, Task, req.query.since, function(updates) { 128 | res.send(updates); 129 | }); 130 | }); 131 | 132 | app.post('/taskUpdates', function(req, res) { 133 | persistenceSync.receiveUpdates(req.conn, req.tx, Task, req.body, function(result) { 134 | res.send(result); 135 | }); 136 | }); 137 | 138 | app.get('/tagUpdates', function(req, res) { 139 | persistenceSync.pushUpdates(req.conn, req.tx, Tag, req.query.since, function(updates) { 140 | res.send(updates); 141 | }); 142 | }); 143 | 144 | app.post('/tagUpdates', function(req, res) { 145 | persistenceSync.receiveUpdates(req.conn, req.tx, Tag, req.body, function(result) { 146 | res.send(result); 147 | }); 148 | }); 149 | 150 | app.get('/markAllDone', function(req, res) { 151 | Task.all(req.conn).list(req.tx, function(tasks) { 152 | tasks.forEach(function(task) { 153 | task.done = true; 154 | }); 155 | req.conn.flush(req.tx, function() { 156 | res.send({status: 'ok'}); 157 | }); 158 | }); 159 | }); 160 | 161 | app.get('/markAllUndone', function(req, res) { 162 | Task.all(req.conn).list(req.tx, function(tasks) { 163 | tasks.forEach(function(task) { 164 | task.done = false; 165 | }); 166 | req.conn.flush(req.tx, function() { 167 | res.send({status: 'ok'}); 168 | }); 169 | }); 170 | }); 171 | 172 | app.listen(8888); 173 | 174 | console.log('Server running at http://127.0.0.1:8888/'); 175 | -------------------------------------------------------------------------------- /test/node/partial.sync.schema.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- Table structure for table `_syncremovedobject` 3 | -- 4 | 5 | DROP TABLE IF EXISTS `_syncremovedobject`; 6 | /*!40101 SET @saved_cs_client = @@character_set_client */; 7 | /*!40101 SET character_set_client = utf8 */; 8 | CREATE TABLE `_syncremovedobject` ( 9 | `entity` varchar(255) DEFAULT NULL, 10 | `objectId` varchar(32) DEFAULT NULL, 11 | `date` bigint(20) DEFAULT NULL, 12 | `id` varchar(32) NOT NULL, 13 | PRIMARY KEY (`id`) 14 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 15 | /*!40101 SET character_set_client = @saved_cs_client */; 16 | 17 | -- 18 | -- Dumping data for table `_syncremovedobject` 19 | -- 20 | 21 | LOCK TABLES `_syncremovedobject` WRITE; 22 | /*!40000 ALTER TABLE `_syncremovedobject` DISABLE KEYS */; 23 | /*!40000 ALTER TABLE `_syncremovedobject` ENABLE KEYS */; 24 | UNLOCK TABLES; 25 | 26 | -- 27 | -- Table structure for table `class` 28 | -- 29 | 30 | DROP TABLE IF EXISTS `class`; 31 | /*!40101 SET @saved_cs_client = @@character_set_client */; 32 | /*!40101 SET character_set_client = utf8 */; 33 | CREATE TABLE `class` ( 34 | `class_id` int(11) NOT NULL AUTO_INCREMENT, 35 | `teacher_id` int(11) NOT NULL, 36 | `class_name` varchar(120) NOT NULL, 37 | `id` varchar(32) DEFAULT NULL, 38 | `_lastChange` bigint(20) DEFAULT NULL, 39 | PRIMARY KEY (`class_id`), 40 | KEY `fk_teacher_id` (`teacher_id`), 41 | CONSTRAINT `fk_teacher_id` FOREIGN KEY (`teacher_id`) REFERENCES `teacher` (`teacher_id`) 42 | ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=latin1; 43 | /*!40101 SET character_set_client = @saved_cs_client */; 44 | 45 | -- 46 | -- Dumping data for table `class` 47 | -- 48 | 49 | LOCK TABLES `class` WRITE; 50 | /*!40000 ALTER TABLE `class` DISABLE KEYS */; 51 | INSERT INTO `class` VALUES (1,1,'Computer Systems','d2ffcf2a33c911e08f5eb58579ce9ead',1297199638),(2,2,'Linux/Dark Arts','d2ffd2e533c911e08f5eb58579ce9ead',1297199638); 52 | /*!40000 ALTER TABLE `class` ENABLE KEYS */; 53 | UNLOCK TABLES; 54 | 55 | -- 56 | -- Table structure for table `student` 57 | -- 58 | 59 | DROP TABLE IF EXISTS `student`; 60 | /*!40101 SET @saved_cs_client = @@character_set_client */; 61 | /*!40101 SET character_set_client = utf8 */; 62 | CREATE TABLE `student` ( 63 | `student_id` int(11) NOT NULL AUTO_INCREMENT, 64 | `first_name` varchar(60) NOT NULL, 65 | `last_name` varchar(60) NOT NULL, 66 | `id` varchar(32) DEFAULT NULL, 67 | `_lastChange` bigint(20) DEFAULT NULL, 68 | PRIMARY KEY (`student_id`) 69 | ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=latin1; 70 | /*!40101 SET character_set_client = @saved_cs_client */; 71 | 72 | -- 73 | -- Dumping data for table `student` 74 | -- 75 | 76 | LOCK TABLES `student` WRITE; 77 | /*!40000 ALTER TABLE `student` DISABLE KEYS */; 78 | INSERT INTO `student` VALUES (1,'John','Doe','cd6068c833c911e08f5eb58579ce9ead',1297199634),(2,'Jane','Doe','cd606df733c911e08f5eb58579ce9ead',1297199634),(3,'Chris','Farmer','cd6070b733c911e08f5eb58579ce9ead',1297199634),(4,'Bob','Jones','cd60738433c911e08f5eb58579ce9ead',1297199634),(5,'Christine','Alexander','cd60761833c911e08f5eb58579ce9ead',1297199634),(6,'Abe','Lincoln','cd60786e33c911e08f5eb58579ce9ead',1297199634),(7,'Adrian','Doty','cd607af033c911e08f5eb58579ce9ead',1297199634),(8,'Eileen','Nyman','cd607dbe33c911e08f5eb58579ce9ead',1297199634),(9,'Amber','Chase','cd60807033c911e08f5eb58579ce9ead',1297199634); 79 | /*!40000 ALTER TABLE `student` ENABLE KEYS */; 80 | UNLOCK TABLES; 81 | 82 | -- 83 | -- Table structure for table `student_class` 84 | -- 85 | 86 | DROP TABLE IF EXISTS `student_class`; 87 | /*!40101 SET @saved_cs_client = @@character_set_client */; 88 | /*!40101 SET character_set_client = utf8 */; 89 | CREATE TABLE `student_class` ( 90 | `student_id` int(11) NOT NULL, 91 | `class_id` int(11) NOT NULL, 92 | `id` varchar(32) DEFAULT NULL, 93 | `_lastChange` bigint(20) DEFAULT NULL, 94 | KEY `fk_student_id` (`student_id`), 95 | KEY `fk_class_id` (`class_id`), 96 | CONSTRAINT `fk_class_id` FOREIGN KEY (`class_id`) REFERENCES `class` (`class_id`) ON DELETE CASCADE ON UPDATE CASCADE, 97 | CONSTRAINT `fk_student_id` FOREIGN KEY (`student_id`) REFERENCES `student` (`student_id`) ON DELETE CASCADE ON UPDATE CASCADE 98 | ) ENGINE=InnoDB DEFAULT CHARSET=latin1; 99 | /*!40101 SET character_set_client = @saved_cs_client */; 100 | 101 | -- 102 | -- Dumping data for table `student_class` 103 | -- 104 | 105 | LOCK TABLES `student_class` WRITE; 106 | /*!40000 ALTER TABLE `student_class` DISABLE KEYS */; 107 | INSERT INTO `student_class` VALUES (1,1,'d61678e133c911e08f5eb58579ce9ead',1297199644),(2,1,'d6167d0c33c911e08f5eb58579ce9ead',1297199644),(3,1,'d6167f2833c911e08f5eb58579ce9ead',1297199644),(4,1,'d616812c33c911e08f5eb58579ce9ead',1297199644),(5,1,'d616833c33c911e08f5eb58579ce9ead',1297199644),(6,2,'d616854733c911e08f5eb58579ce9ead',1297199644),(7,2,'d616874e33c911e08f5eb58579ce9ead',1297199644),(8,2,'d616895933c911e08f5eb58579ce9ead',1297199644),(9,2,'d6168b6c33c911e08f5eb58579ce9ead',1297199644); 108 | /*!40000 ALTER TABLE `student_class` ENABLE KEYS */; 109 | UNLOCK TABLES; 110 | 111 | -- 112 | -- Table structure for table `teacher` 113 | -- 114 | 115 | DROP TABLE IF EXISTS `teacher`; 116 | /*!40101 SET @saved_cs_client = @@character_set_client */; 117 | /*!40101 SET character_set_client = utf8 */; 118 | CREATE TABLE `teacher` ( 119 | `teacher_id` int(11) NOT NULL AUTO_INCREMENT, 120 | `first_name` varchar(60) NOT NULL, 121 | `last_name` varchar(60) NOT NULL, 122 | `id` varchar(32) DEFAULT NULL, 123 | `_lastChange` bigint(20) DEFAULT NULL, 124 | PRIMARY KEY (`teacher_id`) 125 | ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=latin1; 126 | /*!40101 SET character_set_client = @saved_cs_client */; 127 | 128 | -- 129 | -- Dumping data for table `teacher` 130 | -- 131 | 132 | LOCK TABLES `teacher` WRITE; 133 | /*!40000 ALTER TABLE `teacher` DISABLE KEYS */; 134 | INSERT INTO `teacher` VALUES (1,'Mark','Price','d1521f2933c911e08f5eb58579ce9ead',1297199641),(2,'Tony','Basil','d152235d33c911e08f5eb58579ce9ead',1297199641); 135 | /*!40000 ALTER TABLE `teacher` ENABLE KEYS */; 136 | UNLOCK TABLES; 137 | /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; 138 | 139 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; 140 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; 141 | /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; 142 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 143 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 144 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 145 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; 146 | -------------------------------------------------------------------------------- /lib/persistence.store.titanium.js: -------------------------------------------------------------------------------- 1 | try { 2 | if(!window) { 3 | window = {}; 4 | //exports.console = console; 5 | } 6 | } catch(e) { 7 | window = {}; 8 | exports.console = console; 9 | } 10 | 11 | var persistence = (window && window.persistence) ? window.persistence : {}; 12 | 13 | if(!persistence.store) { 14 | persistence.store = {}; 15 | } 16 | 17 | persistence.store.titanium = {}; 18 | 19 | persistence.store.titanium.config = function(persistence, dbname) { 20 | var conn = null; 21 | 22 | /** 23 | * Create a transaction 24 | * 25 | * @param callback, 26 | * the callback function to be invoked when the transaction 27 | * starts, taking the transaction object as argument 28 | */ 29 | persistence.transaction = function (callback) { 30 | if(!conn) { 31 | throw new Error("No ongoing database connection, please connect first."); 32 | } else { 33 | conn.transaction(callback); 34 | } 35 | }; 36 | 37 | ////////// Low-level database interface, abstracting from HTML5 and Gears databases \\\\ 38 | persistence.db = persistence.db || {}; 39 | 40 | persistence.db.conn = null; 41 | 42 | persistence.db.titanium = {}; 43 | 44 | persistence.db.titanium.connect = function (dbname) { 45 | var that = {}; 46 | var conn = Titanium.Database.open(dbname); 47 | 48 | that.transaction = function (fn) { 49 | fn(persistence.db.titanium.transaction(conn)); 50 | }; 51 | return that; 52 | }; 53 | 54 | persistence.db.titanium.transaction = function (conn) { 55 | var that = {}; 56 | that.executeSql = function (query, args, successFn, errorFn) { 57 | 58 | if(persistence.debug) { 59 | console.log(query, args); 60 | } 61 | try { 62 | var executeVarArgs = [query]; 63 | if (args) { 64 | executeVarArgs = executeVarArgs.concat(args); 65 | }; 66 | var rs = Function.apply.call(conn.execute, conn, executeVarArgs); 67 | if (successFn) { 68 | var results = []; 69 | if (rs) { 70 | while (rs.isValidRow()) { 71 | var result = {}; 72 | for ( var i = 0; i < rs.fieldCount(); i++) { 73 | result[rs.fieldName(i)] = rs.field(i); 74 | } 75 | results.push(result); 76 | rs.next(); 77 | } 78 | rs.close(); 79 | }; 80 | successFn(results); 81 | } 82 | } catch(e) { 83 | if (errorFn) { 84 | errorFn(null, e); 85 | }; 86 | } 87 | }; 88 | return that; 89 | }; 90 | 91 | ///////////////////////// SQLite dialect 92 | 93 | persistence.store.titanium.sqliteDialect = { 94 | // columns is an array of arrays, e.g. 95 | // [["id", "VARCHAR(32)", "PRIMARY KEY"], ["name", "TEXT"]] 96 | createTable: function(tableName, columns) { 97 | var tm = persistence.typeMapper; 98 | var sql = "CREATE TABLE IF NOT EXISTS `" + tableName + "` ("; 99 | var defs = []; 100 | for(var i = 0; i < columns.length; i++) { 101 | var column = columns[i]; 102 | defs.push("`" + column[0] + "` " + tm.columnType(column[1]) + (column[2] ? " " + column[2] : "")); 103 | } 104 | sql += defs.join(", "); 105 | sql += ')'; 106 | return sql; 107 | }, 108 | 109 | // columns is array of column names, e.g. 110 | // ["id"] 111 | createIndex: function(tableName, columns, options) { 112 | options = options || {}; 113 | return "CREATE "+(options.unique?"UNIQUE ":"")+"INDEX IF NOT EXISTS `" + tableName + "__" + columns.join("_") + 114 | "` ON `" + tableName + "` (" + 115 | columns.map(function(col) { return "`" + col + "`"; }).join(", ") + ")"; 116 | }, 117 | 118 | typeMapper: { 119 | idType: persistence.store.sql.defaultTypeMapper.idType, 120 | classNameType: persistence.store.sql.defaultTypeMapper.classNameType, 121 | inVar: persistence.store.sql.defaultTypeMapper.inVar, 122 | outVar: persistence.store.sql.defaultTypeMapper.outVar, 123 | outId: persistence.store.sql.defaultTypeMapper.outId, 124 | inIdVar: persistence.store.sql.defaultTypeMapper.inIdVar, 125 | outIdVar: persistence.store.sql.defaultTypeMapper.outIdVar, 126 | entityIdToDbId: persistence.store.sql.defaultTypeMapper.entityIdToDbId, 127 | zeroPaddingMap: ['0000000000000000', 128 | '000000000000000', 129 | '00000000000000', 130 | '0000000000000', 131 | '000000000000', 132 | '00000000000', 133 | '0000000000', 134 | '000000000', 135 | '00000000', 136 | '0000000', 137 | '000000', 138 | '00000', 139 | '0000', 140 | '000', 141 | '00', 142 | '0'], 143 | zeroPadded: function(val) { 144 | var result = val.toString(); 145 | if (result.length < 16) { 146 | return persistence.store.titanium.sqliteDialect.typeMapper.zeroPaddingMap[result.length] + result; 147 | } else { 148 | return result; 149 | }; 150 | }, 151 | columnType: function(type) { 152 | if (type === 'BIGINT') { 153 | return 'TEXT'; 154 | } else { 155 | return persistence.store.sql.defaultTypeMapper.columnType(type); 156 | }; 157 | }, 158 | dbValToEntityVal: function(val, type){ 159 | if (val === null || val === undefined) { 160 | return val; 161 | } else if (type === 'BIGIN') { 162 | return parseInt(val); 163 | } else { 164 | return persistence.store.sql.defaultTypeMapper.dbValToEntityVal(val, type); 165 | } 166 | }, 167 | entityValToDbVal: function(val, type){ 168 | if (val === undefined || val === null) { 169 | return null; 170 | } else if (type === 'BIGINT') { 171 | return persistence.store.titanium.sqliteDialect.typeMapper.zeroPadded(val); 172 | } else { 173 | return persistence.store.sql.defaultTypeMapper.entityValToDbVal(val, type); 174 | }; 175 | } 176 | } 177 | }; 178 | 179 | // Configure persistence for generic sql persistence, using sqliteDialect 180 | persistence.store.sql.config(persistence, persistence.store.titanium.sqliteDialect); 181 | 182 | // Make the connection 183 | conn = persistence.db.titanium.connect(dbname); 184 | if(!conn) { 185 | throw new Error("No supported database found"); 186 | } 187 | }; 188 | 189 | try { 190 | exports.persistence = persistence; 191 | } catch(e) {} 192 | -------------------------------------------------------------------------------- /test/browser/test.mixin.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | persistence.store.websql.config(persistence, 'persistencetest', 'My db', 5 * 1024 * 1024); 3 | //persistence.store.memory.config(persistence); 4 | persistence.debug = true; 5 | //persistence.debug = false; 6 | 7 | var startTime = new Date().getTime(); 8 | 9 | var Project = persistence.define('Project', { 10 | name: "TEXT" 11 | }); 12 | 13 | var Task = persistence.define('Task', { 14 | name: "TEXT" 15 | }); 16 | 17 | var Tag = persistence.define('Tag', { 18 | name: "TEXT" 19 | }); 20 | 21 | Task.hasMany('tags', Tag, 'tasks'); 22 | Tag.hasMany('tasks', Task, 'tags'); 23 | 24 | Project.hasMany('tasks', Task, 'project'); 25 | 26 | var Note = persistence.define('Note', { 27 | text: "TEXT" 28 | }); 29 | 30 | var Annotatable = persistence.defineMixin('Annotatable', { 31 | lastAnnotated: "DATE" 32 | }); 33 | 34 | Annotatable.hasMany('notes', Note, 'annotated'); 35 | 36 | Task.hasMany('tags', Tag, 'tasks'); 37 | Tag.hasMany('tasks', Task, 'tags'); 38 | 39 | Project.hasMany('tasks', Task, 'project'); 40 | 41 | Task.is(Annotatable); 42 | Project.is(Annotatable); 43 | 44 | var M1 = persistence.defineMixin('M1', { 45 | seq: "INT", 46 | m1: "TEXT" 47 | }); 48 | 49 | var M2 = persistence.defineMixin('M2', { 50 | seq: "INT", 51 | m2: "TEXT" 52 | }); 53 | 54 | M1.hasOne('oneM2', M2); 55 | M1.hasMany('manyM2', M2, 'oneM1'); 56 | M1.hasMany('manyManyM2', M2, 'manyManyM1'); 57 | M2.hasMany('manyManyM1', M1, 'manyManyM2'); 58 | 59 | var A1 = persistence.define('A1', { 60 | a1: 'TEXT' 61 | }); 62 | var A2 = persistence.define('A2', { 63 | a2: 'TEXT' 64 | }); 65 | var B1 = persistence.define('B1', { 66 | b1: 'TEXT' 67 | }); 68 | var B2 = persistence.define('B2', { 69 | b2: 'TEXT' 70 | }); 71 | 72 | A1.is(M1); 73 | A2.is(M2); 74 | B1.is(M1); 75 | B2.is(M2); 76 | 77 | window.Project = Project; 78 | window.Task = Task 79 | window.Project = Project; 80 | 81 | module("Setup"); 82 | 83 | asyncTest("setting up database", 1, function(){ 84 | persistence.schemaSync(function(tx){ 85 | ok(true, 'schemaSync called callback function'); 86 | start(); 87 | }); 88 | }); 89 | 90 | module("Annotatable mixin", { 91 | setup: function() { 92 | stop(); 93 | persistence.reset(function() { 94 | persistence.schemaSync(start); 95 | }); 96 | } 97 | }); 98 | 99 | 100 | asyncTest("basic mixin", 7, function(){ 101 | var now = new Date(); 102 | now.setMilliseconds(0); 103 | 104 | var p = new Project({ 105 | name: "project p" 106 | }); 107 | persistence.add(p); 108 | var n1 = new Note({ 109 | text: "note 1" 110 | }); 111 | var n2 = new Note({ 112 | text: "note 2" 113 | }); 114 | p.notes.add(n1); 115 | n2.annotated = p; 116 | p.lastAnnotated = now; 117 | persistence.flush(function(){ 118 | Project.all().list(function(projects){ 119 | persistence.clean(); 120 | equals(projects.length, 1) 121 | var p = projects[0]; 122 | p.notes.order('text', true).list(function(notes){ 123 | equals(notes.length, 2); 124 | equals(notes[0].text, "note 1"); 125 | equals(notes[1].text, "note 2"); 126 | notes[0].fetch("annotated", function(source){ 127 | equals(p.id, source.id); 128 | equals(typeof source.lastAnnotated, typeof now); 129 | equals(source.lastAnnotated.getTime(), now.getTime()); 130 | start(); 131 | }); 132 | }); 133 | }); 134 | }); 135 | }); 136 | 137 | asyncTest("many to many with mixins", 17, function(){ 138 | var a1 = new A1({ 139 | seq: 1, 140 | a1: "a1" 141 | }); 142 | var b1 = new B1({ 143 | seq: 2, 144 | b1: "b1" 145 | }); 146 | var a2 = new A2({ 147 | seq: 3, 148 | a2: "a2" 149 | }); 150 | var a2x = new A2({ 151 | seq: 4, 152 | a2: "a2x" 153 | }); 154 | var a2y = new A2({ 155 | seq: 5, 156 | a2: "a2y" 157 | }); 158 | var b2x = new B2({ 159 | seq: 6, 160 | b2: "b2x" 161 | }); 162 | var b2y = new B2({ 163 | seq: 7, 164 | b2: "b2y" 165 | }); 166 | persistence.add(a1); 167 | a1.oneM2 = b2x; 168 | a1.manyM2.add(a2x); 169 | a1.manyM2.add(b2x); 170 | persistence.flush(function(){ 171 | persistence.clean(); 172 | A1.all().list(function(a1s){ 173 | equals(a1s.length, 1, "A1 list ok") 174 | var a1 = a1s[0]; 175 | a1.fetch("oneM2", function(m2){ 176 | ok(m2 != null, "oneM2 not null"); 177 | equals(m2.b2, "b2x", "oneM2 ok"); 178 | a1.manyM2.order('seq', true).list(function(m2s){ 179 | equals(m2s.length, 2, "manyM2 length ok"); 180 | equals(m2s[0].a2, "a2x", "manyM2[0] ok"); 181 | equals(m2s[1].b2, "b2x", "manyM2[1] ok"); 182 | m2s[1].fetch("oneM1", function(m1){ 183 | ok(m1 != null, "manyM2[1].oneM1 not null"); 184 | ok(m1.a1, "a1", "manyM2[1].oneM1 ok"); 185 | a1.manyManyM2.add(a2x); 186 | a1.manyManyM2.add(b2x); 187 | persistence.add(b2y); 188 | b2y.manyManyM1.add(a1); 189 | b2y.manyManyM1.add(b1); 190 | persistence.flush(function(){ 191 | persistence.clean(); 192 | A1.all().list(function(a1s){ 193 | equals(a1s.length, 1, "A1 list ok") 194 | var a1 = a1s[0]; 195 | a1.manyManyM2.order('seq', true).list(function(m2s){ 196 | equals(m2s.length, 3, "manyManyM2 length ok"); 197 | equals(m2s[0].a2, "a2x", "manyManyM2[0] ok"); 198 | equals(m2s[1].b2, "b2x", "manyManyM2[1] ok"); 199 | equals(m2s[2].b2, "b2y", "manyManyM2[2] ok"); 200 | m2s[2].manyManyM1.order('seq', true).list(function(m1s){ 201 | equals(m1s.length, 2, "manyManyM1 length ok"); 202 | equals(m1s[0].a1, "a1", "manyManyM1[0] ok"); 203 | equals(m1s[1].b1, "b1", "manyManyM1[1] ok"); 204 | a1.manyManyM2.count(function(count){ 205 | equals(count, 3, "count ok on polymorphic list"); 206 | //a1.manyManyM2.destroyAll(function(){ 207 | start(); 208 | //}) 209 | }); 210 | }) 211 | }); 212 | }); 213 | }) 214 | }); 215 | }); 216 | }); 217 | }); 218 | }); 219 | }); 220 | 221 | 222 | }); 223 | -------------------------------------------------------------------------------- /lib/persistence.store.websql.js: -------------------------------------------------------------------------------- 1 | try { 2 | if(!window) { 3 | window = {}; 4 | //exports.console = console; 5 | } 6 | } catch(e) { 7 | window = {}; 8 | exports.console = console; 9 | } 10 | 11 | var persistence = (window && window.persistence) ? window.persistence : {}; 12 | 13 | if(!persistence.store) { 14 | persistence.store = {}; 15 | } 16 | 17 | persistence.store.websql = {}; 18 | 19 | 20 | persistence.store.websql.config = function(persistence, dbname, description, size) { 21 | var conn = null; 22 | 23 | /** 24 | * Create a transaction 25 | * 26 | * @param callback, 27 | * the callback function to be invoked when the transaction 28 | * starts, taking the transaction object as argument 29 | */ 30 | persistence.transaction = function (callback) { 31 | if(!conn) { 32 | throw new Error("No ongoing database connection, please connect first."); 33 | } else { 34 | conn.transaction(callback); 35 | } 36 | }; 37 | 38 | ////////// Low-level database interface, abstracting from HTML5 and Gears databases \\\\ 39 | persistence.db = persistence.db || {}; 40 | 41 | persistence.db.implementation = "unsupported"; 42 | persistence.db.conn = null; 43 | 44 | // window object does not exist on Qt Declarative UI (http://doc.trolltech.org/4.7-snapshot/declarativeui.html) 45 | if (window && window.openDatabase) { 46 | persistence.db.implementation = "html5"; 47 | } else if (window && window.google && google.gears) { 48 | persistence.db.implementation = "gears"; 49 | } else { 50 | try { 51 | if (openDatabaseSync) { 52 | // TODO: find a browser that implements openDatabaseSync and check out if 53 | // it is attached to the window or some other object 54 | persistence.db.implementation = "html5-sync"; 55 | } 56 | } catch(e) { 57 | } 58 | } 59 | 60 | persistence.db.html5 = {}; 61 | 62 | persistence.db.html5.connect = function (dbname, description, size) { 63 | var that = {}; 64 | var conn = openDatabase(dbname, '1.0', description, size); 65 | 66 | that.transaction = function (fn) { 67 | return conn.transaction(function (sqlt) { 68 | return fn(persistence.db.html5.transaction(sqlt)); 69 | }); 70 | }; 71 | return that; 72 | }; 73 | 74 | persistence.db.html5.transaction = function (t) { 75 | var that = {}; 76 | that.executeSql = function (query, args, successFn, errorFn) { 77 | if(persistence.debug) { 78 | console.log(query, args); 79 | } 80 | t.executeSql(query, args, function (_, result) { 81 | if (successFn) { 82 | var results = []; 83 | for ( var i = 0; i < result.rows.length; i++) { 84 | results.push(result.rows.item(i)); 85 | } 86 | successFn(results); 87 | } 88 | }, errorFn); 89 | }; 90 | return that; 91 | }; 92 | 93 | persistence.db.html5Sync = {}; 94 | 95 | persistence.db.html5Sync.connect = function (dbname, description, size) { 96 | var that = {}; 97 | var conn = openDatabaseSync(dbname, '1.0', description, size); 98 | 99 | that.transaction = function (fn) { 100 | return conn.transaction(function (sqlt) { 101 | return fn(persistence.db.html5Sync.transaction(sqlt)); 102 | }); 103 | }; 104 | return that; 105 | }; 106 | 107 | persistence.db.html5Sync.transaction = function (t) { 108 | var that = {}; 109 | that.executeSql = function (query, args, successFn, errorFn) { 110 | if (args == null) args = []; 111 | 112 | if(persistence.debug) { 113 | console.log(query, args); 114 | } 115 | 116 | var result = t.executeSql(query, args); 117 | if (result) { 118 | if (successFn) { 119 | var results = []; 120 | for ( var i = 0; i < result.rows.length; i++) { 121 | results.push(result.rows.item(i)); 122 | } 123 | successFn(results); 124 | } 125 | } 126 | }; 127 | return that; 128 | }; 129 | 130 | persistence.db.gears = {}; 131 | 132 | persistence.db.gears.connect = function (dbname) { 133 | var that = {}; 134 | var conn = google.gears.factory.create('beta.database'); 135 | conn.open(dbname); 136 | 137 | that.transaction = function (fn) { 138 | fn(persistence.db.gears.transaction(conn)); 139 | }; 140 | return that; 141 | }; 142 | 143 | persistence.db.gears.transaction = function (conn) { 144 | var that = {}; 145 | that.executeSql = function (query, args, successFn, errorFn) { 146 | if(persistence.debug) { 147 | console.log(query, args); 148 | } 149 | var rs = conn.execute(query, args); 150 | if (successFn) { 151 | var results = []; 152 | while (rs.isValidRow()) { 153 | var result = {}; 154 | for ( var i = 0; i < rs.fieldCount(); i++) { 155 | result[rs.fieldName(i)] = rs.field(i); 156 | } 157 | results.push(result); 158 | rs.next(); 159 | } 160 | successFn(results); 161 | } 162 | }; 163 | return that; 164 | }; 165 | 166 | persistence.db.connect = function (dbname, description, size) { 167 | if (persistence.db.implementation == "html5") { 168 | return persistence.db.html5.connect(dbname, description, size); 169 | } else if (persistence.db.implementation == "html5-sync") { 170 | return persistence.db.html5Sync.connect(dbname, description, size); 171 | } else if (persistence.db.implementation == "gears") { 172 | return persistence.db.gears.connect(dbname); 173 | } 174 | }; 175 | 176 | ///////////////////////// SQLite dialect 177 | 178 | persistence.store.websql.sqliteDialect = { 179 | // columns is an array of arrays, e.g. 180 | // [["id", "VARCHAR(32)", "PRIMARY KEY"], ["name", "TEXT"]] 181 | createTable: function(tableName, columns) { 182 | var tm = persistence.typeMapper; 183 | var sql = "CREATE TABLE IF NOT EXISTS `" + tableName + "` ("; 184 | var defs = []; 185 | for(var i = 0; i < columns.length; i++) { 186 | var column = columns[i]; 187 | defs.push("`" + column[0] + "` " + tm.columnType(column[1]) + (column[2] ? " " + column[2] : "")); 188 | } 189 | sql += defs.join(", "); 190 | sql += ')'; 191 | return sql; 192 | }, 193 | 194 | // columns is array of column names, e.g. 195 | // ["id"] 196 | createIndex: function(tableName, columns, options) { 197 | options = options || {}; 198 | return "CREATE "+(options.unique?"UNIQUE ":"")+"INDEX IF NOT EXISTS `" + tableName + "__" + columns.join("_") + 199 | "` ON `" + tableName + "` (" + 200 | columns.map(function(col) { return "`" + col + "`"; }).join(", ") + ")"; 201 | } 202 | }; 203 | 204 | // Configure persistence for generic sql persistence, using sqliteDialect 205 | persistence.store.sql.config(persistence, persistence.store.websql.sqliteDialect); 206 | 207 | // Make the connection 208 | conn = persistence.db.connect(dbname, description, size); 209 | if(!conn) { 210 | throw new Error("No supported database found in this browser."); 211 | } 212 | }; 213 | 214 | try { 215 | exports.persistence = persistence; 216 | } catch(e) {} 217 | -------------------------------------------------------------------------------- /test/node/node-blog.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2010 Zef Hemel 3 | * 4 | * Permission is hereby granted, free of charge, to any person 5 | * obtaining a copy of this software and associated documentation 6 | * files (the "Software"), to deal in the Software without 7 | * restriction, including without limitation the rights to use, 8 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the 10 | * Software is furnished to do so, subject to the following 11 | * conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be 14 | * included in all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | * OTHER DEALINGS IN THE SOFTWARE. 24 | * 25 | * 26 | * USAGE: 27 | * On first run, be sure to initialize the database first: http://localhost:8888/init 28 | * otherwise the application will hang (because the select query fails). After that, 29 | * just visit http://localhost:8888/ 30 | */ 31 | var sys = require('sys'); 32 | var parseUrl = require('url').parse; 33 | 34 | var persistence = require('persistencejs/persistence').persistence; 35 | var persistenceStore = require('persistencejs/persistence.store.mysql'); 36 | 37 | // Database configuration 38 | persistenceStore.config(persistence, 'localhost', 3306, 'nodejs_mysql', 'test', 'test'); 39 | 40 | // Switch off query logging: 41 | //persistence.db.log = false; 42 | 43 | function log(o) { 44 | sys.print(sys.inspect(o) + "\n"); 45 | } 46 | 47 | // Data model 48 | var Post = persistence.define('Post', { 49 | title: "TEXT", 50 | text: "TEXT", 51 | date: "DATE" 52 | }); 53 | var Comment = persistence.define('Comment', { 54 | author: "TEXT", 55 | text: "TEXT", 56 | date: "DATE" 57 | }); 58 | 59 | Post.hasMany('comments', Comment, 'post'); 60 | 61 | // HTML utilities 62 | 63 | function htmlHeader(res, title) { 64 | res.write("" + title + ""); 65 | } 66 | function htmlFooter(res) { 67 | res.write('
                Home'); 68 | res.write(""); 69 | } 70 | 71 | // Actions 72 | 73 | function initDatabase(session, tx, req, res, callback) { 74 | htmlHeader(res, "Initializing database."); 75 | session.schemaSync(tx, function() { 76 | res.write("Done."); 77 | htmlFooter(res); 78 | callback(); 79 | }); 80 | } 81 | 82 | function resetDatabase(session, tx, req, res, callback) { 83 | htmlHeader(res, "Dropping all tables"); 84 | session.reset(tx, function() { 85 | res.write('All tables dropped, Click here to create fresh ones'); 86 | htmlFooter(res); 87 | callback(); 88 | }); 89 | } 90 | 91 | function showItems(session, tx, req, res, callback) { 92 | htmlHeader(res, "Blog"); 93 | res.write('

                Latest Posts

                '); 94 | Post.all(session).order("date", false).list(tx, function(posts) { 95 | for(var i = 0; i < posts.length; i++) { 96 | var post = posts[i]; 97 | res.write('

                ' + post.title + '

                '); 98 | res.write(post.text); 99 | res.write('
                '); 100 | res.write('Posted ' + post.date); 101 | } 102 | res.write('

                Create new post

                '); 103 | res.write('
                '); 104 | res.write('

                Title:

                '); 105 | res.write('

                '); 106 | res.write('

                '); 107 | res.write('
                '); 108 | htmlFooter(res); 109 | callback(); 110 | }); 111 | } 112 | 113 | function showItem(session, tx, req, res, callback) { 114 | htmlHeader(res, "Blog"); 115 | var query = parseUrl(req.url, true).query; 116 | Post.load(session, tx, query.id, function(post) { 117 | res.write('

                ' + post.title + '

                '); 118 | res.write(post.text); 119 | res.write('
                '); 120 | res.write('Posted ' + post.date); 121 | res.write('

                Comments

                '); 122 | post.comments.order('date', true).list(tx, function(comments) { 123 | for(var i = 0; i < comments.length; i++) { 124 | var comment = comments[i]; 125 | res.write('

                By ' + comment.author + '

                '); 126 | res.write(comment.text); 127 | res.write('
                '); 128 | res.write('Posted ' + post.date); 129 | } 130 | res.write('

                Add a comment

                '); 131 | res.write('
                '); 132 | res.write(''); 133 | res.write('

                Your name:

                '); 134 | res.write('

                '); 135 | res.write('

                '); 136 | res.write('
                '); 137 | htmlFooter(res); 138 | callback(); 139 | }); 140 | }); 141 | } 142 | 143 | function post(session, tx, req, res, callback) { 144 | htmlHeader(res, "Created new post"); 145 | var query = parseUrl(req.url, true).query; 146 | var post = new Post(session, {title: query.title, text: query.text, date: new Date()}); 147 | session.add(post); 148 | session.flush(tx, function() { 149 | res.write('

                Post added.

                '); 150 | res.write('Go back'); 151 | htmlFooter(res); 152 | callback(); 153 | }); 154 | } 155 | 156 | function postComment(session, tx, req, res, callback) { 157 | htmlHeader(res, "Created new comment"); 158 | var query = parseUrl(req.url, true).query; 159 | var comment = new Comment(session, {text: query.text, author: query.author, date: new Date()}); 160 | Post.load(session, tx, query.post, function(post) { 161 | post.comments.add(comment); 162 | session.flush(tx, function() { 163 | res.write('

                Comment added.

                '); 164 | res.write('Go back'); 165 | htmlFooter(res); 166 | callback(); 167 | }); 168 | }); 169 | } 170 | 171 | var urlMap = { 172 | '/init': initDatabase, 173 | '/reset': resetDatabase, 174 | '/post': post, 175 | '/postComment': postComment, 176 | '/show': showItem, 177 | '/': showItems 178 | }; 179 | 180 | var http = require('http'); 181 | http.createServer(function (req, res) { 182 | res.writeHead(200, {'Content-Type': 'text/html'}); 183 | var parsed = parseUrl(req.url, true); 184 | var fn = urlMap[parsed.pathname]; 185 | if(fn) { 186 | var session = persistenceStore.getSession(); 187 | session.transaction(function(tx) { 188 | fn(session, tx, req, res, function() { 189 | session.close(); 190 | res.end(); 191 | }); 192 | }); 193 | } else { 194 | res.end("Not found: " + req.url); 195 | } 196 | }).listen(8888, "127.0.0.1"); 197 | console.log('Server running at http://127.0.0.1:8888/'); 198 | -------------------------------------------------------------------------------- /lib/persistence.store.cordovasql.js: -------------------------------------------------------------------------------- 1 | try { 2 | if (!window) { 3 | window = {}; 4 | //exports.console = console; 5 | } 6 | } catch (e) { 7 | window = {}; 8 | exports.console = console; 9 | } 10 | 11 | var persistence = (window && window.persistence) ? window.persistence : {}; 12 | 13 | if (!persistence.store) { 14 | persistence.store = {}; 15 | } 16 | 17 | persistence.store.cordovasql = {}; 18 | 19 | /** 20 | * Configure the database connection (either sqliteplugin or websql) 21 | * 22 | * @param persistence 23 | * @param dbname 24 | * @param dbversion 25 | * @param description 26 | * @param size 27 | * @param backgroundProcessing 28 | * @param iOSLocation 29 | */ 30 | persistence.store.cordovasql.config = function (persistence, dbname, dbversion, description, size, backgroundProcessing, iOSLocation) { 31 | var conn = null; 32 | 33 | /** 34 | * Create a transaction 35 | * 36 | * @param callback 37 | * the callback function to be invoked when the transaction 38 | * starts, taking the transaction object as argument 39 | */ 40 | persistence.transaction = function (callback) { 41 | if (!conn) { 42 | throw new Error("No ongoing database connection, please connect first."); 43 | } else { 44 | conn.transaction(callback); 45 | } 46 | }; 47 | 48 | persistence.db = persistence.db || {}; 49 | persistence.db.implementation = "unsupported"; 50 | persistence.db.conn = null; 51 | 52 | /* Find out if sqliteplugin is loaded. Otherwise, we'll fall back to WebSql */ 53 | if (window && 'sqlitePlugin' in window) { 54 | persistence.db.implementation = 'sqliteplugin'; 55 | } else if (window && window.openDatabase) { 56 | persistence.db.implementation = "websql"; 57 | } else { 58 | // Well, we are stuck! 59 | } 60 | 61 | /* 62 | * Cordova SqlitePlugin 63 | */ 64 | persistence.db.sqliteplugin = {}; 65 | 66 | /** 67 | * Connect to Sqlite plugin database 68 | * 69 | * @param dbname 70 | * @param backgroundProcessing 71 | * @param iOSLocation 72 | * @returns {{}} 73 | */ 74 | persistence.db.sqliteplugin.connect = function (dbname, backgroundProcessing, iOSLocation) { 75 | var that = {}; 76 | var conn = window.sqlitePlugin.openDatabase({name: dbname, bgType: backgroundProcessing, location: (iOSLocation || 0)}); 77 | 78 | that.transaction = function (fn) { 79 | return conn.transaction(function (sqlt) { 80 | return fn(persistence.db.websql.transaction(sqlt)); 81 | }); 82 | }; 83 | return that; 84 | }; 85 | 86 | /** 87 | * Run transaction on Sqlite plugin database 88 | * 89 | * @param t 90 | * @returns {{}} 91 | */ 92 | persistence.db.sqliteplugin.transaction = function (t) { 93 | var that = {}; 94 | that.executeSql = function (query, args, successFn, errorFn) { 95 | if (persistence.debug) { 96 | console.log(query, args); 97 | } 98 | t.executeSql(query, args, function (_, result) { 99 | if (successFn) { 100 | var results = []; 101 | for (var i = 0; i < result.rows.length; i++) { 102 | results.push(result.rows.item(i)); 103 | } 104 | successFn(results); 105 | } 106 | }, errorFn); 107 | }; 108 | return that; 109 | }; 110 | 111 | /* 112 | * WebSQL 113 | */ 114 | persistence.db.websql = {}; 115 | 116 | /** 117 | * Connect to the default WebSQL database 118 | * 119 | * @param dbname 120 | * @param dbversion 121 | * @param description 122 | * @param size 123 | * @returns {{}} 124 | */ 125 | persistence.db.websql.connect = function (dbname, dbversion, description, size) { 126 | var that = {}; 127 | var conn = openDatabase(dbname, dbversion, description, size); 128 | 129 | that.transaction = function (fn) { 130 | return conn.transaction(function (sqlt) { 131 | return fn(persistence.db.websql.transaction(sqlt)); 132 | }); 133 | }; 134 | return that; 135 | }; 136 | 137 | /** 138 | * Run transaction on WebSQL database 139 | * 140 | * @param t 141 | * @returns {{}} 142 | */ 143 | persistence.db.websql.transaction = function (t) { 144 | var that = {}; 145 | that.executeSql = function (query, args, successFn, errorFn) { 146 | if (persistence.debug) { 147 | console.log(query, args); 148 | } 149 | t.executeSql(query, args, function (_, result) { 150 | if (successFn) { 151 | var results = []; 152 | for (var i = 0; i < result.rows.length; i++) { 153 | results.push(result.rows.item(i)); 154 | } 155 | successFn(results); 156 | } 157 | }, errorFn); 158 | }; 159 | return that; 160 | }; 161 | 162 | /** 163 | * Connect() wrapper 164 | * 165 | * @param dbname 166 | * @param dbversion 167 | * @param description 168 | * @param size 169 | * @param backgroundProcessing 170 | * @param iOSLocation 171 | * @returns {*} 172 | */ 173 | persistence.db.connect = function (dbname, dbversion, description, size, backgroundProcessing, iOSLocation) { 174 | if (persistence.db.implementation == "sqliteplugin") { 175 | return persistence.db.sqliteplugin.connect(dbname, backgroundProcessing, iOSLocation); 176 | } else if (persistence.db.implementation == "websql") { 177 | return persistence.db.websql.connect(dbname, dbversion, description, size); 178 | } 179 | 180 | return null; 181 | }; 182 | 183 | /** 184 | * Set the sqlite dialect 185 | * 186 | * @type {{createTable: createTable, createIndex: createIndex}} 187 | */ 188 | persistence.store.cordovasql.sqliteDialect = { 189 | 190 | /** 191 | * columns is an array of arrays, e.g. [["id", "VARCHAR(32)", "PRIMARY KEY"], ["name", "TEXT"]] 192 | * 193 | * @param tableName 194 | * @param columns 195 | * @returns {string} 196 | */ 197 | createTable: function (tableName, columns) { 198 | var tm = persistence.typeMapper; 199 | var sql = "CREATE TABLE IF NOT EXISTS `" + tableName + "` ("; 200 | var defs = []; 201 | for (var i = 0; i < columns.length; i++) { 202 | var column = columns[i]; 203 | defs.push("`" + column[0] + "` " + tm.columnType(column[1]) + (column[2] ? " " + column[2] : "")); 204 | } 205 | sql += defs.join(", "); 206 | sql += ')'; 207 | return sql; 208 | }, 209 | 210 | /** 211 | * columns is array of column names, e.g. ["id"] 212 | * @param tableName 213 | * @param columns 214 | * @param options 215 | * @returns {string} 216 | */ 217 | createIndex: function (tableName, columns, options) { 218 | options = options || {}; 219 | return "CREATE " + (options.unique ? "UNIQUE " : "") + "INDEX IF NOT EXISTS `" + tableName + "__" + columns.join("_") + 220 | "` ON `" + tableName + "` (" + 221 | columns.map(function (col) { 222 | return "`" + col + "`"; 223 | }).join(", ") + ")"; 224 | } 225 | }; 226 | 227 | // Configure persistence for generic sql persistence, using sqliteDialect 228 | persistence.store.sql.config(persistence, persistence.store.cordovasql.sqliteDialect); 229 | 230 | // Make the connection 231 | conn = persistence.db.connect(dbname, dbversion, description, size, backgroundProcessing, iOSLocation); 232 | if (!conn) { 233 | throw new Error("No supported database found in this browser."); 234 | } 235 | }; 236 | 237 | try { 238 | exports.persistence = persistence; 239 | } catch (e) { 240 | } 241 | -------------------------------------------------------------------------------- /lib/persistence.sync.server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2010 Zef Hemel 3 | * 4 | * Permission is hereby granted, free of charge, to any person 5 | * obtaining a copy of this software and associated documentation 6 | * files (the "Software"), to deal in the Software without 7 | * restriction, including without limitation the rights to use, 8 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the 10 | * Software is furnished to do so, subject to the following 11 | * conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be 14 | * included in all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | * OTHER DEALINGS IN THE SOFTWARE. 24 | * 25 | * 26 | * USAGE: 27 | * On first run, be sure to initialize the database first: http://localhost:8888/init 28 | * otherwise the application will hang (because the select query fails). After that, 29 | * just visit http://localhost:8888/ 30 | */ 31 | var sys = require('sys'); 32 | 33 | function log(o) { 34 | sys.print(sys.inspect(o) + "\n"); 35 | } 36 | 37 | function jsonToEntityVal(value, type) { 38 | if(type) { 39 | switch(type) { 40 | case 'DATE': 41 | if (value > 1000000000000) { 42 | // it's in milliseconds 43 | return new Date(value); 44 | } else { 45 | return new Date(value * 1000); 46 | } 47 | break; 48 | default: 49 | return value; 50 | } 51 | } else { 52 | return value; 53 | } 54 | } 55 | 56 | function getEpoch(date) { 57 | return date.getTime(); //Math.round(date.getTime()/1000); 58 | } 59 | 60 | function entityValToJson(value, type) { 61 | if(type) { 62 | switch(type) { 63 | case 'DATE': 64 | return Math.round(date.getTime() / 1000); 65 | break; 66 | default: 67 | return value; 68 | } 69 | } else { 70 | return value; 71 | } 72 | } 73 | 74 | 75 | exports.pushUpdates = function(session, tx, Entity, since, callback) { 76 | var queryCollection; 77 | if(typeof(Entity) == "function"){ 78 | queryCollection = Entity.all(session); 79 | }else if(typeof(Entity) == "object"){ 80 | queryCollection = Entity; 81 | } 82 | queryCollection.filter("_lastChange", ">", since).list(tx, function(items) { 83 | var results = []; 84 | var meta = Entity.meta; 85 | var fieldSpec = meta.fields; 86 | for(var i = 0; i < items.length; i++) { 87 | var itemData = items[i]._data; 88 | var item = {id: items[i].id}; 89 | for(var p in fieldSpec) { 90 | if(fieldSpec.hasOwnProperty(p)) { 91 | item[p] = entityValToJson(itemData[p], fieldSpec[p]); 92 | } 93 | } 94 | for(var p in meta.hasOne) { 95 | if(meta.hasOne.hasOwnProperty(p)) { 96 | item[p] = entityValToJson(itemData[p]); 97 | } 98 | } 99 | results.push(item); 100 | } 101 | if(since>0){ 102 | session.sync.RemovedObject.all(session).filter("entity", "=", meta.name).filter("date", ">", since).list(tx, function(items) { 103 | for(var i = 0; i < items.length; i++) { 104 | results.push({id: items[i].id, _removed: true}); 105 | } 106 | callback({now: getEpoch(new Date()), updates: results}); 107 | }); 108 | }else{ 109 | callback({now: getEpoch(new Date()), updates: results}); 110 | } 111 | }); 112 | }; 113 | 114 | exports.receiveUpdates = function(session, tx, Entity, updates, callback) { 115 | var allIds = []; 116 | var updateLookup = {}; 117 | var now = getEpoch(new Date()); 118 | var removedIds = []; 119 | for(var i = 0; i < updates.length; i++) { 120 | if(updates[i]._removed) { // removed 121 | removedIds.push(updates[i].id); 122 | } else { 123 | allIds.push(updates[i].id); 124 | updateLookup[updates[i].id] = updates[i]; 125 | } 126 | } 127 | Entity.all(session).filter("id", "in", removedIds).destroyAll(function() { 128 | removedIds.forEach(function(id) { 129 | session.add(new session.sync.RemovedObject({objectId: id, entity: Entity.meta.name, date: now})); 130 | }); 131 | Entity.all(session).filter("id", "in", allIds).list(tx, function(existingItems) { 132 | var fieldSpec = Entity.meta.fields; 133 | 134 | for(var i = 0; i < existingItems.length; i++) { 135 | var existingItem = existingItems[i]; 136 | var updateItem = updateLookup[existingItem.id]; 137 | for(var p in updateItem) { 138 | if(updateItem.hasOwnProperty(p)) { 139 | if(updateItem[p] !== existingItem._data[p]) { 140 | existingItem[p] = jsonToEntityVal(updateItem[p], fieldSpec[p]); 141 | existingItem._lastChange = now; 142 | } 143 | } 144 | } 145 | delete updateLookup[existingItem.id]; 146 | } 147 | // All new items 148 | for(var id in updateLookup) { 149 | if(updateLookup.hasOwnProperty(id)) { 150 | var update = updateLookup[id]; 151 | delete update.id; 152 | var newItem = new Entity(session); 153 | newItem.id = id; 154 | for(var p in update) { 155 | if(update.hasOwnProperty(p)) { 156 | newItem[p] = jsonToEntityVal(update[p], fieldSpec[p]); 157 | } 158 | } 159 | newItem._lastChange = now; 160 | session.add(newItem); 161 | } 162 | } 163 | session.flush(tx, function() { 164 | callback({status: 'ok', now: now}); 165 | }); 166 | }); 167 | }); 168 | }; 169 | 170 | exports.config = function(persistence) { 171 | persistence.sync = persistence.sync || {}; 172 | persistence.sync.RemovedObject = persistence.define('_SyncRemovedObject', { 173 | entity: "VARCHAR(255)", 174 | objectId: "VARCHAR(32)", 175 | date: "BIGINT" 176 | }); 177 | 178 | persistence.entityDecoratorHooks.push(function(Entity) { 179 | /** 180 | * Declares an entity to be tracked for changes 181 | */ 182 | Entity.enableSync = function() { 183 | Entity.meta.enableSync = true; 184 | Entity.meta.fields['_lastChange'] = 'BIGINT'; 185 | }; 186 | }); 187 | 188 | /** 189 | * Resets _lastChange property if the object has dirty project (i.e. the object has changed) 190 | */ 191 | persistence.flushHooks.push(function(session, tx, callback) { 192 | var queries = []; 193 | for (var id in session.getTrackedObjects()) { 194 | if (session.getTrackedObjects().hasOwnProperty(id)) { 195 | var obj = session.getTrackedObjects()[id]; 196 | var meta = persistence.getEntityMeta()[obj._type]; 197 | if(meta.enableSync) { 198 | var isDirty = obj._new; 199 | var lastChangeIsDirty = false; 200 | for ( var p in obj._dirtyProperties) { 201 | if (obj._dirtyProperties.hasOwnProperty(p)) { 202 | isDirty = true; 203 | } 204 | if(p === '_lastChange') { 205 | lastChangeIsDirty = true; 206 | } 207 | } 208 | if(isDirty && !lastChangeIsDirty) { 209 | // Only set _lastChange if it has not been set manually (during a sync) 210 | obj._lastChange = getEpoch(new Date()); 211 | } 212 | } 213 | } 214 | } 215 | session.objectsRemoved.forEach(function(rec) { 216 | var meta = session.getMeta(rec.entity); 217 | if(meta.enableSync) { 218 | session.add(new persistence.sync.RemovedObject({entity: rec.entity, objectId: rec.id, date: getEpoch(new Date())})); 219 | } 220 | }); 221 | callback(); 222 | }); 223 | }; 224 | -------------------------------------------------------------------------------- /lib/persistence.store.memory.js: -------------------------------------------------------------------------------- 1 | try { 2 | if(!window) { 3 | window = {}; 4 | //exports.console = console; 5 | } 6 | } catch(e) { 7 | window = {}; 8 | exports.console = console; 9 | } 10 | 11 | var persistence = (window && window.persistence) ? window.persistence : {}; 12 | 13 | if(!persistence.store) { 14 | persistence.store = {}; 15 | } 16 | 17 | persistence.store.memory = {}; 18 | 19 | persistence.store.memory.config = function(persistence, dbname) { 20 | var argspec = persistence.argspec; 21 | dbname = dbname || 'persistenceData'; 22 | 23 | var allObjects = {}; // entityName -> LocalQueryCollection 24 | 25 | persistence.getAllObjects = function() { return allObjects; }; 26 | 27 | var defaultAdd = persistence.add; 28 | 29 | persistence.add = function(obj) { 30 | if(!this.trackedObjects[obj.id]) { 31 | defaultAdd.call(this, obj); 32 | var entityName = obj._type; 33 | if(!allObjects[entityName]) { 34 | allObjects[entityName] = new persistence.LocalQueryCollection(); 35 | allObjects[entityName]._session = persistence; 36 | } 37 | allObjects[entityName].add(obj); 38 | } 39 | return this; 40 | }; 41 | 42 | var defaultRemove = persistence.remove; 43 | 44 | persistence.remove = function(obj) { 45 | defaultRemove.call(this, obj); 46 | var entityName = obj._type; 47 | allObjects[entityName].remove(obj); 48 | }; 49 | 50 | persistence.schemaSync = function (tx, callback, emulate) { 51 | var args = argspec.getArgs(arguments, [ 52 | { name: "tx", optional: true, check: persistence.isTransaction, defaultValue: null }, 53 | { name: "callback", optional: true, check: argspec.isCallback(), defaultValue: function(){} }, 54 | { name: "emulate", optional: true, check: argspec.hasType('boolean') } 55 | ]); 56 | 57 | args.callback(); 58 | }; 59 | 60 | persistence.flush = function (tx, callback) { 61 | var args = argspec.getArgs(arguments, [ 62 | { name: "tx", optional: true, check: persistence.isTransaction }, 63 | { name: "callback", optional: true, check: argspec.isCallback(), defaultValue: function(){} } 64 | ]); 65 | 66 | var fns = persistence.flushHooks; 67 | var session = this; 68 | persistence.asyncForEach(fns, function(fn, callback) { 69 | fn(session, tx, callback); 70 | }, function() { 71 | var trackedObjects = persistence.trackedObjects; 72 | for(var id in trackedObjects) { 73 | if(trackedObjects.hasOwnProperty(id)) { 74 | if (persistence.objectsToRemove.hasOwnProperty(id)) { 75 | delete trackedObjects[id]; 76 | } else { 77 | trackedObjects[id]._dirtyProperties = {}; 78 | } 79 | } 80 | } 81 | args.callback(); 82 | }); 83 | }; 84 | 85 | persistence.transaction = function(callback) { 86 | setTimeout(function() { 87 | callback({executeSql: function() {} }); 88 | }, 0); 89 | }; 90 | 91 | persistence.loadFromLocalStorage = function(callback) { 92 | var dump = window.localStorage.getItem(dbname); 93 | if(dump) { 94 | this.loadFromJson(dump, callback); 95 | } else { 96 | callback && callback(); 97 | } 98 | }; 99 | 100 | persistence.saveToLocalStorage = function(callback) { 101 | this.dumpToJson(function(dump) { 102 | window.localStorage.setItem(dbname, dump); 103 | if(callback) { 104 | callback(); 105 | } 106 | }); 107 | }; 108 | 109 | /** 110 | * Remove all tables in the database (as defined by the model) 111 | */ 112 | persistence.reset = function (tx, callback) { 113 | var args = argspec.getArgs(arguments, [ 114 | { name: "tx", optional: true, check: persistence.isTransaction, defaultValue: null }, 115 | { name: "callback", optional: true, check: argspec.isCallback(), defaultValue: function(){} } 116 | ]); 117 | tx = args.tx; 118 | callback = args.callback; 119 | 120 | allObjects = {}; 121 | this.clean(); 122 | callback(); 123 | }; 124 | 125 | /** 126 | * Dummy 127 | */ 128 | persistence.close = function() {}; 129 | 130 | // QueryCollection's list 131 | 132 | function makeLocalClone(otherColl) { 133 | var coll = allObjects[otherColl._entityName]; 134 | if(!coll) { 135 | coll = new persistence.LocalQueryCollection(); 136 | } 137 | coll = coll.clone(); 138 | coll._filter = otherColl._filter; 139 | coll._prefetchFields = otherColl._prefetchFields; 140 | coll._orderColumns = otherColl._orderColumns; 141 | coll._limit = otherColl._limit; 142 | coll._skip = otherColl._skip; 143 | coll._reverse = otherColl._reverse; 144 | return coll; 145 | } 146 | /** 147 | * Asynchronous call to actually fetch the items in the collection 148 | * @param tx transaction to use 149 | * @param callback function to be called taking an array with 150 | * result objects as argument 151 | */ 152 | persistence.DbQueryCollection.prototype.list = function (tx, callback) { 153 | var args = argspec.getArgs(arguments, [ 154 | { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, 155 | { name: 'callback', optional: false, check: argspec.isCallback() } 156 | ]); 157 | tx = args.tx; 158 | callback = args.callback; 159 | 160 | var coll = makeLocalClone(this); 161 | coll.list(null, callback); 162 | }; 163 | 164 | /** 165 | * Asynchronous call to remove all the items in the collection. 166 | * Note: does not only remove the items from the collection, but 167 | * the items themselves. 168 | * @param tx transaction to use 169 | * @param callback function to be called when clearing has completed 170 | */ 171 | persistence.DbQueryCollection.prototype.destroyAll = function (tx, callback) { 172 | var args = argspec.getArgs(arguments, [ 173 | { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, 174 | { name: 'callback', optional: true, check: argspec.isCallback(), defaultValue: function(){} } 175 | ]); 176 | tx = args.tx; 177 | callback = args.callback; 178 | 179 | var coll = makeLocalClone(this); 180 | coll.destroyAll(null, callback); 181 | }; 182 | 183 | /** 184 | * Asynchronous call to count the number of items in the collection. 185 | * @param tx transaction to use 186 | * @param callback function to be called when clearing has completed 187 | */ 188 | persistence.DbQueryCollection.prototype.count = function (tx, callback) { 189 | var args = argspec.getArgs(arguments, [ 190 | { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, 191 | { name: 'callback', optional: false, check: argspec.isCallback() } 192 | ]); 193 | tx = args.tx; 194 | callback = args.callback; 195 | 196 | var coll = makeLocalClone(this); 197 | coll.count(null, callback); 198 | }; 199 | 200 | persistence.ManyToManyDbQueryCollection = function(session, entityName) { 201 | this.init(session, entityName, persistence.ManyToManyDbQueryCollection); 202 | this._items = []; 203 | }; 204 | 205 | persistence.ManyToManyDbQueryCollection.prototype = new persistence.LocalQueryCollection(); 206 | 207 | persistence.ManyToManyDbQueryCollection.prototype.initManyToMany = function(obj, coll) { 208 | this._obj = obj; 209 | this._coll = coll; // column name 210 | }; 211 | 212 | persistence.ManyToManyDbQueryCollection.prototype.add = function(item, recursing) { 213 | persistence.LocalQueryCollection.prototype.add.call(this, item); 214 | if(!recursing) { // prevent recursively adding to one another 215 | // Let's find the inverse collection 216 | var meta = persistence.getMeta(this._obj._type); 217 | var inverseProperty = meta.hasMany[this._coll].inverseProperty; 218 | persistence.get(item, inverseProperty).add(this._obj, true); 219 | } 220 | }; 221 | 222 | persistence.ManyToManyDbQueryCollection.prototype.remove = function(item, recursing) { 223 | persistence.LocalQueryCollection.prototype.remove.call(this, item); 224 | if(!recursing) { // prevent recursively adding to one another 225 | // Let's find the inverse collection 226 | var meta = persistence.getMeta(this._obj._type); 227 | var inverseProperty = meta.hasMany[this._coll].inverseProperty; 228 | persistence.get(item, inverseProperty).remove(this._obj, true); 229 | } 230 | }; 231 | }; 232 | 233 | try { 234 | exports.config = persistence.store.memory.config; 235 | exports.getSession = function() { return persistence; }; 236 | } catch(e) {} 237 | 238 | -------------------------------------------------------------------------------- /test/browser/test.sync.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | persistence.store.websql.config(persistence, 'persistencetest', 'My db', 5 * 1024 * 1024); 3 | //persistence.store.memory.config(persistence); 4 | persistence.debug = true; 5 | 6 | var Project = persistence.define('Project', { 7 | name: "TEXT" 8 | }); 9 | 10 | var Task = persistence.define('Task', { 11 | name: "TEXT", 12 | done: "BOOL" 13 | }); 14 | 15 | var Tag = persistence.define('Tag', { 16 | name: "TEXT" 17 | }); 18 | 19 | Task.hasMany('tags', Tag, 'tasks'); 20 | Tag.hasMany('tasks', Task, 'tags'); 21 | 22 | Project.hasMany('tasks', Task, 'project'); 23 | 24 | Task.enableSync('/taskUpdates'); 25 | Project.enableSync('/projectUpdates'); 26 | Tag.enableSync('/tagUpdates'); 27 | 28 | module("Setup"); 29 | 30 | asyncTest("setting up local database", function() { 31 | persistence.reset(function() { 32 | persistence.schemaSync(function(){ 33 | ok(true, 'came back from schemaSync'); 34 | start(); 35 | }); 36 | }); 37 | }); 38 | 39 | asyncTest("setting up remote database", 1, function() { 40 | persistence.sync.getJSON('/reset', function(data) { 41 | same(data, {status: 'ok'}, "Remote reset"); 42 | start(); 43 | }); 44 | }); 45 | 46 | module("Sync"); 47 | 48 | function noConflictsHandler(conflicts, updatesToPush, callback) { 49 | ok(false, "Should not go to conflict resolving"); 50 | console.log("Conflicts: ", conflicts); 51 | callback(); 52 | } 53 | 54 | asyncTest("initial sync of project", function() { 55 | Project.syncAll(noConflictsHandler, function() { 56 | ok(true, "Came back from sync"); 57 | Project.syncAll(noConflictsHandler, function() { 58 | ok(true, "Came back from second sync"); 59 | Project.all().list(function(projects) { 60 | equals(projects.length, 1, "1 project synced"); 61 | var p = projects[0]; 62 | equals(p.name, "Main project", "project name"); 63 | start(); 64 | }); 65 | }); 66 | }); 67 | }); 68 | 69 | asyncTest("initial sync of tasks", function() { 70 | Task.syncAll(noConflictsHandler, function() { 71 | ok(true, "Came back from sync"); 72 | Task.syncAll(noConflictsHandler, function() { 73 | ok(true, "Came back from second sync"); 74 | Task.all().list(function(tasks) { 75 | equals(tasks.length, 25, "25 tasks synced"); 76 | tasks.forEach(function(task) { 77 | equals(false, task.done, "task not done"); 78 | }); 79 | start(); 80 | }); 81 | }); 82 | }); 83 | }); 84 | 85 | asyncTest("setting some tasks to done and syncing again", function() { 86 | Task.all().list(function(tasks) { 87 | for(var i = 0; i < tasks.length; i++) { 88 | if(i % 2 === 0) { 89 | tasks[i].done = true; 90 | } 91 | } 92 | Task.syncAll(noConflictsHandler, function() { 93 | ok(true, "Came back from sync"); 94 | start(); 95 | }); 96 | }); 97 | }); 98 | 99 | function resetResync(callback) { 100 | persistence.reset(function() { 101 | persistence.schemaSync(function() { 102 | ok(true, "Database reset"); 103 | 104 | Project.syncAll(noConflictsHandler, function() { 105 | ok(true, "Came back from project sync"); 106 | Task.syncAll(noConflictsHandler, function() { 107 | ok(true, "Came back from task sync"); 108 | callback(); 109 | }); 110 | }); 111 | }); 112 | }); 113 | } 114 | 115 | asyncTest("resetting local db and resyncing", function() { 116 | resetResync(function() { 117 | Task.all().filter("done", "=", true).count(function(n) { 118 | equals(n, 13, "right number of tasks done"); 119 | start(); 120 | }); 121 | }); 122 | }); 123 | 124 | asyncTest("creating some new objects", function() { 125 | var p = new Project({name: "Locally created project"}); 126 | persistence.add(p); 127 | for(var i = 0; i < 10; i++) { 128 | var t = new Task({name: "Local task " + i}); 129 | p.tasks.add(t); 130 | } 131 | persistence.flush(function() { 132 | ok(true, "project and tasks added locally"); 133 | Project.syncAll(noConflictsHandler, function() { 134 | ok(true, "returned from project sync"); 135 | Task.syncAll(noConflictsHandler, function() { 136 | ok(true, "returned from task sync"); 137 | p.tasks.list(function(tasks) { 138 | equals(tasks.length, 10, 'check collection size'); 139 | tasks.forEach(function(task) { 140 | task.done = true; 141 | }); 142 | Task.syncAll(noConflictsHandler, function() { 143 | start(); 144 | }); 145 | }); 146 | }); 147 | }); 148 | }); 149 | }); 150 | 151 | asyncTest("resetting local db and resyncing", function() { 152 | resetResync(function() { 153 | Task.all().filter("done", "=", true).count(function(n) { 154 | equals(n, 23, "right number of tasks done."); 155 | start(); 156 | }); 157 | }); 158 | }); 159 | 160 | asyncTest("marking all tasks done remotely", function() { 161 | persistence.sync.getJSON('/markAllDone', function(data) { 162 | same(data, {status: 'ok'}, "Remote reset"); 163 | Task.syncAll(noConflictsHandler, function() { 164 | ok(true, "Came back from sync"); 165 | Task.all().filter("done", "=", true).count(function(n) { 166 | equals(35, n, "all tasks were marked done and synced correctly"); 167 | start(); 168 | }); 169 | }); 170 | }); 171 | }); 172 | 173 | module("Conflicts"); 174 | 175 | asyncTest("prefer local conflict handler", 8, function() { 176 | persistence.sync.getJSON('/markAllUndone', function(data) { 177 | same(data, {status: 'ok'}, "Remote marking undone"); 178 | Task.all().list(function(tasks) { 179 | for(var i = 0; i < tasks.length; i++) { 180 | if(i % 2 === 0) { 181 | // Force a dirty flag 182 | tasks[i].done = true; 183 | tasks[i].done = false; 184 | tasks[i].done = true; 185 | } 186 | } 187 | persistence.flush(function() { 188 | Task.syncAll(function(conflicts, updatesToPush, callback) { 189 | ok(true, "Conflict resolver called"); 190 | equals(conflicts.length, 18, "Number of conflicts"); 191 | console.log("Conflicts: ", conflicts); 192 | persistence.sync.preferLocalConflictHandler(conflicts, updatesToPush, callback); 193 | }, function() { 194 | ok(true, "Came back from sync"); 195 | resetResync(function() { 196 | Task.all().filter("done", "=", true).list(function(tasks) { 197 | equals(tasks.length, 18, "Conflicts were properly resolved towards the server"); 198 | start(); 199 | }); 200 | }); 201 | }); 202 | }); 203 | }); 204 | }); 205 | }); 206 | 207 | asyncTest("prefer remote conflict handler", 5, function() { 208 | persistence.sync.getJSON('/markAllUndone', function(data) { 209 | same(data, {status: 'ok'}, "Remote marking undone"); 210 | Task.all().list(function(tasks) { 211 | for(var i = 0; i < tasks.length; i++) { 212 | if(i % 2 === 0) { 213 | // Force a dirty flag 214 | tasks[i].done = true; 215 | tasks[i].done = false; 216 | tasks[i].done = true; 217 | } 218 | } 219 | persistence.flush(function() { 220 | Task.syncAll(function(conflicts, updatesToPush, callback) { 221 | ok(true, "Conflict resolver called"); 222 | equals(conflicts.length, 18, "Number of conflicts"); 223 | console.log("Conflicts: ", conflicts); 224 | persistence.sync.preferRemoteConflictHandler(conflicts, updatesToPush, callback); 225 | }, function() { 226 | ok(true, "Came back from sync"); 227 | Task.all().filter("done", "=", true).list(function(tasks) { 228 | equals(tasks.length, 0, "Conflicts were properly resolved"); 229 | start(); 230 | }); 231 | }); 232 | }); 233 | }); 234 | }); 235 | }); 236 | 237 | asyncTest("Object removal", function() { 238 | Task.all().list(function(tasks) { 239 | for(var i = 0; i < tasks.length; i++) { 240 | if(i % 2 === 0) { 241 | persistence.remove(tasks[i]); 242 | } 243 | } 244 | 245 | persistence.flush(function() { 246 | console.log("Now going to sync"); 247 | Task.syncAll(noConflictsHandler, function() { 248 | //Task.syncAll(noConflictsHandler, function() { 249 | start(); 250 | //}); 251 | }); 252 | }); 253 | }); 254 | }); 255 | }); 256 | -------------------------------------------------------------------------------- /lib/persistence.search.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2010 Zef Hemel 4 | * 5 | * Permission is hereby granted, free of charge, to any person 6 | * obtaining a copy of this software and associated documentation 7 | * files (the "Software"), to deal in the Software without 8 | * restriction, including without limitation the rights to use, 9 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the 11 | * Software is furnished to do so, subject to the following 12 | * conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be 15 | * included in all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | * OTHER DEALINGS IN THE SOFTWARE. 25 | */ 26 | 27 | try { 28 | if(!window) { 29 | window = {}; 30 | } 31 | } catch(e) { 32 | window = {}; 33 | exports.console = console; 34 | } 35 | 36 | var persistence = (window && window.persistence) ? window.persistence : {}; 37 | 38 | persistence.search = {}; 39 | 40 | persistence.search.config = function(persistence, dialect) { 41 | var filteredWords = {'and':true, 'the': true, 'are': true}; 42 | 43 | var argspec = persistence.argspec; 44 | 45 | function normalizeWord(word, filterShortWords) { 46 | if(!(word in filteredWords || (filterShortWords && word.length < 3))) { 47 | word = word.replace(/ies$/, 'y'); 48 | word = word.length > 3 ? word.replace(/s$/, '') : word; 49 | return word; 50 | } else { 51 | return false; 52 | } 53 | } 54 | 55 | /** 56 | * Does extremely basic tokenizing of text. Also includes some basic stemming. 57 | */ 58 | function searchTokenizer(text) { 59 | var words = text.toLowerCase().split(/[^\w\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+/); 60 | var wordDict = {}; 61 | // Prefixing words with _ to also index Javascript keywords and special fiels like 'constructor' 62 | for(var i = 0; i < words.length; i++) { 63 | var normalizedWord = normalizeWord(words[i]); 64 | if(normalizedWord) { 65 | var word = '_' + normalizedWord; 66 | // Some extremely basic stemming 67 | if(word in wordDict) { 68 | wordDict[word]++; 69 | } else { 70 | wordDict[word] = 1; 71 | } 72 | } 73 | } 74 | return wordDict; 75 | } 76 | 77 | /** 78 | * Parses a search query and returns it as list SQL parts later to be OR'ed or AND'ed. 79 | */ 80 | function searchPhraseParser(query, indexTbl, prefixByDefault) { 81 | query = query.toLowerCase().replace(/['"]/, '').replace(/(^\s+|\s+$)/g, ''); 82 | var words = query.split(/\s+/); 83 | var sqlParts = []; 84 | var restrictedToColumn = null; 85 | for(var i = 0; i < words.length; i++) { 86 | var word = normalizeWord(words[i]); 87 | if(!word) { 88 | continue; 89 | } 90 | if(word.search(/:$/) !== -1) { 91 | restrictedToColumn = word.substring(0, word.length-1); 92 | continue; 93 | } 94 | var sql = '('; 95 | if(word.search(/\*/) !== -1) { 96 | sql += "`" + indexTbl + "`.`word` LIKE '" + word.replace(/\*/g, '%') + "'"; 97 | } else if(prefixByDefault) { 98 | sql += "`" + indexTbl + "`.`word` LIKE '" + word + "%'"; 99 | } else { 100 | sql += "`" + indexTbl + "`.`word` = '" + word + "'"; 101 | } 102 | if(restrictedToColumn) { 103 | sql += ' AND `' + indexTbl + "`.`prop` = '" + restrictedToColumn + "'"; 104 | } 105 | sql += ')'; 106 | sqlParts.push(sql); 107 | } 108 | return sqlParts.length === 0 ? ["1=1"] : sqlParts; 109 | } 110 | 111 | var queryCollSubscribers = {}; // entityName -> subscription obj 112 | persistence.searchQueryCollSubscribers = queryCollSubscribers; 113 | 114 | function SearchFilter(query, entityName) { 115 | this.query = query; 116 | this.entityName = entityName; 117 | } 118 | 119 | SearchFilter.prototype.match = function (o) { 120 | var meta = persistence.getMeta(this.entityName); 121 | var query = this.query.toLowerCase(); 122 | var text = ''; 123 | for(var p in o) { 124 | if(meta.textIndex.hasOwnProperty(p)) { 125 | if(o[p]) { 126 | text += o[p]; 127 | } 128 | } 129 | } 130 | text = text.toLowerCase(); 131 | return text && text.indexOf(query) !== -1; 132 | } 133 | 134 | SearchFilter.prototype.sql = function (o) { 135 | return "1=1"; 136 | } 137 | 138 | SearchFilter.prototype.subscribeGlobally = function(coll, entityName) { 139 | var meta = persistence.getMeta(entityName); 140 | for(var p in meta.textIndex) { 141 | if(meta.textIndex.hasOwnProperty(p)) { 142 | persistence.subscribeToGlobalPropertyListener(coll, entityName, p); 143 | } 144 | } 145 | }; 146 | 147 | SearchFilter.prototype.unsubscribeGlobally = function(coll, entityName) { 148 | var meta = persistence.getMeta(entityName); 149 | for(var p in meta.textIndex) { 150 | if(meta.textIndex.hasOwnProperty(p)) { 151 | persistence.unsubscribeFromGlobalPropertyListener(coll, entityName, p); 152 | } 153 | } 154 | }; 155 | 156 | SearchFilter.prototype.toUniqueString = function() { 157 | return "SEARCH: " + this.query; 158 | } 159 | 160 | function SearchQueryCollection(session, entityName, query, prefixByDefault) { 161 | this.init(session, entityName, SearchQueryCollection); 162 | this.subscribers = queryCollSubscribers[entityName]; 163 | this._filter = new SearchFilter(query, entityName); 164 | 165 | 166 | if(query) { 167 | this._additionalJoinSqls.push(', `' + entityName + '_Index`'); 168 | this._additionalWhereSqls.push('`root`.id = `' + entityName + '_Index`.`entityId`'); 169 | this._additionalWhereSqls.push('(' + searchPhraseParser(query, entityName + '_Index', prefixByDefault).join(' OR ') + ')'); 170 | this._additionalGroupSqls.push(' GROUP BY (`' + entityName + '_Index`.`entityId`)'); 171 | this._additionalGroupSqls.push(' ORDER BY SUM(`' + entityName + '_Index`.`occurrences`) DESC'); 172 | } 173 | } 174 | 175 | SearchQueryCollection.prototype = new persistence.DbQueryCollection(); 176 | 177 | SearchQueryCollection.prototype.oldClone = SearchQueryCollection.prototype.clone; 178 | 179 | 180 | SearchQueryCollection.prototype.clone = function() { 181 | var clone = this.oldClone(false); 182 | var entityName = this._entityName; 183 | clone.subscribers = queryCollSubscribers[entityName]; 184 | return clone; 185 | }; 186 | 187 | SearchQueryCollection.prototype.order = function() { 188 | throw new Error("Imposing additional orderings is not support for search query collections."); 189 | }; 190 | 191 | /* 192 | SearchQueryCollection.prototype.filter = function (property, operator, value) { 193 | var c = this.clone(); 194 | c._filter = new persistence.AndFilter(this._filter, new persistence.PropertyFilter(property, operator, value)); 195 | // Add global listener (TODO: memory leak waiting to happen!) 196 | //session.subscribeToGlobalPropertyListener(c, this._entityName, property); 197 | return c; 198 | }; 199 | */ 200 | 201 | persistence.entityDecoratorHooks.push(function(Entity) { 202 | /** 203 | * Declares a property to be full-text indexed. 204 | */ 205 | Entity.textIndex = function(prop) { 206 | if(!Entity.meta.textIndex) { 207 | Entity.meta.textIndex = {}; 208 | } 209 | Entity.meta.textIndex[prop] = true; 210 | // Subscribe 211 | var entityName = Entity.meta.name; 212 | if(!queryCollSubscribers[entityName]) { 213 | queryCollSubscribers[entityName] = {}; 214 | } 215 | }; 216 | 217 | /** 218 | * Returns a query collection representing the result of a search 219 | * @param query an object with the following fields: 220 | */ 221 | Entity.search = function(session, query, prefixByDefault) { 222 | var args = argspec.getArgs(arguments, [ 223 | { name: 'session', optional: true, check: function(obj) { return obj.schemaSync; }, defaultValue: persistence }, 224 | { name: 'query', optional: false, check: argspec.hasType('string') }, 225 | { name: 'prefixByDefault', optional: false } 226 | ]); 227 | session = args.session; 228 | query = args.query; 229 | prefixByDefault = args.prefixByDefault; 230 | 231 | return session.uniqueQueryCollection(new SearchQueryCollection(session, Entity.meta.name, query, prefixByDefault)); 232 | }; 233 | }); 234 | 235 | persistence.schemaSyncHooks.push(function(tx) { 236 | var entityMeta = persistence.getEntityMeta(); 237 | var queries = []; 238 | for(var entityName in entityMeta) { 239 | var meta = entityMeta[entityName]; 240 | if(meta.textIndex) { 241 | queries.push([dialect.createTable(entityName + '_Index', [['entityId', 'VARCHAR(32)'], ['prop', 'VARCHAR(30)'], ['word', 'VARCHAR(100)'], ['occurrences', 'INT']]), null]); 242 | queries.push([dialect.createIndex(entityName + '_Index', ['prop', 'word']), null]); 243 | queries.push([dialect.createIndex(entityName + '_Index', ['word']), null]); 244 | persistence.generatedTables[entityName + '_Index'] = true; 245 | } 246 | } 247 | queries.reverse(); 248 | persistence.executeQueriesSeq(tx, queries); 249 | }); 250 | 251 | 252 | persistence.flushHooks.push(function(session, tx, callback) { 253 | var queries = []; 254 | for (var id in session.getTrackedObjects()) { 255 | if (session.getTrackedObjects().hasOwnProperty(id)) { 256 | var obj = session.getTrackedObjects()[id]; 257 | var meta = session.define(obj._type).meta; 258 | var indexTbl = obj._type + '_Index'; 259 | if(meta.textIndex) { 260 | for ( var p in obj._dirtyProperties) { 261 | if (obj._dirtyProperties.hasOwnProperty(p) && p in meta.textIndex) { 262 | queries.push(['DELETE FROM `' + indexTbl + '` WHERE `entityId` = ? AND `prop` = ?', [id, p]]); 263 | var occurrences = searchTokenizer(obj._data[p]); 264 | for(var word in occurrences) { 265 | if(occurrences.hasOwnProperty(word)) { 266 | queries.push(['INSERT INTO `' + indexTbl + '` VALUES (?, ?, ?, ?)', [obj.id, p, word.substring(1), occurrences[word]]]); 267 | } 268 | } 269 | } 270 | } 271 | } 272 | } 273 | } 274 | for (var id in persistence.getObjectsToRemove()) { 275 | if (persistence.getObjectsToRemove().hasOwnProperty(id)) { 276 | var obj = persistence.getObjectsToRemove()[id]; 277 | var meta = persistence.getEntityMeta()[obj._type]; 278 | if(meta.textIndex) { 279 | queries.push(['DELETE FROM `' + obj._type + '_Index` WHERE `entityId` = ?', [id]]); 280 | } 281 | } 282 | } 283 | queries.reverse(); 284 | persistence.executeQueriesSeq(tx, queries, callback); 285 | }); 286 | }; 287 | 288 | if(typeof exports === 'object') { 289 | exports.config = persistence.search.config; 290 | } 291 | 292 | -------------------------------------------------------------------------------- /lib/persistence.migrations.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2010 Fábio Rehm 4 | * 5 | * Permission is hereby granted, free of charge, to any person 6 | * obtaining a copy of this software and associated documentation 7 | * files (the "Software"), to deal in the Software without 8 | * restriction, including without limitation the rights to use, 9 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the 11 | * Software is furnished to do so, subject to the following 12 | * conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be 15 | * included in all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | * OTHER DEALINGS IN THE SOFTWARE. 25 | */ 26 | 27 | if(!window.persistence) { // persistence.js not loaded! 28 | throw new Error("persistence.js should be loaded before persistence.migrations.js"); 29 | } 30 | 31 | (function() { 32 | 33 | var Migrator = { 34 | migrations: [], 35 | 36 | version: function(callback) { 37 | persistence.transaction(function(t){ 38 | t.executeSql('SELECT current_version FROM schema_version', null, function(result){ 39 | if (result.length == 0) { 40 | t.executeSql('INSERT INTO schema_version VALUES (0)', null, function(){ 41 | callback(0); 42 | }); 43 | } else { 44 | callback(result[0].current_version); 45 | } 46 | }); 47 | }); 48 | }, 49 | 50 | setVersion: function(v, callback) { 51 | persistence.transaction(function(t){ 52 | t.executeSql('UPDATE schema_version SET current_version = ?', [v], function(){ 53 | Migrator._version = v; 54 | if (callback) callback(); 55 | }); 56 | }); 57 | }, 58 | 59 | setup: function(callback) { 60 | persistence.transaction(function(t){ 61 | t.executeSql('CREATE TABLE IF NOT EXISTS schema_version (current_version INTEGER)', null, function(){ 62 | // Creates a dummy migration just to force setting schema version when cleaning DB 63 | Migrator.migration(0, { up: function() { }, down: function() { } }); 64 | if (callback) callback(); 65 | }); 66 | }); 67 | }, 68 | 69 | // Method should only be used for testing 70 | reset: function(callback) { 71 | // Creates a dummy migration just to force setting schema version when cleaning DB 72 | Migrator.migrations = []; 73 | Migrator.migration(0, { up: function() { }, down: function() { } }); 74 | Migrator.setVersion(0, callback); 75 | }, 76 | 77 | migration: function(version, actions) { 78 | Migrator.migrations[version] = new Migration(version, actions); 79 | return Migrator.migrations[version]; 80 | }, 81 | 82 | migrateUpTo: function(version, callback) { 83 | var migrationsToRun = []; 84 | 85 | function migrateOne() { 86 | var migration = migrationsToRun.pop(); 87 | 88 | if (!migration) callback(); 89 | 90 | migration.up(function(){ 91 | if (migrationsToRun.length > 0) { 92 | migrateOne(); 93 | } else if (callback) { 94 | callback(); 95 | } 96 | }); 97 | } 98 | 99 | this.version(function(currentVersion){ 100 | for (var v = currentVersion+1; v <= version; v++) 101 | migrationsToRun.unshift(Migrator.migrations[v]); 102 | 103 | if (migrationsToRun.length > 0) { 104 | migrateOne(); 105 | } else if (callback) { 106 | callback(); 107 | } 108 | }); 109 | }, 110 | 111 | migrateDownTo: function(version, callback) { 112 | var migrationsToRun = []; 113 | 114 | function migrateOne() { 115 | var migration = migrationsToRun.pop(); 116 | 117 | if (!migration) callback(); 118 | 119 | migration.down(function(){ 120 | if (migrationsToRun.length > 0) { 121 | migrateOne(); 122 | } else if (callback) { 123 | callback(); 124 | } 125 | }); 126 | } 127 | 128 | this.version(function(currentVersion){ 129 | for (var v = currentVersion; v > version; v--) 130 | migrationsToRun.unshift(Migrator.migrations[v]); 131 | 132 | if (migrationsToRun.length > 0) { 133 | migrateOne(); 134 | } else if (callback) { 135 | callback(); 136 | } 137 | }); 138 | }, 139 | 140 | migrate: function(version, callback) { 141 | if ( arguments.length === 1 ) { 142 | callback = version; 143 | version = this.migrations.length-1; 144 | } 145 | 146 | this.version(function(curVersion){ 147 | if (curVersion < version) 148 | Migrator.migrateUpTo(version, callback); 149 | else if (curVersion > version) 150 | Migrator.migrateDownTo(version, callback); 151 | else 152 | callback(); 153 | }); 154 | } 155 | } 156 | 157 | var Migration = function(version, body) { 158 | this.version = version; 159 | // TODO check if actions contains up and down methods 160 | this.body = body; 161 | this.actions = []; 162 | }; 163 | 164 | Migration.prototype.executeActions = function(callback, customVersion) { 165 | var actionsToRun = this.actions; 166 | var version = (customVersion!==undefined) ? customVersion : this.version; 167 | 168 | persistence.transaction(function(tx){ 169 | function nextAction() { 170 | if (actionsToRun.length == 0) 171 | Migrator.setVersion(version, callback); 172 | else { 173 | var action = actionsToRun.pop(); 174 | action(tx, nextAction); 175 | } 176 | } 177 | 178 | nextAction(); 179 | }); 180 | } 181 | 182 | Migration.prototype.up = function(callback) { 183 | if (this.body.up) this.body.up.apply(this); 184 | this.executeActions(callback); 185 | } 186 | 187 | Migration.prototype.down = function(callback) { 188 | if (this.body.down) this.body.down.apply(this); 189 | this.executeActions(callback, this.version-1); 190 | } 191 | 192 | Migration.prototype.createTable = function(tableName, callback) { 193 | var table = new ColumnsHelper(); 194 | 195 | if (callback) callback(table); 196 | 197 | var column; 198 | var sql = 'CREATE TABLE ' + tableName + ' (id VARCHAR(32) PRIMARY KEY'; 199 | while (column = table.columns.pop()) 200 | sql += ', ' + column; 201 | 202 | this.executeSql(sql + ')'); 203 | } 204 | 205 | Migration.prototype.dropTable = function(tableName) { 206 | var sql = 'DROP TABLE ' + tableName; 207 | this.executeSql(sql); 208 | } 209 | 210 | Migration.prototype.addColumn = function(tableName, columnName, columnType) { 211 | var sql = 'ALTER TABLE ' + tableName + ' ADD ' + columnName + ' ' + columnType; 212 | this.executeSql(sql); 213 | } 214 | 215 | Migration.prototype.removeColumn = function(tableName, columnName) { 216 | this.action(function(tx, nextCommand){ 217 | var sql = 'select sql from sqlite_master where type = "table" and name == "'+tableName+'"'; 218 | tx.executeSql(sql, null, function(result){ 219 | var columns = new RegExp("CREATE TABLE `\\w+` |\\w+ \\((.+)\\)").exec(result[0].sql)[1].split(', '); 220 | var selectColumns = []; 221 | var columnsSql = []; 222 | 223 | for (var i = 0; i < columns.length; i++) { 224 | var colName = new RegExp("((`\\w+`)|(\\w+)) .+").exec(columns[i])[1]; 225 | if (colName == columnName) continue; 226 | 227 | columnsSql.push(columns[i]); 228 | selectColumns.push(colName); 229 | } 230 | columnsSql = columnsSql.join(', '); 231 | selectColumns = selectColumns.join(', '); 232 | 233 | var queries = []; 234 | queries.unshift(["ALTER TABLE " + tableName + " RENAME TO " + tableName + "_bkp;", null]); 235 | queries.unshift(["CREATE TABLE " + tableName + " (" + columnsSql + ");", null]); 236 | queries.unshift(["INSERT INTO " + tableName + " SELECT " + selectColumns + " FROM " + tableName + "_bkp;", null]); 237 | queries.unshift(["DROP TABLE " + tableName + "_bkp;", null]); 238 | 239 | persistence.executeQueriesSeq(tx, queries, nextCommand); 240 | }); 241 | }); 242 | } 243 | 244 | Migration.prototype.addIndex = function(tableName, columnName, unique) { 245 | var sql = 'CREATE ' + (unique === true ? 'UNIQUE' : '') + ' INDEX ' + tableName + '_' + columnName + ' ON ' + tableName + ' (' + columnName + ')'; 246 | this.executeSql(sql); 247 | } 248 | 249 | Migration.prototype.removeIndex = function(tableName, columnName) { 250 | var sql = 'DROP INDEX ' + tableName + '_' + columnName; 251 | this.executeSql(sql); 252 | } 253 | 254 | Migration.prototype.executeSql = function(sql, args) { 255 | this.action(function(tx, nextCommand){ 256 | tx.executeSql(sql, args, nextCommand); 257 | }); 258 | } 259 | 260 | Migration.prototype.action = function(callback) { 261 | this.actions.unshift(callback); 262 | } 263 | 264 | var ColumnsHelper = function() { 265 | this.columns = []; 266 | } 267 | 268 | ColumnsHelper.prototype.text = function(columnName) { 269 | this.columns.unshift(columnName + ' TEXT'); 270 | } 271 | 272 | ColumnsHelper.prototype.integer = function(columnName) { 273 | this.columns.unshift(columnName + ' INT'); 274 | } 275 | 276 | ColumnsHelper.prototype.real = function(columnName) { 277 | this.columns.unshift(columnName + ' REAL'); 278 | } 279 | 280 | ColumnsHelper.prototype['boolean'] = function(columnName) { 281 | this.columns.unshift(columnName + ' BOOL'); 282 | } 283 | 284 | ColumnsHelper.prototype.date = function(columnName) { 285 | this.columns.unshift(columnName + ' DATE'); 286 | } 287 | 288 | ColumnsHelper.prototype.json = function(columnName) { 289 | this.columns.unshift(columnName + ' TEXT'); 290 | } 291 | 292 | // Makes Migrator and Migration available to tests 293 | persistence.migrations = {}; 294 | persistence.migrations.Migrator = Migrator; 295 | persistence.migrations.Migration = Migration; 296 | persistence.migrations.init = function() { Migrator.setup.apply(Migrator, Array.prototype.slice.call(arguments, 0))}; 297 | 298 | persistence.migrate = function() { Migrator.migrate.apply(Migrator, Array.prototype.slice.call(arguments, 0))}; 299 | persistence.defineMigration = function() { Migrator.migration.apply(Migrator, Array.prototype.slice.call(arguments, 0))}; 300 | 301 | }()); 302 | -------------------------------------------------------------------------------- /test/browser/test.migrations.js: -------------------------------------------------------------------------------- 1 | function createMigrations(starting, amount, actions){ 2 | var amount = starting+amount; 3 | 4 | for (var i = starting; i < amount; i++) { 5 | var newActions = { 6 | up: actions.up, 7 | down: actions.down 8 | }; 9 | 10 | if (actions.createDown) 11 | newActions.down = actions.createDown(i); 12 | 13 | if (actions.createUp) 14 | newActions.up = actions.createUp(i); 15 | 16 | persistence.defineMigration(i, newActions); 17 | } 18 | } 19 | 20 | var Migrator = persistence.migrations.Migrator; 21 | 22 | $(document).ready(function(){ 23 | persistence.store.websql.config(persistence, 'migrationstest', 'My db', 5 * 1024 * 1024); 24 | persistence.debug = true; 25 | 26 | persistence.migrations.init(function() { 27 | 28 | module("Migrator", { 29 | setup: function() { 30 | 31 | }, 32 | teardown: function() { 33 | stop(); 34 | Migrator.reset(start); 35 | } 36 | }); 37 | 38 | asyncTest("getting and setting db version", 2, function() { 39 | Migrator.version(function(v){ 40 | equals(v, 0, 'initial db version'); 41 | }); 42 | 43 | var newVersion = 100; 44 | 45 | Migrator.setVersion(newVersion, function() { 46 | Migrator.version(function(v){ 47 | equals(v, newVersion, 'checking if version was set'); 48 | start(); 49 | }); 50 | }); 51 | }); 52 | 53 | asyncTest("migrations scope", 2, function(){ 54 | var migration = Migrator.migration(1, { 55 | up: function() { 56 | same(this, migration, 'up'); 57 | }, 58 | down: function() { 59 | same(this, migration, 'down'); 60 | } 61 | }); 62 | 63 | migration.up(function(){ 64 | migration.down(function(){ 65 | start(); 66 | }); 67 | }); 68 | }); 69 | 70 | asyncTest("migrating up to some version", 7, function(){ 71 | var actionsRan = 0; 72 | var totalActions = 5; 73 | 74 | createMigrations(1, totalActions, { 75 | up: function() { 76 | actionsRan++; 77 | equals(this.version, actionsRan, 'running migration in order'); 78 | } 79 | }); 80 | 81 | Migrator.migrate(totalActions, function(){ 82 | equals(actionsRan, totalActions, 'actions ran'); 83 | Migrator.version(function(v){ 84 | equals(v, totalActions, 'version changed to'); 85 | start(); 86 | }); 87 | }); 88 | }); 89 | 90 | asyncTest("migrating down to some version", 7, function(){ 91 | var actionsRan = 0; 92 | var totalActions = 5; 93 | 94 | createMigrations(1, totalActions, { 95 | createDown: function(i) { 96 | var position = Math.abs(actionsRan - i); 97 | return function () { 98 | actionsRan++; 99 | equals(this.version, position, 'running migration in order'); 100 | }; 101 | } 102 | }); 103 | 104 | Migrator.setVersion(totalActions, function(){ 105 | Migrator.migrate(0, function(){ 106 | equals(actionsRan, totalActions, 'actions ran'); 107 | Migrator.version(function(v){ 108 | equals(v, 0, 'version changed to'); 109 | start(); 110 | }); 111 | }); 112 | }); 113 | }); 114 | 115 | asyncTest("migrate to latest", 1, function(){ 116 | var totalActions = 3; 117 | 118 | createMigrations(1, totalActions, { up: function() { } }); 119 | 120 | Migrator.migrate(function() { 121 | Migrator.version(function(v){ 122 | equals(v, totalActions, 'latest version'); 123 | start(); 124 | }); 125 | }); 126 | }); 127 | 128 | module("Migration", { 129 | setup: function() { 130 | 131 | }, 132 | teardown: function() { 133 | stop(); 134 | // DROPS ALL TABLES 135 | var query = "select 'drop table ' || name || ';' AS dropTable from sqlite_master where type = 'table' and name not in ('__WebKitDatabaseInfoTable__', 'schema_version')"; 136 | 137 | persistence.transaction(function(tx){ 138 | tx.executeSql(query, null, function(result){ 139 | var dropTablesSql = []; 140 | for (var i = 0; i < result.length; i++) 141 | dropTablesSql.push([result[i].dropTable, null]); 142 | 143 | persistence.executeQueriesSeq(tx, dropTablesSql, function(){ 144 | Migrator.setVersion(0, function(){Migrator.reset(start);}); 145 | }); 146 | }); 147 | }); 148 | } 149 | }); 150 | 151 | asyncTest("API", 12, function(){ 152 | var m = Migrator.migration(1, { 153 | up: function() { 154 | ok(typeof(this.addColumn) == "function", 'addColumn'); 155 | ok(typeof(this.removeColumn) == "function", 'removeColumn'); 156 | ok(typeof(this.addIndex) == "function", 'addIndex'); 157 | ok(typeof(this.removeIndex) == "function", 'removeIndex'); 158 | ok(typeof(this.executeSql) == "function", 'execute'); 159 | ok(typeof(this.dropTable) == "function", 'dropTable'); 160 | ok(typeof(this.createTable) == "function", 'createTable'); 161 | 162 | this.createTable('posts', function(table){ 163 | ok(typeof(table.text) == "function", 'text column'); 164 | ok(typeof(table.integer) == "function", 'integer column'); 165 | ok(typeof(table.boolean) == "function", 'boolean column'); 166 | ok(typeof(table.json) == "function", 'json column'); 167 | ok(typeof(table.date) == "function", 'date column'); 168 | }); 169 | } 170 | }); 171 | 172 | m.up(start); 173 | }); 174 | 175 | asyncTest("execute", 1, function(){ 176 | Migrator.migration(1, { 177 | up: function() { 178 | this.executeSql('CREATE TABLE test (id INTEGER)'); 179 | } 180 | }); 181 | 182 | Migrator.migrate(function(){ 183 | var sql = 'select name from sqlite_master where type = "table" and name == "test"'; 184 | persistence.transaction(function(tx){ 185 | tx.executeSql(sql, null, function(result){ 186 | ok(result.length == 1, 'sql command ran'); 187 | start(); 188 | }); 189 | }); 190 | }); 191 | }); 192 | 193 | asyncTest("createTable", 1, function(){ 194 | Migrator.migration(1, { 195 | up: function() { 196 | this.createTable('testing'); 197 | } 198 | }); 199 | 200 | Migrator.migrate(function(){ 201 | tableExists('testing', start) 202 | }); 203 | }); 204 | 205 | asyncTest("createTable adds id by default", 1, function(){ 206 | Migrator.migration(1, { 207 | up: function() { 208 | this.createTable('testing'); 209 | } 210 | }); 211 | 212 | Migrator.migrate(function(){ 213 | columnExists('testing', 'id', 'VARCHAR(32) PRIMARY KEY', start); 214 | }); 215 | }); 216 | 217 | asyncTest("createTable with text column", 1, function(){ 218 | Migrator.migration(1, { 219 | up: function() { 220 | this.createTable('customer', function(t){ 221 | t.text('name'); 222 | }); 223 | } 224 | }); 225 | 226 | Migrator.migrate(function(){ 227 | columnExists('customer', 'name', 'TEXT', start); 228 | }); 229 | }); 230 | 231 | asyncTest("createTable with integer column", 1, function(){ 232 | Migrator.migration(1, { 233 | up: function() { 234 | this.createTable('customer', function(t){ 235 | t.integer('age'); 236 | }); 237 | } 238 | }); 239 | 240 | Migrator.migrate(function(){ 241 | columnExists('customer', 'age', 'INT', start); 242 | }); 243 | }); 244 | 245 | asyncTest("createTable with boolean column", 1, function(){ 246 | Migrator.migration(1, { 247 | up: function() { 248 | this.createTable('customer', function(t){ 249 | t.boolean('married'); 250 | }); 251 | } 252 | }); 253 | 254 | Migrator.migrate(function(){ 255 | columnExists('customer', 'married', 'BOOL', start); 256 | }); 257 | }); 258 | 259 | asyncTest("createTable with date column", 1, function(){ 260 | Migrator.migration(1, { 261 | up: function() { 262 | this.createTable('customer', function(t){ 263 | t.date('birth'); 264 | }); 265 | } 266 | }); 267 | 268 | Migrator.migrate(function(){ 269 | columnExists('customer', 'birth', 'DATE', start); 270 | }); 271 | }); 272 | 273 | asyncTest("createTable with json column", 1, function(){ 274 | Migrator.migration(1, { 275 | up: function() { 276 | this.createTable('customer', function(t){ 277 | t.json('sample_json'); 278 | }); 279 | } 280 | }); 281 | 282 | Migrator.migrate(function(){ 283 | columnExists('customer', 'sample_json', 'TEXT', start); 284 | }); 285 | }); 286 | 287 | asyncTest("addColumn", 1, function(){ 288 | Migrator.migration(1, { 289 | up: function() { 290 | this.createTable('customer'); 291 | this.addColumn('customer', 'name', 'TEXT'); 292 | } 293 | }); 294 | 295 | Migrator.migrate(function(){ 296 | columnExists('customer', 'name', 'TEXT', start); 297 | }); 298 | }); 299 | 300 | asyncTest("removeColumn", 2, function(){ 301 | Migrator.migration(1, { 302 | up: function() { 303 | this.createTable('customer', function(t){ 304 | t.json('sample_json'); 305 | }); 306 | this.removeColumn('customer', 'sample_json'); 307 | } 308 | }); 309 | 310 | Migrator.migrate(function(){ 311 | columnExists('customer', 'id', 'VARCHAR(32) PRIMARY KEY'); 312 | columnNotExists('customer', 'sample_json', 'TEXT', start); 313 | }); 314 | }); 315 | 316 | asyncTest("dropTable", 1, function(){ 317 | Migrator.migration(1, { 318 | up: function() { 319 | this.createTable('customer'); 320 | this.dropTable('customer'); 321 | } 322 | }); 323 | 324 | Migrator.migrate(function(){ 325 | tableNotExists('customer', start); 326 | }); 327 | }); 328 | 329 | asyncTest("addIndex", 1, function(){ 330 | Migrator.migration(1, { 331 | up: function() { 332 | this.createTable('customer', function(t){ 333 | t.integer('age'); 334 | }); 335 | this.addIndex('customer', 'age'); 336 | } 337 | }); 338 | 339 | Migrator.migrate(function(){ 340 | indexExists('customer', 'age', start); 341 | }); 342 | }); 343 | 344 | asyncTest("removeIndex", 1, function(){ 345 | Migrator.migration(1, { 346 | up: function() { 347 | this.createTable('customer', function(t){ 348 | t.integer('age'); 349 | }); 350 | this.addIndex('customer', 'age'); 351 | this.removeIndex('customer', 'age'); 352 | } 353 | }); 354 | 355 | Migrator.migrate(function(){ 356 | indexNotExists('customer', 'age', start); 357 | }); 358 | }); 359 | 360 | module("Models", { 361 | setup: function() { 362 | 363 | stop(); 364 | 365 | this.Task = persistence.define('Task', { 366 | name: "TEXT", 367 | description: "TEXT", 368 | done: "BOOL" 369 | }); 370 | 371 | Migrator.migration(1, { 372 | up: function() { 373 | this.createTable('Task', function(t){ 374 | t.text('name'); 375 | t.text('description'); 376 | t.boolean('done'); 377 | }); 378 | }, 379 | down: function() { 380 | this.dropTable('Task'); 381 | } 382 | }); 383 | 384 | Migrator.migrate(function(){ 385 | start(); 386 | }); 387 | }, 388 | teardown: function() { 389 | stop(); 390 | 391 | Migrator.migrate(0, function(){ 392 | start(); 393 | }); 394 | } 395 | }); 396 | 397 | asyncTest("Adding and retrieving Entity after migration", 1, function(){ 398 | var task = new this.Task({name: 'test'}); 399 | var allTasks = this.Task.all(); 400 | 401 | persistence.add(task).flush(function() { 402 | persistence.clean(); delete task; 403 | allTasks.list(function(result){ 404 | equals(result.length, 1, 'task found'); 405 | start(); 406 | }); 407 | }); 408 | }); 409 | 410 | module("Custom actions", { 411 | setup: function() { 412 | stop(); 413 | 414 | this.User = persistence.define('User', { 415 | userName: "TEXT", 416 | email: "TEXT" 417 | }); 418 | 419 | Migrator.migration(1, { 420 | up: function() { 421 | this.createTable('User', function(t){ 422 | t.text('userName'); 423 | }); 424 | }, 425 | down: function() { 426 | this.dropTable('User'); 427 | } 428 | }); 429 | 430 | Migrator.migrate(function(){ 431 | start(); 432 | }); 433 | }, 434 | teardown: function() { 435 | stop(); 436 | 437 | Migrator.migrate(0, function(){ 438 | start(); 439 | }); 440 | } 441 | }); 442 | 443 | 444 | asyncTest("Running custom actions", 1, function(){ 445 | var user1 = new this.User({userName: 'user1'}); 446 | var user2 = new this.User({userName: 'user2'}); 447 | var allUsers = this.User.all(); 448 | 449 | function addUsers() { 450 | persistence.add(user1).add(user2).flush(createAndRunMigration); 451 | } 452 | 453 | function createAndRunMigration() { 454 | Migrator.migration(2, { 455 | up: function() { 456 | this.addColumn('User', 'email', 'TEXT'); 457 | this.action(function(tx, nextAction){ 458 | ok(true); 459 | nextAction(); 460 | }); 461 | } 462 | }); 463 | Migrator.migrate(start); 464 | } 465 | 466 | addUsers(); 467 | }); 468 | 469 | }); // end persistence.migrations.init() 470 | }); 471 | -------------------------------------------------------------------------------- /lib/persistence.jquery.mobile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2010 Roberto Saccon 3 | * 4 | * Permission is hereby granted, free of charge, to any person 5 | * obtaining a copy of this software and associated documentation 6 | * files (the "Software"), to deal in the Software without 7 | * restriction, including without limitation the rights to use, 8 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the 10 | * Software is furnished to do so, subject to the following 11 | * conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be 14 | * included in all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | * OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | 26 | if (!window.persistence.jquery) { 27 | throw new Error("persistence.jquery.js should be loaded before persistence.jquery.mobile.js"); 28 | } 29 | 30 | persistence.jquery.mobile = {}; 31 | 32 | (function($){ 33 | var $pjqm = persistence.jquery.mobile; 34 | 35 | if (window.openDatabase) { 36 | $pjqm.pageEntityName = "Page"; 37 | $pjqm.imageEntityName = "Image"; 38 | $pjqm.pathField = "path"; 39 | $pjqm.dataField = "data"; 40 | 41 | var originalAjaxMethod = $.ajax; 42 | 43 | function expand(docPath, srcPath) { 44 | var basePath = (/\/$/.test(location.pathname) || (location.pathname == "")) ? 45 | location.pathname : 46 | location.pathname.substring(0, location.pathname.lastIndexOf("/")); 47 | if (/^\.\.\//.test(srcPath)) { 48 | // relative path with upward directory traversal 49 | var count = 1, splits = docPath.split("/"); 50 | while (/^\.\.\//.test(srcPath)) { 51 | srcPath = srcPath.substring(3); 52 | count++; 53 | } 54 | return basePath + ((count >= splits.length) ? 55 | srcPath : 56 | splits.slice(0, splits.length-count).join("/") + "/" + srcPath); 57 | } else if (/^\//.test(srcPath)) { 58 | // absolute path 59 | return srcPath; 60 | } else { 61 | // relative path without directory traversal 62 | return basePath + docPath + "/" + srcPath; 63 | } 64 | } 65 | 66 | function base64Image(img, type) { 67 | var canvas = document.createElement("canvas"); 68 | canvas.width = img.width; 69 | canvas.height = img.height; 70 | 71 | // Copy the image contents to the canvas 72 | var ctx = canvas.getContext("2d"); 73 | ctx.drawImage(img, 0, 0); 74 | 75 | return canvas.toDataURL("image/" + type); 76 | } 77 | 78 | // parseUri 1.2.2 79 | // (c) Steven Levithan 80 | // MIT License 81 | 82 | var parseUriOptions = { 83 | strictMode: false, 84 | key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"], 85 | q: { 86 | name: "queryKey", 87 | parser: /(?:^|&)([^&=]*)=?([^&]*)/g 88 | }, 89 | parser: { 90 | strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/, 91 | loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ 92 | } 93 | }; 94 | 95 | function parseUri (str) { 96 | var o = parseUriOptions, 97 | m = o.parser[o.strictMode ? "strict" : "loose"].exec(str), 98 | uri = {}, 99 | i = 14; 100 | 101 | while (i--) uri[o.key[i]] = m[i] || ""; 102 | 103 | uri[o.q.name] = {}; 104 | uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) { 105 | if ($1) uri[o.q.name][$1] = $2; 106 | }); 107 | 108 | return uri; 109 | } 110 | 111 | function getImageType(parsedUri) { 112 | if (parsedUri.queryKey.type) { 113 | return parsedUri.queryKey.type; 114 | } else { 115 | return (/\.png$/i.test(parsedUri.path)) ? "png" : "jpeg"; 116 | } 117 | } 118 | 119 | $.ajax = function(settings) { 120 | var parsedUrl = parseUri(settings.url); 121 | var entities = {}, urlPathSegments = parsedUrl.path.split("/"); 122 | if ((settings.type == "post") && (urlPathSegments.length > 1)) { 123 | var entityName = (urlPathSegments[1].charAt(0).toUpperCase() + urlPathSegments[1].substring(1)); 124 | if (persistence.isDefined(entityName)) { 125 | var Form = persistence.define(entityName); 126 | 127 | var persistFormData = function() { 128 | var obj = {}; 129 | settings.data.replace(/(?:^|&)([^&=]*)=?([^&]*)/g, function ( $0, $1, $2 ) { 130 | if ($1) { 131 | obj[$1] = $2; 132 | } 133 | }); 134 | 135 | var entity = new Form(obj); 136 | persistence.add(entity); 137 | persistence.flush(); 138 | }; 139 | 140 | if (!navigator.hasOwnProperty("onLine") || navigator.onLine) { 141 | originalAjaxMethod({ 142 | url: settings.url, 143 | success: function(data) { 144 | settings.success(data); 145 | persistFormData(); 146 | }, 147 | error: settings.error 148 | }); 149 | } else { 150 | persistFormData(); 151 | } 152 | } else { 153 | originalAjaxMethod(settings); 154 | } 155 | } else if (persistence.urlExcludeRx && persistence.urlExcludeRx.test(parsedUrl.path)) { 156 | originalAjaxMethod(settings); 157 | } else { 158 | if (persistence.isDefined($pjqm.pageEntityName)) { 159 | var Page = persistence.define($pjqm.pageEntityName); 160 | Page.findBy($pjqm.pathField, settings.url, function(page) { 161 | if (page) { 162 | // 163 | // load page and images from persistencejs 164 | // 165 | if (settings.success) { 166 | var pos = 0, countOuter = 0, countInner = 0; 167 | var inStr = page[$pjqm.dataField](), outStr = ""; 168 | var regExp = /(]+src\s*=\s*[\'\"])([^\'\"]+)([\'\"][^>]*>)/ig; 169 | var replaced = inStr.replace(regExp, function($0, $1, $2, $3, offset) { 170 | countOuter++; 171 | if (persistence.isDefined($pjqm.imageEntityName)) { 172 | var Img = persistence.define($pjqm.imageEntityName); 173 | Img.findBy($pjqm.pathField, expand(settings.url, $2), function(image){ 174 | countInner++; 175 | if (image) { 176 | var imgTagStr = $1 + image[$pjqm.dataField]() + $3; 177 | outStr += inStr.substring(pos, offset) + imgTagStr; 178 | pos = offset + imgTagStr.length; 179 | } else { 180 | outStr += inStr.substring(pos, offset) + imgTagStr; 181 | pos = offset; 182 | } 183 | if (countInner == countOuter) { 184 | settings.success(outStr); 185 | } 186 | return ""; 187 | }); 188 | } else { 189 | outStr += inStr.substring(pos, offset) + imgTagStr; 190 | pos = offset; 191 | } 192 | }); 193 | if (replaced == inStr) { 194 | settings.success(inStr); 195 | } else if (!persistence.isDefined($pjqm.imageEntityName)) { 196 | settings.success(outStr); 197 | }; 198 | } 199 | } else { 200 | // 201 | // ajax-load page and persist page and images 202 | // 203 | originalAjaxMethod({ 204 | url: settings.url, 205 | success: function(data) { 206 | settings.success(data); 207 | if (persistence.isDefined($pjqm.pageEntityName)) { 208 | var entities = [], crawlImages = false; 209 | var Page = persistence.define($pjqm.pageEntityName); 210 | if (persistence.isDefined($pjqm.imageEntityName)) { 211 | var Img = persistence.define($pjqm.imageEntityName), count = 0; 212 | $("#"+settings.url.replace(/\//g,"\\/").replace(/\./g,"\\.")+" img").each(function(i, img){ 213 | crawlImages = true; 214 | count++; 215 | $(img).load(function() { 216 | var obj = {}, parsedImgSrc = parseUri(img.src); 217 | obj[$pjqm.pathField] = parsedImgSrc.path; 218 | obj[$pjqm.dataField] = base64Image(img, getImageType(parsedImgSrc)); 219 | entities.push(new Img(obj)); 220 | 221 | if (crawlImages && (--count == 0)) { 222 | for (var j=0; j