Repository: zefhemel/persistencejs Branch: master Commit: f692c4eb554a Files: 78 Total size: 558.7 KB Directory structure: gitextract_o626fc1h/ ├── .gitignore ├── AUTHORS ├── CHANGES ├── README.md ├── bower.json ├── demo/ │ └── jquerymobile/ │ ├── README.md │ ├── docs/ │ │ ├── text.html │ │ └── text_and_images.html │ ├── index.html │ └── order/ │ └── form-fake-response.html ├── docs/ │ ├── DEVELOPMENT.md │ ├── jquery.md │ ├── jquery.mobile.md │ ├── migrations.md │ ├── search.md │ └── sync.md ├── index.js ├── lib/ │ ├── index.js │ ├── persistence.jquery.js │ ├── persistence.jquery.mobile.js │ ├── persistence.js │ ├── persistence.migrations.js │ ├── persistence.pool.js │ ├── persistence.search.js │ ├── persistence.store.appengine.js │ ├── persistence.store.config.js │ ├── persistence.store.cordovasql.js │ ├── persistence.store.memory.js │ ├── persistence.store.mysql.js │ ├── persistence.store.react-native.js │ ├── persistence.store.sql.js │ ├── persistence.store.sqlite.js │ ├── persistence.store.sqlite3.js │ ├── persistence.store.titanium.js │ ├── persistence.store.websql.js │ ├── persistence.sync.js │ ├── persistence.sync.server.js │ ├── persistence.sync.server.php │ └── persistence.sync.server.php.sql ├── package.json └── test/ ├── appengine/ │ └── test.js ├── browser/ │ ├── qunit/ │ │ ├── jquery.js │ │ ├── qunit.css │ │ └── qunit.js │ ├── tasks.client.js │ ├── tasks.html │ ├── test.jquery-persistence.html │ ├── test.jquery-persistence.js │ ├── test.migrations.html │ ├── test.migrations.js │ ├── test.mixin.html │ ├── test.mixin.js │ ├── test.persistence.html │ ├── test.persistence.js │ ├── test.search.html │ ├── test.search.js │ ├── test.sync.html │ ├── test.sync.js │ ├── test.uki-persistence.html │ ├── test.uki-persistence.js │ ├── uki/ │ │ └── uki-persistence.js │ └── util.js ├── node/ │ ├── node-blog.js │ ├── partial.sync.schema.sql │ ├── test.error.handling.js │ ├── test.memory.store.js │ ├── test.sqlite.store.js │ ├── test.sqlite3.store.js │ ├── test.store.config.js │ └── test.sync.server.js └── titanium/ ├── .gitignore ├── Resources/ │ ├── app.js │ ├── qunit/ │ │ ├── qunit.js │ │ └── titanium_adaptor.js │ ├── runner.js │ └── test/ │ └── tests_to_run.js ├── manifest └── tiapp.xml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ test/titanium/build ================================================ FILE: AUTHORS ================================================ # Authors ordered by first contribution. Zef Hemel Fabio Rehm Lukas Berns Roberto Saccon Wilker Lúcio Bruno Jouhier Robin Wenglewski Matthias Hochgatterer Chris Chua Mike Smullin Masahiro Hayashi Mick Staugaard Shane Tomlinson Eugene Ware ================================================ FILE: CHANGES ================================================ Changes ======= * Moved all the SQL stuff into persistence.store.sql.js, and WebSQL to persistence.store.websql.js. So, to use a WebSQL browser database you need to include 3 files in your HTML now: Then, instead of using `persistence.connect` use: persistence.store.websql.config(persistence, 'dbname', 'My db', 5 * 1024 * 1024); For node.js and MySQL: var persistence = require('./persistence').persistence; var persistenceStore = require('./persistence.store.mysql'); persistenceStore.config(persistence, 'localhost', 'somedb', 'user', 'pw'); var session = persistenceStore.getSession(); ... session.close(); * persistence.db.log is now called persistence.debug v0.1.1: Last version with only one persistence.js file ================================================ FILE: README.md ================================================ persistence.js ============== `persistence.js` is a asynchronous Javascript object-relational mapper library. It can be used both in the web browser and on the server using [node.js](http://nodejs.org). It currently supports 4 types of data stores: * [HTML5 WebSQL database](http://dev.w3.org/html5/webdatabase/), a somewhat controversial part of HTML5 that is supported in Webkit browsers, specifically on mobile devices, including iPhone, Android and Palm's WebOS. * [Google Gears](http://gears.google.com), a browser plug-in that adds a number of feature to the browser, including a in-browser database. * [MySQL](http://www.mysql.com), using the [node-mysql](http://github.com/felixge/node-mysql), node.js module on the server. * In-memory, as a fallback. Keeps the database in memory and is cleaned upon a page refresh (or server restart), unless saved to [localStorage](http://dev.w3.org/html5/webstorage/). There is also an experimental support for [Qt 4.7 Declarative UI framework (QML)](http://doc.trolltech.org/4.7-snapshot/declarativeui.html) which is an extension to JavaScript. For browser use, `persistence.js` has no dependencies on any other frameworks, other than the Google Gears [initialization script](http://code.google.com/apis/gears/gears_init.js), in case you want to enable Gears support. Plug-ins -------- There are a few `persistence.js` plug-ins available that add functionality: * `persistence.search.js`, adds simple full-text search capabilities, see `docs/search.md` for more information. * `persistence.migrations.js`, supports data migrations (changes to the database schema), see `docs/migrations.md` for more information. * `persistence.sync.js`, supports database synchronization with a remote server, see `docs/sync.md` for more information. * `jquery.persistence.js`, adds jQuery integration, including jQuery-mobile ajax request interception and re-routing to persistencejs, see `docs/jquery.md` for more information and `demo/jquerymobile` for a simple demo. A Brief Intro to Async Programming ---------------------------------- In browsers, Javascript and the web page's rendering engine share a single thread. The result of this is that only one thing can happen at a time. If a database query would be performed _synchronously_, like in many other programming environments like Java and PHP the browser would freeze from the moment the query was issued until the results came back. Therefore, many APIs in Javascript are defined as _asynchronous_ APIs, which mean that they do not block when an "expensive" computation is performed, but instead provide the call with a function that will be invoked once the result is known. In the meantime, the browser can perform other duties. For instance, a synchronous database call call would look as follows: var results = db.query("SELECT * FROM Table"); for(...) { ... } The execution of the first statement could take half a second, during which the browser doesn't do anything else. By contrast, the asynchronous version looks as follows: db.query("SELECT * FROM Table", function(results) { for(...) { ... } }); Note that there will be a delay between the `db.query` call and the result being available and that while the database is processing the query, the execution of the Javascript continues. To make this clear, consider the following program: db.query("SELECT * FROM Table", function(results) { console.log("hello"); }); console.log("world"); Although one could assume this would print "hello", followed by "world", the result will likely be that "world" is printed before "hello", because "hello" is only printed when the results from the query are available. This is a tricky thing about asynchronous programming that a Javascript developer will have to get used to. Using persistence.js in the browser =================================== Browser support --------------- * Modern webkit browsers (Google Chrome and Safari) * Firefox (through Google Gears) * Opera * Android browser (tested on 1.6 and 2.x) * iPhone browser (iPhone OS 3+) * Palm WebOS (tested on 1.4.0) * Other browsers supporting `localStorage` (e.g. Firefox) (The following is being worked on:) Internet Explorer is likely not supported (untested) because it lacks `__defineGetter__` and `__defineSetter__` support, which `persistence.js` uses heavily. This may change in IE 9. Setting up ---------- * Using `bower`: ```shell bower install persistence ``` Add a ` If you want to use the in-memory store (in combination with `localStorage`) you also need the `persistence.store.memory.js` included. * Using directly from source: git clone git://github.com/zefhemel/persistencejs.git Copy directories you will need following almost the same instructions above. Setup your database ------------------- You need to explicitly configure the data store you want to use, configuration of the data store is store-specific. The WebSQL store (which includes Google Gears support) is configured as follows: persistence.store.websql.config(persistence, 'yourdbname', 'A database description', 5 * 1024 * 1024); The first argument is always supposed to be `persistence`. The second in your database name (it will create it if it does not already exist, the third is a description for you database, the last argument is the maximum size of your database in bytes (5MB in this example). ## Setting up for Cordova with SQLitePlugin/WebSQL Use following if you want to use `persistencejs` in a [Cordova](https://cordova.apache.org/) mobile app and you plan to use the [Cordova SQLitePlugin](https://github.com/brodysoft/Cordova-SQLitePlugin): persistence.store.cordovasql.config( persistence, 'yourdbname', '0.0.1', // DB version 'My database', // DB display name 5 * 1024 * 1024, // DB size (WebSQL fallback only) 0, // SQLitePlugin Background processing disabled 2 // DB location (iOS only), 0 (default): Documents, 1: Library, 2: Library/LocalDatabase // 0: iTunes + iCloud, 1: NO iTunes + iCloud, 2: NO iTunes + NO iCloud // More information at https://github.com/litehelpers/Cordova-sqlite-storage#opening-a-database ); For more information on the SQLitePlugin background processing please refer to the [SQLitePlugin](https://github.com/brodysoft/Cordova-SQLitePlugin) readme. The Cordova support in `persistencejs` will try to work with the [SQLitePlugin](https://github.com/brodysoft/Cordova-SQLitePlugin) if it is loaded; if not it will automatically fall back to [WebSQL](http://docs.phonegap.com/en/edge/cordova_storage_storage.md.html#Storage). Please note that to use Cordova store, you must use the master branch, because it is not included up to release v0.3.0. The in-memory store --------------------------------------- The in-memory store is offered as a fallback for browsers that do not support any of the other supported stores (e.g. WebSQL or Gears). In principal, it only keeps data in memory, which means that navigating away from the page (including a reload or tab close) will result in the loss of all data. A way around this is using the `persistence.saveToLocalStorage` and `persistence.loadFromLocalStorage` functions that can save the entire database to the [localStorage](http://dev.w3.org/html5/webstorage/), which is persisted indefinitely (similar to WebSQL). If you're going to use the in-memory store, you can configure it as follows: persistence.store.memory.config(persistence); Then, if desired, current data can be loaded from the localStorage using: persistence.loadFromLocalStorage(function() { alert("All data loaded!"); }); And saved using: persistence.saveToLocalStorage(function() { alert("All data saved!"); }); Drawbacks of the in-memory store: * Performance: All actions that are typically performed by a database (sorting, filtering), are now all performed in-memory using Javascript. * Limited database size: Loading and saving requires serialization of all data from and to JSON, which gets more expensive as your dataset grows. Most browsers have a maximum size of 5MB for `localStorage`. * Synchronous behavior: Although the API is asynchronous, all persistence actions will be performed synchronously on the main Javascript thread, which may make the browser less responsive. Schema definition ----------------- A data model is declared using `persistence.define`. The following two definitions define a `Task` and `Category` entity with a few simple properties. The property types are based on [SQLite types](http://www.sqlite.org/datatype3.html), specifically supported types are (but any SQLite type is supported): * `TEXT`: for textual data * `INT`: for numeric values * `BOOL`: for boolean values (`true` or `false`) * `DATE`: for date/time value (with precision of 1 second) * `JSON`: a special type that can be used to store arbitrary [JSON](http://www.json.org) data. Note that this data can not be used to filter or sort in any sensible way. If internal changes are made to a `JSON` property, `persistence.js` may not register them. Therefore, a manual call to `anObj.markDirty('jsonPropertyName')` is required before calling `persistence.flush`. Example use: var Task = persistence.define('Task', { name: "TEXT", description: "TEXT", done: "BOOL" }); var Category = persistence.define('Category', { name: "TEXT", metaData: "JSON" }); var Tag = persistence.define('Tag', { name: "TEXT" }); The returned values are constructor functions and can be used to create new instances of these entities later. It is possible to create indexes on one or more columns using `EntityName.index`, for instance: Task.index('done'); Task.index(['done', 'name']); These indexes can also be used to impose unique constraints : Task.index(['done', 'name'],{unique:true}); Relationships between entities are defined using the constructor function's `hasMany` call: // This defines a one-to-many relationship: Category.hasMany('tasks', Task, 'category'); // These two definitions define a many-to-many relationship Task.hasMany('tags', Tag, 'tasks'); Tag.hasMany('tasks', Task, 'tags'); The first statement defines a `tasks` relationship on category objects containing a `QueryCollection` (see the section on query collections later) of `Task`s, it also defines an inverse relationship on `Task` objects with the name `category`. The last two statements define a many-to-many relationships between `Task` and `Tag`. `Task` gets a `tags` property (a `QueryCollection`) containing all its tags and vice versa, `Tag` gets a `tasks` property containing all of its tasks. The defined entity definitions are synchronized (activated) with the database using a `persistence.schemaSync` call, which takes a callback function (with a newly created transaction as an argument), that is called when the schema synchronization has completed, the callback is optional. persistence.schemaSync(); // or persistence.schemaSync(function(tx) { // tx is the transaction object of the transaction that was // automatically started }); There is also a migrations plugin you can check out, documentation can be found in [docs/migrations.md](docs/migrations.md) file. Mix-ins ------- You can also define mix-ins and apply them to entities of the model. A mix-in definition is similar to an entity definition, except using `defineMixin` rather than just `define`. For example: var Annotatable = persistence.defineMixin('Annotatable', { lastAnnotated: "DATE" }); You can define relationships between mix-in and entities. For example: // A normal entity var Note = persistence.define('Note', { text: "TEXT" }); // relationship between a mix-in and a normal entity Annotatable.hasMany('notes', Note, 'annotated'); Once you have defined a mix-in, you can apply it to any entity of your model, with the `Entity.is(mixin)` method. For example: Project.is(Annotatable); Task.is(Annotatable); Now, your `Project` and `Task` entities have an additional `lastAnnotated` property. They also have a one to many relationship called `notes` to the `Note` entity. And you can also traverse the reverse relationship from a `Note` to its `annotated` object. Note that `annotated` is a polymorphic relationship as it may yield either a `Project` or a `Task` (or any other entity which is `Annotatable'). Note: Prefetch is not allowed (yet) on a relationship that targets a mixin. In the example above you cannot prefetch the `annotated` relationship when querying the `Note` entity. Notes: this feature is very experimental at this stage. It needs more testing. Support for "is a" relationships (classical inheritance) is also in the works. Creating and manipulating objects --------------------------------- New objects can be instantiated with the constructor functions. Optionally, an object with initial property values can be passed as well, or the properties may be set later: var task = new Task(); var category = new Category({name: "My category"}); category.metaData = {rating: 5}; var tag = new Tag(); tag.name = "work"; Many-to-one relationships are accessed using their specified name, e.g.: task.category = category; One-to-many and many-to-many relationships are access and manipulated through the `QueryCollection` API that will be discussed later: task.tags.add(tag); tasks.tags.remove(tag); tasks.tags.list(tx, function(allTags) { console.log(allTags); }); Persisting/removing objects --------------------------- Similar to [hibernate](http://www.hibernate.org), `persistence.js` uses a tracking mechanism to determine which objects' changes have to be persisted to the database. All objects retrieved from the database are automatically tracked for changes. New entities can be tracked to be persisted using the `persistence.add` function: var c = new Category({name: "Main category"}); persistence.add(c); for ( var i = 0; i < 5; i++) { var t = new Task(); t.name = 'Task ' + i; t.done = i % 2 == 0; t.category = c; persistence.add(t); } Objects can also be removed from the database: persistence.remove(c); All changes made to tracked objects can be flushed to the database by using `persistence.flush`, which takes a transaction object and callback function as arguments. A new transaction can be started using `persistence.transaction`: persistence.transaction(function(tx) { persistence.flush(tx, function() { alert('Done flushing!'); }); }); For convenience, it is also possible to not specify a transaction or callback, in that case a new transaction will be started automatically. For instance: persistence.flush(); // or, with callback persistence.flush(function() { alert('Done flushing'); }); Note that when no callback is defined, the flushing still happens asynchronously. __Important__: Changes and new objects will not be persisted until you explicitly call `persistence.flush()`. The exception to this rule is using the `list(...)` method on a database `QueryCollection`, which also flushes first, although this behavior may change in the future. Dumping and restoring data -------------------------- The library supports two kinds of dumping and restoring data. `persistence.dump` can be used to create an object containing a full dump of a database. Naturally, it is adviced to only do this with smaller databases. Example: persistence.dump(tx, [Task, Category], function(dump) { console.log(dump); }); The `tx` is left out, a new transaction will be started for the operation. If the second argument is left out, `dump` defaults to dumping _all_ defined entities. The dump format is: {"entity-name": [list of instances], ...} `persistence.load` is used to restore the dump produced by `persistence.dump`. Usage: persistence.load(tx, dumpObj, function() { alert('Dump restored!'); }); The `tx` argument can be left out to automatically start a new transaction. Note that `persistence.load` does not empty the database first, it simply attempts to add all objects to the database. If objects with, e.g. the same ID already exist, this will fail. Similarly, `persistence.loadFromJson` and `persistence.dumpToJson` respectively load and dump all the database's data as JSON strings. Entity constructor functions ---------------------------- The constructor function returned by a `persistence.define` call cannot only be used to instantiate new objects, it also has some useful methods of its own: * `EntityName.all([session])` returns a query collection containing all persisted instances of that object. The `session` argument is optional and only required when `persistence.js` is used in multi-session mode. * `EntityName.load([session], [tx], id, callback)` loads an particular object from the database by id or returns `null` if it has not been found. * `EntityName.findBy([session], [tx], property, value, callback)` searches for a particular object based on a property value (this is assumed to be unique), the callback function is called with the found object or `null` if it has not been found. * `EntityName.index([col1, col2, ..., colN], options)` creates an index on a column of a combination of columns, for faster searching. If options.unique is true, the index will impose a unique constraint on the values of the columns. And of course the methods to define relationships to other entities: * `EntityName.hasMany(property, Entity, inverseProperty)` defines a 1:N or N:M relationship (depending on the inverse property) * `EntityName.hasOne(property, Entity)` defines a 1:1 or N:1 relationship Entity objects -------------- Entity instances also have a few predefined properties and methods you should be aware of: * `obj.id`, contains the identifier of your entity, this is a automatically generated (approximation of a) UUID. You should never write to this property. * `obj.fetch(prop, callback)`, if an object has a `hasOne` relationship to another which has not yet been fetched from the database (e.g. when `prefetch` wasn't used), you can fetch in manually using `fetch`. When the property object is retrieved the callback function is invoked with the result, the result is also cached in the entity object itself. * `obj.selectJSON([tx], propertySpec, callback)`, sometime you need to extract a subset of data from an entity. You for instance need to post a JSON representation of your entity, but do not want to include all properties. `selectJSON` allows you to do that. The `propertySpec` arguments expects an array with property names. Some examples: * `['id', 'name']`, will return an object with the id and name property of this entity * `['*']`, will return an object with all the properties of this entity, not recursive * `['project.name']`, will return an object with a project property which has a name property containing the project name (hasOne relationship) * `['project.[id, name]']`, will return an object with a project property which has an id and name property containing the project name (hasOne relationship) * `['tags.name']`, will return an object with an array `tags` property containing objects each with a single property: name Query collections ----------------- A core concept of `persistence.js` is the `QueryCollection`. A `QueryCollection` represents a (sometimes) virtual collection that can be filtered, ordered or paginated. `QueryCollection`s are somewhate inspired by [Google AppEngine's Query class](http://code.google.com/appengine/docs/python/datastore/queryclass.html). A `QueryCollection` has the following methods: * `filter(property, operator, value)` Returns a new `QueryCollection` that adds a filter, filtering a certain property based on an operator and value. Supported operators are '=', '!=', '<', '<=', '>', '>=', 'in' and 'not in'. Example: `.filter('done', '=', true)` * `or(filter)` Returns a new `QueryCollection` that contains items either matching the filters specified before calling `or`, or the filter represented in the argument. The `filter` argument is of a `Filter` type, there are three types of filters: - `persistence.PropertyFilter`, which filters on properties (internally called when `filter(...)` is used. Example: `new persistence.PropertyFilter('done', '=', true)` - `persistence.AndFilter`, which is passed two filter objects as arguments, both of which should be true. Example: `new persistence.AndFilter(new persistence.PropertyFilter('done', '=', true), new persistence.PropertyFilter('archived', '=', true))` - `persistence.OrFilter`, which is passed two filter objects as arguments, one of which should be true. Example: `new persistence.OrFilter(new persistence.PropertyFilter('done', '=', true), new persistence.PropertyFilter('archived', '=', true))` * `and(filter)` same as `or(filter)` except that both conditions should hold for items to be in the collection. * `order(property, ascending)` Returns a new `QueryCollection` that will order its results by the property specified in either an ascending (ascending === true) or descending (ascending === false) order. * `limit(n)` Returns a new `QueryCollection` that limits the size of the result set to `n` items. Useful for pagination. * `skip(n)` Returns a new `QueryCollection` that skips the first `n` results. Useful for pagination. * `prefetch(rel)` Returns a new `QueryCollection` that prefetches entities linked through relationship `rel`, note that this only works for one-to-one and many-to-one relationships. * `add(obj)` Adds object `obj` to the collection. * `remove(obj)` Removes object `obj` from the collection. * `list([tx], callback)` Asynchronously fetches the results matching the formulated query. Once retrieved, the callback function is invoked with an array of entity objects as argument. * `each([tx], eachCallback)` Asynchronously fetches the results matching the formulated query. Once retrieved, the `eachCallback` function is invoked on each element of the result objects. * `forEach([tx], eachCallback)` Alias for `each` * `one([tx], callback)` Asynchronously fetches the first element of the collection, or `null` if none. * `destroyAll([tx], callback)` Asynchronously removes all the items in the collection. __Important__: this does not only remove the items from the collection, but removes the items themselves! * `count([tx], callback)` Asynchronously counts the number of items in the collection. The arguments passed to the `callback` function is the number of items. Query collections are returned by: * `EntityName.all()`, e.g. `Task.all()` * one-to-many and many-to-many relationships, e.g. `task.tags` Example: var allTasks = Task.all().filter("done", '=', true).prefetch("category").order("name", false).limit(10); allTasks.list(null, function (results) { results.forEach(function (r) { console.log(r.name) window.task = r; }); }); Using persistence.js on the server ================================== Installing `persistence.js` on node is easy using [npm](http://npmjs.org): npm install persistencejs Sadly the node.js server environment requires slight changes to `persistence.js` to make it work with multiple database connections: * A `Session` object needs to be passed as an extra argument to certain method calls, typically as a first argument. * Methods previously called on the `persistence` object itself are now called on the `Session` object. An example `node.js` application is included in `test/node-blog.js`. Setup ----- You need to `require` two modules, the `persistence.js` library itself and the MySQL backend module. var persistence = require('persistencejs'); var persistenceStore = persistence.StoreConfig.init(persistence, { adaptor: 'mysql' }); Then, you configure the database settings to use: persistenceStore.config(persistence, 'localhost', 3306, 'dbname', 'username', 'password'); Subsequently, for every connection you handle (assuming you're building a sever), you call the `persistenceStore.getSession()` method: var session = persistenceStore.getSession(); This session is what you pass around, typically together with a transaction object. Note that currently you can only have one transaction open per session and transactions cannot be nested. session.transaction(function(tx) { ... }); Commit and Rollback ------------------- `persistence.js` works in autocommit mode by default. You can override this behavior and enable explicit commit and rollback by passing true as first argument to `persistence.transaction`. You can then use the following two methods to control the transaction: * `transaction.commit(session, callback)` commits the changes. * `transaction.rollback(session, callback)` rollbacks the changes. Typical code will look like: session.transaction(true, function(tx) { // create/update/delete objects modifyThings(session, tx, function(err, result) { if (err) { // something went wrong tx.rollback(session, function() { console.log('changes have been rolled back: ' + ex.message); }); } else { // success tx.commit(session, function() { console.log('changes have been committed: ' result); }); }); }); Explicit commit and rollback is only supported on MySQL (server side) for now. Defining your data model ------------------------ Defining your data model is done in exactly the same way as regular `persistence.js`: var Task = persistence.define('Task', { name: "TEXT", description: "TEXT", done: "BOOL" }); A `schemaSync` is typically performed as follows: session.schemaSync(tx, function() { ... }); Creating and manipulating objects --------------------------------- Creating and manipulating objects is done much the same way as with regular `persistence.js`, except that in the entity's constructor you need to reference the `Session` again: var t = new Task(session); ... session.add(t); session.flush(tx, function() { ... }); Query collections ----------------- Query collections work the same way as in regular `persistence.js` with the exception of the `Entity.all()` method that now also requires a `Session` to be passed to it: Task.all(session).filter('done', '=', true).list(tx, function(tasks) { ... }); Closing the session ------------------- After usage, you need to close your session: session.close(); Bugs and Contributions ====================== If you find a bug, please [report it](https://github.com/zefhemel/persistencejs/issues). or fork the project, fix the problem and send me a pull request. For a list of planned features and open issues, have a look at the [issue tracker](https://github.com/zefhemel/persistencejs/issues). For support and discussion, please join the [persistence.js Google Group](http://groups.google.com/group/persistencejs). Thanks goes to the people listed in `AUTHORS` for their contributions. If you use [GWT](http://code.google.com/webtoolkit/) (the Google Web Toolkit), be sure to have a look at [Dennis Z. Jiang's GWT persistence.js wrapper](http://github.com/dennisjzh/GwtMobile-Persistence) License ======= This work is licensed under the [MIT license](http://en.wikipedia.org/wiki/MIT_License). Support this work ----------------- You can support this project by flattering it: ================================================ FILE: bower.json ================================================ { "name": "persistence", "main": "./lib/persistence.js", "version": "0.3.0", "_release": "0.3.0", "_target": "~0.3.0", "_source": "git://github.com/zefhemel/persistencejs.git", "homepage": "http://persistencejs.org", "authors": [ "Zef Hemel ", "Fabio Rehm ", "Lukas Berns", "Roberto Saccon ", "Wilker Lúcio ", "Bruno Jouhier ", "Robin Wenglewski ", "Matthias Hochgatterer ", "Chris Chua ", "Mike Smullin ", "Masahiro Hayashi ", "Mick Staugaard ", "Shane Tomlinson ", "Eugene Ware " ], "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).", "license": "MIT", "ignore": [ "**/.*", "node_modules", "test" ] } ================================================ FILE: demo/jquerymobile/README.md ================================================ To try this demo, you need to run it through a web server. `index.html` uses relative links to persistence.js (in `../../lib/persistence.js` to be exact), so this path needs to be available. Example images, design and text used in the demo are copy-pasted straight from jquerymobile documentation. ================================================ FILE: demo/jquerymobile/docs/text.html ================================================ Text only

Text only

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.

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.

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.

================================================ FILE: demo/jquerymobile/docs/text_and_images.html ================================================ Text and image

Text and image

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.

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.

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.

Smartphone and tablet designs
================================================ FILE: demo/jquerymobile/index.html ================================================ jQuery mobile / persistencejs integration

jQuery Mobile Framework

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

Alpha Release

Reset DB

Form submission

================================================ FILE: demo/jquerymobile/order/form-fake-response.html ================================================ Form submission

Sample form response

Fake response

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

================================================ FILE: docs/DEVELOPMENT.md ================================================ Documentation for developers ============================ Constructor functions --------------------- var Task = persistence.define('Task', { name: "TEXT", done: "BOOL" }); var Category = persistence.define('Category', { name: "TEXT" }); Task.hasOne('category', Category); `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: * `name`, the name of the entity as set as first argument of `define` * `fields`, the field object passed to the original `define`, consisting of property names as keys and textual column types as values. * `hasOne`, an object with relation names as keys and relationship objects as values. The relationship object currently has one field: `type`: which links to the constructor function of the type of the relation. Example: `Task.hasOne.category.type` will equal the `Category` constructor. * `hasMany`, an object with relation anmes as keys and relationship objects as values. The relationship has the following fields: * `type`: the constructor function of the relationship entity * `inverseProperty`: the property name of the inverse relation * `manyToMany`: a boolean value indicating if this is a manyToMany relationship or not (then it's a one-tomany) * `tableName`: name of the utility coupling table used for a many-to-many relationship Extension hooks ---------------- * `persistence.entityDecoratorHooks`: a list of functions (with the constructor function as argument) to be called to decorate. Useful to add new functionality to constructo functions, such as `Task.index`. * `persistence.flushHooks`: a list of functions to be called before flushing. * `persistence.schemaSyncHooks`: a list of functions to be called before syncing the schema. ================================================ FILE: docs/jquery.md ================================================ # persistence.jquery.js `persistence.jquery.js` is a jquery plugin for `persistence.js` that allows the usage of jquery notation for crossbrowser-access of persistencejs entities. Example ------- Simple example: var User = persistence.define('User', { firstname: "TEXT", lastname: "TEXT" }); var user = new User({firstname: "Joe", lastname: "Doo"}); // setter $(user).data('firstname', "Mike") // getter console.log($(user).data('firstname')); // => Mike You can find more examples in `test/test.persistence-jquery.js`. ================================================ FILE: docs/jquery.mobile.md ================================================ # persistence.jquery.mobile.js `persistence.jquery.mobile.js` is a plugin for `persistence.js` and [jQuery mobile](http://jquerymobile.com) that allows ajax request re-routing to persitencejs for: * Html text: caches ajax-loaded HTML pages in local DB. * Images (in `img` tags of ajax-loaded HTML pages): grabs/encodes them via `canvas` and caches them as data-URL strings in local DB. * Form submission (only POST requests). For ajax-loaded HTML pages and images, the content-providing entities get their name from user-overwritable default values. For form submissions, the entity is matched according to the following URL pattern: entity-name / path1/path2/../pathN Ajax re-routing to persitencejs only takes place if the required entities exist. Global settings (and it's default values): persistence.jquery.mobile.pageEntityName = "Page"; // Html page entity name persistence.jquery.mobile.imageEntityName = "Image"; // Image entity name persistence.jquery.mobile.pathField = "path"; // Entity path-field name persistence.jquery.mobile.dataField = "data"; // Entity data-field name Optional Regular Expression to exclude URLs from re-routing to persistencejs: persistence.jquery.mobile.urlExcludeRx Example: `persistence.jquery.mobile.urlExcludeRx = /^\/admin\//;` (all URL paths starting with "/admin/" are excluded) Ajax page loading example: URL: "about/intro.html" => entity name: "Page" => entity path field: "about/intro.html" => entity data field: (the HTML content of the page) Images: (all images contained in the page specified above) => entity name: "Image" => entity path field: (src attribute value of IMG tag) => entity data field: (the imgae data as Base64 encoded dataURL) Ajax form submission examples: URL (POST): "order/response.html" => entity name: "Order" => entity fields (other than path): retrieved from POST data You can find a demo at `demo/jquerymobile/index.html` (you must load it from a server). ================================================ FILE: docs/migrations.md ================================================ # persistence.migrations.js `persistence.migrations.js` is a plugin for `persistence.js` that provides a simple API for altering your databases in a structured and organised manner inspired by [Ruby on Rails migrations](http://guides.rubyonrails.org/migrations.html). ## Anatomy of a Migration persistence.defineMigration(1, { up: function() { this.createTable('Task', function(t){ t.text('name'); t.text('description'); t.boolean('done'); }); }, down: function() { this.dropTable('Task'); } }); This migration adds a table called `Task` with a string column called `name`, a text column called `description` and a boolean column called `done`. A `id VARCHAR(32) PRIMARY KEY` collumn will also be added, however since this is the default we do not need to ask for this. Reversing this migration is as simple as dropping the table. The first argument passed to `defineMigration` is the migration version which should be incremented when defining following migrations Migrations are not limited to changing the schema. You can also use them to fix bad data in the database or populate new fields: persistence.defineMigration(2, { up: function() { this.addColumn('User', 'email', 'TEXT'); // You can execute some raw SQL this.executeSql('UPDATE User SET email = username + "@domain.com"'); // OR // you can define a custom action to query for objects and manipulate them this.action(function(tx, nextAction){ allUsers.list(tx, function(result){ result.forEach(function(u){ u.email = u.userName + '@domain.com'; persistence.add(u); }); persistence.flush(tx, function() { // Please remember to call this when you are done with an action, // otherwise the system will hang nextAction(); }); }); }); } }); This migration adds a `email` column to the `User` table and sets all emails to `"{userName}@domain.com"`. ## API methods persistence.defineMigration(3, { up: function() { this.addColumn('TableName', 'columnName', 'COLUMN_TYPE'); this.removeColumn('TableName', 'columnName'); this.addIndex('TableName', 'columnName'); this.removeIndex('TableName', 'columnName'); this.executeSql('RAW SQL'); this.dropTable('TableName'); this.createTable('TableName', function(table){ table.text('textColumnName'); table.integer('integerColumnName'); table.boolean('booleanColumnName'); table.json('jsonColumnName'); // JSON columns will be mapped to TEXT columns on database table.date('dateColumnName'); }); } }); ## Running Migrations First thing you need to do is initialize migrations plugin: persistence.migrations.init(function() { // Optional callback to be executed after initialization }); Then you should load your migrations and run: * `persistence.migrate()` to run migrations up to the most recent * `persistence.migrate(function)` to run migrations up to the most recent and execute some code * `persistence.migrate(version, function)` to run migrations up / down to the specified version and execute some code To load migrations you should use something like [RequireJS](http://requirejs.org/). ================================================ FILE: docs/search.md ================================================ persistence.search.js ============== `persistence.search.js` is a light-weight extension of the `persistence.js` library that adds full-text search through a simple API. Initialization: persistence.search.config(persistence, persistence.store.websql.sqliteDialect); Example usage: var Note = persistence.define('Note', { name: "TEXT", text: "TEXT", status: "TEXT" }); Note.textIndex('name'); Note.textIndex('text'); This sample defines a `Note` entity with three properties of which `name` and `text` are full-text indexed. For this a new database table will be created that stores the index. Searching is done as follows: Note.search("note").list(tx, function(results) { console.log(results); }); or you can paginate your results using `limit` and `skip` (similar to `limit` and `skip` in QueryCollections). Note.search("note").limit(10).skip(10).list(null, function(results) { console.log(results); }); Query language -------------- Queries can contain regular words. In addition the `*` wildcard can be used anywhere with a word. The `property:` notation can be used to search only a particular field. Examples: * `note` * `name: note` * `interesting` * `inter*` * `important essential` Note that currently a result is return when _any_ word matches. Results are ranked by number of occurences of one of the words in the text. ================================================ FILE: docs/sync.md ================================================ persistence.sync.js =================== `persystence.sync.js` is a `persistence.js` plug-in that adds data synchronization with remote servers. It comes with a client-side component (`persistence.sync.js`) and a sample server-side component (`persistence.sync.server.js`) for use with [node.js](http://nodejs.org). It should be fairly easy to implement server-components using other languages, any contributions there are welcome. Client-side usage ----------------- After including both `persistence.js` and `persistence.sync.js` in your page, you can enable syncing on entities individually: var Task = persistence.define("Task", { name: "TEXT", done: "BOOL" }); Task.enableSync('/taskChanges'); The argument passed to `enableSync` is the URI of the sync server component. To initiate a sync, the `EntityName.syncAll(..)` method is used. The method signature is the following: EntityName.syncAll(conflictHandler, successCallback, errorCallback) successCallback and errorCallback are optional. successCallback occurs after a successful sync errorCallback occurs on error (I.E. a non-200 response code). conflictHandler is called in the event of a conflict between local and remote data: function conflictHandler(conflicts, updatesToPush, callback) { // Decide what to do with the conflicts here, possibly add to updatesToPush callback(); } EntityName.syncAll(conflictHandler, function() { alert('Done!'); }, errorHandler); There are two sample conflict handlers: 1. `persistence.sync.preferLocalConflictHandler`, which in case of a data conflict will always pick the local changes. 2. `persistence.sync.preferRemoteConflictHandler`, which in case of a data conflict will always pick the remote changes. For instance: EntityName.syncAll(persistence.sync.preferLocalConflictHandler, function() { alert('Done!'); }, errorCallback); Note that you are responsible for syncing all entities and that there are no database consistencies after a sync, e.g. if you only sync `Task`s that refer to a `Project` object and that `Project` object has not (yet) been synced, the database will be (temporarily) inconsistent. Server-side (Java, Slim3, AppEngine) ------------------------------------ Roberto Saccon developed a [Java server-side implementation of persistence sync using the Slim3 framework](http://github.com/rsaccon/Slim3PersistenceSync). Server-side (node.js) --------------------- The server must expose a resource located at the given URI that responds to: * `GET` requests with a `since=` GET parameter that will return a JSON object with two properties: * `now`, the timestamp of the current time at the server (in ms since 1/1/1970) * `updates`, an array of objects updated since the timestamp `since`. Each object has at least an `id` and `_lastChange` field (in the same timestamp format). For instance: /taskChanges?since=1279888110373 {"now":1279888110421, "updates": [ {"id": "F89F99F7B887423FB4B9C961C3883C0A", "name": "Main project", "_lastChange": 1279888110370 } ] } * `POST` requests with as its body a JSON array of new/updated objects. Every object needs to have at least an `id` property. Example, posting to: /taskChanges with body: [{"id":"BDDF85807155497490C12D6DA3A833F1", "name":"Locally created project"}] The server is supposed to persist these changes (if valid). Internally the items must be assigned a `_lastChange` timestamp `TS`. If OK, the server will return a JSON object with "ok" as `status` and `TS` as `now`. _Note:_ it is important that the timestamp of all items and the one returned are the same. {"status": "ok", "now": 1279888110797} Server-side filtering ------------------- In certain circumstances, it is not necessary or desired to push all records down to a client. A standard GET URI looks like this: app.get('/taskupdates', function(req, res) { persistenceSync.pushUpdates(req.conn, req.tx, Task, req.query.since, function(updates){ res.send(updates); }); }); The third parameter in `pushUpdates` is the Entity model. If you wish to filter, simply pass a Query Collection in its place. app.get('/taskupdates', function(req, res) { var taskCollection = Task.all(req.conn).filter('done','=',false); persistenceSync.pushUpdates(req.conn, req.tx, taskCollection, req.query.since, function(updates){ res.send(updates); }); }); Limitations ----------- * This synchronization library synchronizes on a per-object granularity. It does not keep exact changes on a per-property basis, therefore conflicts may be introduced that need to be resolved. * It does not synchronize many-to-many relationships at this point * There may still be many bugs, I'm not sure. ================================================ FILE: index.js ================================================ module.exports = require('./lib/'); ================================================ FILE: lib/index.js ================================================ module.exports = require('./persistence').persistence; module.exports.StoreConfig = require('./persistence.store.config'); ================================================ FILE: lib/persistence.jquery.js ================================================ /** * Copyright (c) 2010 Roberto Saccon * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ if (!window.jQuery) { throw new Error("jQuery should be loaded before persistence.jquery.js"); } if (!window.persistence) { throw new Error("persistence.js should be loaded before persistence.jquery.js"); } persistence.jquery = {}; /** * crossbrowser implementation for entity-property */ persistence.defineProp = function(scope, field, setterCallback, getterCallback) { scope[field] = function(value) { if (value === undefined) { return getterCallback(); } else { setterCallback(value); return scope; } }; }; /** * crossbrowser implementation for entity-property setter */ persistence.set = function(scope, fieldName, value) { if (persistence.isImmutable(fieldName)) throw new Error("immutable field: "+fieldName); scope[fieldName](value); return scope; }; /** * crossbrowser implementation for entity-property getter */ persistence.get = function(arg1, arg2) { var val = (arguments.length == 1) ? arg1 : arg1[arg2]; return (typeof val === "function") ? val() : val; }; (function($){ var originalDataMethod = $.fn.data; $.fn.data = function(name, data) { if (this[0] && this[0]._session && (this[0]._session === window.persistence)) { if (data) { this[0][name](data); return this; } else { return this[0][name](); } } else { return originalDataMethod.apply(this, arguments); } }; if (persistence.sync) { persistence.sync.getJSON = function(url, success) { $.getJSON(url, null, success); }; persistence.sync.postJSON = function(url, data, success) { $.ajax({ url: url, type: 'POST', data: data, dataType: 'json', success: function(response) { success(JSON.parse(response)); } }); }; } })(jQuery); ================================================ FILE: lib/persistence.jquery.mobile.js ================================================ /** * Copyright (c) 2010 Roberto Saccon * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ if (!window.persistence.jquery) { throw new Error("persistence.jquery.js should be loaded before persistence.jquery.mobile.js"); } persistence.jquery.mobile = {}; (function($){ var $pjqm = persistence.jquery.mobile; if (window.openDatabase) { $pjqm.pageEntityName = "Page"; $pjqm.imageEntityName = "Image"; $pjqm.pathField = "path"; $pjqm.dataField = "data"; var originalAjaxMethod = $.ajax; function expand(docPath, srcPath) { var basePath = (/\/$/.test(location.pathname) || (location.pathname == "")) ? location.pathname : location.pathname.substring(0, location.pathname.lastIndexOf("/")); if (/^\.\.\//.test(srcPath)) { // relative path with upward directory traversal var count = 1, splits = docPath.split("/"); while (/^\.\.\//.test(srcPath)) { srcPath = srcPath.substring(3); count++; } return basePath + ((count >= splits.length) ? srcPath : splits.slice(0, splits.length-count).join("/") + "/" + srcPath); } else if (/^\//.test(srcPath)) { // absolute path return srcPath; } else { // relative path without directory traversal return basePath + docPath + "/" + srcPath; } } function base64Image(img, type) { var canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; // Copy the image contents to the canvas var ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0); return canvas.toDataURL("image/" + type); } // parseUri 1.2.2 // (c) Steven Levithan // MIT License var parseUriOptions = { strictMode: false, key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"], q: { name: "queryKey", parser: /(?:^|&)([^&=]*)=?([^&]*)/g }, parser: { strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/, loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ } }; function parseUri (str) { var o = parseUriOptions, m = o.parser[o.strictMode ? "strict" : "loose"].exec(str), uri = {}, i = 14; while (i--) uri[o.key[i]] = m[i] || ""; uri[o.q.name] = {}; uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) { if ($1) uri[o.q.name][$1] = $2; }); return uri; } function getImageType(parsedUri) { if (parsedUri.queryKey.type) { return parsedUri.queryKey.type; } else { return (/\.png$/i.test(parsedUri.path)) ? "png" : "jpeg"; } } $.ajax = function(settings) { var parsedUrl = parseUri(settings.url); var entities = {}, urlPathSegments = parsedUrl.path.split("/"); if ((settings.type == "post") && (urlPathSegments.length > 1)) { var entityName = (urlPathSegments[1].charAt(0).toUpperCase() + urlPathSegments[1].substring(1)); if (persistence.isDefined(entityName)) { var Form = persistence.define(entityName); var persistFormData = function() { var obj = {}; settings.data.replace(/(?:^|&)([^&=]*)=?([^&]*)/g, function ( $0, $1, $2 ) { if ($1) { obj[$1] = $2; } }); var entity = new Form(obj); persistence.add(entity); persistence.flush(); }; if (!navigator.hasOwnProperty("onLine") || navigator.onLine) { originalAjaxMethod({ url: settings.url, success: function(data) { settings.success(data); persistFormData(); }, error: settings.error }); } else { persistFormData(); } } else { originalAjaxMethod(settings); } } else if (persistence.urlExcludeRx && persistence.urlExcludeRx.test(parsedUrl.path)) { originalAjaxMethod(settings); } else { if (persistence.isDefined($pjqm.pageEntityName)) { var Page = persistence.define($pjqm.pageEntityName); Page.findBy($pjqm.pathField, settings.url, function(page) { if (page) { // // load page and images from persistencejs // if (settings.success) { var pos = 0, countOuter = 0, countInner = 0; var inStr = page[$pjqm.dataField](), outStr = ""; var regExp = /(]+src\s*=\s*[\'\"])([^\'\"]+)([\'\"][^>]*>)/ig; var replaced = inStr.replace(regExp, function($0, $1, $2, $3, offset) { countOuter++; if (persistence.isDefined($pjqm.imageEntityName)) { var Img = persistence.define($pjqm.imageEntityName); Img.findBy($pjqm.pathField, expand(settings.url, $2), function(image){ countInner++; if (image) { var imgTagStr = $1 + image[$pjqm.dataField]() + $3; outStr += inStr.substring(pos, offset) + imgTagStr; pos = offset + imgTagStr.length; } else { outStr += inStr.substring(pos, offset) + imgTagStr; pos = offset; } if (countInner == countOuter) { settings.success(outStr); } return ""; }); } else { outStr += inStr.substring(pos, offset) + imgTagStr; pos = offset; } }); if (replaced == inStr) { settings.success(inStr); } else if (!persistence.isDefined($pjqm.imageEntityName)) { settings.success(outStr); }; } } else { // // ajax-load page and persist page and images // originalAjaxMethod({ url: settings.url, success: function(data) { settings.success(data); if (persistence.isDefined($pjqm.pageEntityName)) { var entities = [], crawlImages = false; var Page = persistence.define($pjqm.pageEntityName); if (persistence.isDefined($pjqm.imageEntityName)) { var Img = persistence.define($pjqm.imageEntityName), count = 0; $("#"+settings.url.replace(/\//g,"\\/").replace(/\./g,"\\.")+" img").each(function(i, img){ crawlImages = true; count++; $(img).load(function() { var obj = {}, parsedImgSrc = parseUri(img.src); obj[$pjqm.pathField] = parsedImgSrc.path; obj[$pjqm.dataField] = base64Image(img, getImageType(parsedImgSrc)); entities.push(new Img(obj)); if (crawlImages && (--count == 0)) { for (var j=0; j * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ if (typeof exports !== 'undefined') { exports.createPersistence = function() { return initPersistence({}) } var singleton; if (typeof (exports.__defineGetter__) === 'function') { exports.__defineGetter__("persistence", function () { if (!singleton) singleton = exports.createPersistence(); return singleton; }); } else { Object.defineProperty(exports, "persistence", { get: function () { if (!singleton) singleton = exports.createPersistence(); return singleton; }, enumerable: true, configurable: true }); } } else { window = window || {}; window.persistence = initPersistence(window.persistence || {}); } function initPersistence(persistence) { if (persistence.isImmutable) // already initialized return persistence; /** * Check for immutable fields */ persistence.isImmutable = function(fieldName) { return (fieldName == "id"); }; /** * Default implementation for entity-property */ persistence.defineProp = function(scope, field, setterCallback, getterCallback) { if (typeof (scope.__defineSetter__) === 'function' && typeof (scope.__defineGetter__) === 'function') { scope.__defineSetter__(field, function (value) { setterCallback(value); }); scope.__defineGetter__(field, function () { return getterCallback(); }); } else { Object.defineProperty(scope, field, { get: getterCallback, set: function (value) { setterCallback(value); }, enumerable: true, configurable: true }); } }; /** * Default implementation for entity-property setter */ persistence.set = function(scope, fieldName, value) { if (persistence.isImmutable(fieldName)) throw new Error("immutable field: "+fieldName); scope[fieldName] = value; }; /** * Default implementation for entity-property getter */ persistence.get = function(arg1, arg2) { return (arguments.length == 1) ? arg1 : arg1[arg2]; }; (function () { var entityMeta = {}; var entityClassCache = {}; persistence.getEntityMeta = function() { return entityMeta; } // Per-session data persistence.trackedObjects = {}; persistence.objectsToRemove = {}; persistence.objectsRemoved = []; // {id: ..., type: ...} persistence.globalPropertyListeners = {}; // EntityType__prop -> QueryColleciton obj persistence.queryCollectionCache = {}; // entityName -> uniqueString -> QueryCollection persistence.getObjectsToRemove = function() { return this.objectsToRemove; }; persistence.getTrackedObjects = function() { return this.trackedObjects; }; // Public Extension hooks persistence.entityDecoratorHooks = []; persistence.flushHooks = []; persistence.schemaSyncHooks = []; // Enable debugging (display queries using console.log etc) persistence.debug = true; persistence.subscribeToGlobalPropertyListener = function(coll, entityName, property) { var key = entityName + '__' + property; if(key in this.globalPropertyListeners) { var listeners = this.globalPropertyListeners[key]; for(var i = 0; i < listeners.length; i++) { if(listeners[i] === coll) { return; } } this.globalPropertyListeners[key].push(coll); } else { this.globalPropertyListeners[key] = [coll]; } } persistence.unsubscribeFromGlobalPropertyListener = function(coll, entityName, property) { var key = entityName + '__' + property; var listeners = this.globalPropertyListeners[key]; for(var i = 0; i < listeners.length; i++) { if(listeners[i] === coll) { listeners.splice(i, 1); return; } } } persistence.propertyChanged = function(obj, property, oldValue, newValue) { if(!this.trackedObjects[obj.id]) return; // not yet added, ignore for now var entityName = obj._type; var key = entityName + '__' + property; if(key in this.globalPropertyListeners) { var listeners = this.globalPropertyListeners[key]; for(var i = 0; i < listeners.length; i++) { var coll = listeners[i]; var dummyObj = obj._data; dummyObj[property] = oldValue; var matchedBefore = coll._filter.match(dummyObj); dummyObj[property] = newValue; var matchedAfter = coll._filter.match(dummyObj); if(matchedBefore != matchedAfter) { coll.triggerEvent('change', coll, obj); } } } } persistence.objectRemoved = function(obj) { var entityName = obj._type; if(this.queryCollectionCache[entityName]) { var colls = this.queryCollectionCache[entityName]; for(var key in colls) { if(colls.hasOwnProperty(key)) { var coll = colls[key]; if(coll._filter.match(obj)) { // matched the filter -> was part of collection coll.triggerEvent('change', coll, obj); } } } } } /** * Retrieves metadata about entity, mostly for internal use */ function getMeta(entityName) { return entityMeta[entityName]; } persistence.getMeta = getMeta; /** * A database session */ function Session(conn) { this.trackedObjects = {}; this.objectsToRemove = {}; this.objectsRemoved = []; this.globalPropertyListeners = {}; // EntityType__prop -> QueryColleciton obj this.queryCollectionCache = {}; // entityName -> uniqueString -> QueryCollection this.conn = conn; } Session.prototype = persistence; // Inherit everything from the root persistence object persistence.Session = Session; /** * Define an entity * * @param entityName * the name of the entity (also the table name in the database) * @param fields * an object with property names as keys and SQLite types as * values, e.g. {name: "TEXT", age: "INT"} * @return the entity's constructor */ persistence.define = function (entityName, fields) { if (entityMeta[entityName]) { // Already defined, ignore return getEntity(entityName); } var meta = { name: entityName, fields: fields, isMixin: false, indexes: [], hasMany: {}, hasOne: {} }; entityMeta[entityName] = meta; return getEntity(entityName); }; /** * Checks whether an entity exists * * @param entityName * the name of the entity (also the table name in the database) * @return `true` if the entity exists, otherwise `false` */ persistence.isDefined = function (entityName) { return !!entityMeta[entityName]; } /** * Define a mixin * * @param mixinName * the name of the mixin * @param fields * an object with property names as keys and SQLite types as * values, e.g. {name: "TEXT", age: "INT"} * @return the entity's constructor */ persistence.defineMixin = function (mixinName, fields) { var Entity = this.define(mixinName, fields); Entity.meta.isMixin = true; return Entity; }; persistence.isTransaction = function(obj) { return !obj || (obj && obj.executeSql); }; persistence.isSession = function(obj) { return !obj || (obj && obj.schemaSync); }; /** * Adds the object to tracked entities to be persisted * * @param obj * the object to be tracked */ persistence.add = function (obj) { if(!obj) return; if (!this.trackedObjects[obj.id]) { this.trackedObjects[obj.id] = obj; if(obj._new) { for(var p in obj._data) { if(obj._data.hasOwnProperty(p)) { this.propertyChanged(obj, p, undefined, obj._data[p]); } } } } return this; }; /** * Marks the object to be removed (on next flush) * @param obj object to be removed */ persistence.remove = function(obj) { if (obj._new) { delete this.trackedObjects[obj.id]; } else { if (!this.objectsToRemove[obj.id]) { this.objectsToRemove[obj.id] = obj; } this.objectsRemoved.push({id: obj.id, entity: obj._type}); } this.objectRemoved(obj); return this; }; /** * Clean the persistence context of cached entities and such. */ persistence.clean = function () { this.trackedObjects = {}; this.objectsToRemove = {}; this.objectsRemoved = []; this.globalPropertyListeners = {}; this.queryCollectionCache = {}; }; /** * asynchronous sequential version of Array.prototype.forEach * @param array the array to iterate over * @param fn the function to apply to each item in the array, function * has two argument, the first is the item value, the second a * callback function * @param callback the function to call when the forEach has ended */ persistence.asyncForEach = function(array, fn, callback) { array = array.slice(0); // Just to be sure function processOne() { var item = array.pop(); fn(item, function(result, err) { if(array.length > 0) { processOne(); } else { callback(result, err); } }); } if(array.length > 0) { processOne(); } else { callback(); } }; /** * asynchronous parallel version of Array.prototype.forEach * @param array the array to iterate over * @param fn the function to apply to each item in the array, function * has two argument, the first is the item value, the second a * callback function * @param callback the function to call when the forEach has ended */ persistence.asyncParForEach = function(array, fn, callback) { var completed = 0; var arLength = array.length; if(arLength === 0) { callback(); } for(var i = 0; i < arLength; i++) { fn(array[i], function(result, err) { completed++; if(completed === arLength) { callback(result, err); } }); } }; /** * Retrieves or creates an entity constructor function for a given * entity name * @return the entity constructor function to be invoked with `new fn()` */ function getEntity(entityName) { if (entityClassCache[entityName]) { return entityClassCache[entityName]; } var meta = entityMeta[entityName]; /** * @constructor */ function Entity (session, obj, noEvents) { var args = argspec.getArgs(arguments, [ { name: "session", optional: true, check: persistence.isSession, defaultValue: persistence }, { name: "obj", optional: true, check: function(obj) { return obj; }, defaultValue: {} } ]); if (meta.isMixin) throw new Error("Cannot instantiate mixin"); session = args.session; obj = args.obj; var that = this; this.id = obj.id || persistence.createUUID(); this._new = true; this._type = entityName; this._dirtyProperties = {}; this._data = {}; this._data_obj = {}; // references to objects this._session = session || persistence; this.subscribers = {}; // observable for ( var field in meta.fields) { (function () { if (meta.fields.hasOwnProperty(field)) { var f = field; // Javascript scopes/closures SUCK persistence.defineProp(that, f, function(val) { // setterCallback var oldValue = that._data[f]; if(oldValue !== val || (oldValue && val && oldValue.getTime && val.getTime)) { // Don't mark properties as dirty and trigger events unnecessarily that._data[f] = val; that._dirtyProperties[f] = oldValue; that.triggerEvent('set', that, f, val); that.triggerEvent('change', that, f, val); session.propertyChanged(that, f, oldValue, val); } }, function() { // getterCallback return that._data[f]; }); that._data[field] = defaultValue(meta.fields[field]); } }()); } for ( var it in meta.hasOne) { if (meta.hasOne.hasOwnProperty(it)) { (function () { var ref = it; var mixinClass = meta.hasOne[it].type.meta.isMixin ? ref + '_class' : null; persistence.defineProp(that, ref, function(val) { // setterCallback var oldValue = that._data[ref]; var oldValueObj = that._data_obj[ref] || session.trackedObjects[that._data[ref]]; if (val == null) { that._data[ref] = null; that._data_obj[ref] = undefined; if (mixinClass) that[mixinClass] = ''; } else if (val.id) { that._data[ref] = val.id; that._data_obj[ref] = val; if (mixinClass) that[mixinClass] = val._type; session.add(val); session.add(that); } else { // let's assume it's an id that._data[ref] = val; } that._dirtyProperties[ref] = oldValue; that.triggerEvent('set', that, ref, val); that.triggerEvent('change', that, ref, val); // Inverse if(meta.hasOne[ref].inverseProperty) { var newVal = that[ref]; if(newVal) { var inverse = newVal[meta.hasOne[ref].inverseProperty]; if(inverse.list && inverse._filter) { inverse.triggerEvent('change', that, ref, val); } } if(oldValueObj) { var inverse = oldValueObj[meta.hasOne[ref].inverseProperty]; if(inverse.list && inverse._filter) { inverse.triggerEvent('change', that, ref, val); } } } }, function() { // getterCallback if (!that._data[ref]) { return null; } else if(that._data_obj[ref] !== undefined) { return that._data_obj[ref]; } else if(that._data[ref] && session.trackedObjects[that._data[ref]]) { that._data_obj[ref] = session.trackedObjects[that._data[ref]]; return that._data_obj[ref]; } else { throw new Error("Property '" + ref + "' of '" + meta.name + "' with id: " + that._data[ref] + " not fetched, either prefetch it or fetch it manually."); } }); }()); } } for ( var it in meta.hasMany) { if (meta.hasMany.hasOwnProperty(it)) { (function () { var coll = it; if (meta.hasMany[coll].manyToMany) { persistence.defineProp(that, coll, function(val) { // setterCallback if(val && val._items) { // Local query collection, just add each item // TODO: this is technically not correct, should clear out existing items too var items = val._items; for(var i = 0; i < items.length; i++) { persistence.get(that, coll).add(items[i]); } } else { throw new Error("Not yet supported."); } }, function() { // getterCallback if (that._data[coll]) { return that._data[coll]; } else { var rel = meta.hasMany[coll]; var inverseMeta = rel.type.meta; var inv = inverseMeta.hasMany[rel.inverseProperty]; var direct = rel.mixin ? rel.mixin.meta.name : meta.name; var inverse = inv.mixin ? inv.mixin.meta.name : inverseMeta.name; var queryColl = new persistence.ManyToManyDbQueryCollection(session, inverseMeta.name); queryColl.initManyToMany(that, coll); queryColl._manyToManyFetch = { table: rel.tableName, prop: direct + '_' + coll, inverseProp: inverse + '_' + rel.inverseProperty, id: that.id }; that._data[coll] = queryColl; return session.uniqueQueryCollection(queryColl); } }); } else { // one to many persistence.defineProp(that, coll, function(val) { // setterCallback if(val && val._items) { // Local query collection, just add each item // TODO: this is technically not correct, should clear out existing items too var items = val._items; for(var i = 0; i < items.length; i++) { persistence.get(that, coll).add(items[i]); } } else { throw new Error("Not yet supported."); } }, function() { // getterCallback if (that._data[coll]) { return that._data[coll]; } else { var queryColl = session.uniqueQueryCollection(new persistence.DbQueryCollection(session, meta.hasMany[coll].type.meta.name).filter(meta.hasMany[coll].inverseProperty, '=', that)); that._data[coll] = queryColl; return queryColl; } }); } }()); } } if(this.initialize) { this.initialize(); } for ( var f in obj) { if (obj.hasOwnProperty(f)) { if(f !== 'id') { persistence.set(that, f, obj[f]); } } } } // Entity Entity.prototype = new Observable(); Entity.meta = meta; Entity.prototype.equals = function(other) { return this.id == other.id; }; Entity.prototype.toJSON = function() { var json = {id: this.id}; for(var p in this._data) { if(this._data.hasOwnProperty(p)) { if (typeof this._data[p] == "object" && this._data[p] != null) { if (this._data[p].toJSON != undefined) { json[p] = this._data[p].toJSON(); } } else { json[p] = this._data[p]; } } } return json; }; /** * Select a subset of data as a JSON structure (Javascript object) * * A property specification is passed that selects the * properties to be part of the resulting JSON object. Examples: * ['id', 'name'] -> Will return an object with the id and name property of this entity * ['*'] -> Will return an object with all the properties of this entity, not recursive * ['project.name'] -> will return an object with a project property which has a name * property containing the project name (hasOne relationship) * ['project.[id, name]'] -> will return an object with a project property which has an * id and name property containing the project name * (hasOne relationship) * ['tags.name'] -> will return an object with an array `tags` property containing * objects each with a single property: name * * @param tx database transaction to use, leave out to start a new one * @param props a property specification * @param callback(result) */ Entity.prototype.selectJSON = function(tx, props, callback) { var that = this; var args = argspec.getArgs(arguments, [ { name: "tx", optional: true, check: persistence.isTransaction, defaultValue: null }, { name: "props", optional: false }, { name: "callback", optional: false } ]); tx = args.tx; props = args.props; callback = args.callback; if(!tx) { this._session.transaction(function(tx) { that.selectJSON(tx, props, callback); }); return; } var includeProperties = {}; props.forEach(function(prop) { var current = includeProperties; var parts = prop.split('.'); for(var i = 0; i < parts.length; i++) { var part = parts[i]; if(i === parts.length-1) { if(part === '*') { current.id = true; for(var p in meta.fields) { if(meta.fields.hasOwnProperty(p)) { current[p] = true; } } for(var p in meta.hasOne) { if(meta.hasOne.hasOwnProperty(p)) { current[p] = true; } } for(var p in meta.hasMany) { if(meta.hasMany.hasOwnProperty(p)) { current[p] = true; } } } else if(part[0] === '[') { part = part.substring(1, part.length-1); var propList = part.split(/,\s*/); propList.forEach(function(prop) { current[prop] = true; }); } else { current[part] = true; } } else { current[part] = current[part] || {}; current = current[part]; } } }); buildJSON(this, tx, includeProperties, callback); }; function buildJSON(that, tx, includeProperties, callback) { var session = that._session; var properties = []; var meta = getMeta(that._type); var fieldSpec = meta.fields; for(var p in includeProperties) { if(includeProperties.hasOwnProperty(p)) { properties.push(p); } } var cheapProperties = []; var expensiveProperties = []; properties.forEach(function(p) { if(includeProperties[p] === true && !meta.hasMany[p]) { // simple, loaded field cheapProperties.push(p); } else { expensiveProperties.push(p); } }); var itemData = that._data; var item = {}; cheapProperties.forEach(function(p) { if(p === 'id') { item.id = that.id; } else if(meta.hasOne[p]) { item[p] = itemData[p] ? {id: itemData[p]} : null; } else { item[p] = persistence.entityValToJson(itemData[p], fieldSpec[p]); } }); properties = expensiveProperties.slice(); persistence.asyncForEach(properties, function(p, callback) { if(meta.hasOne[p]) { that.fetch(tx, p, function(obj) { if(obj) { buildJSON(obj, tx, includeProperties[p], function(result) { item[p] = result; callback(); }); } else { item[p] = null; callback(); } }); } else if(meta.hasMany[p]) { persistence.get(that, p).list(function(objs) { item[p] = []; persistence.asyncForEach(objs, function(obj, callback) { var obj = objs.pop(); if(includeProperties[p] === true) { item[p].push({id: obj.id}); callback(); } else { buildJSON(obj, tx, includeProperties[p], function(result) { item[p].push(result); callback(); }); } }, callback); }); } }, function() { callback(item); }); }; // End of buildJson Entity.prototype.fetch = function(tx, rel, callback) { var args = argspec.getArgs(arguments, [ { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, { name: 'rel', optional: false, check: argspec.hasType('string') }, { name: 'callback', optional: false, check: argspec.isCallback() } ]); tx = args.tx; rel = args.rel; callback = args.callback; var that = this; var session = this._session; if(!tx) { session.transaction(function(tx) { that.fetch(tx, rel, callback); }); return; } if(!this._data[rel]) { // null if(callback) { callback(null); } } else if(this._data_obj[rel]) { // already loaded if(callback) { callback(this._data_obj[rel]); } } else { var type = meta.hasOne[rel].type; if (type.meta.isMixin) { type = getEntity(this._data[rel + '_class']); } type.load(session, tx, this._data[rel], function(obj) { that._data_obj[rel] = obj; if(callback) { callback(obj); } }); } }; /** * Currently this is only required when changing JSON properties */ Entity.prototype.markDirty = function(prop) { this._dirtyProperties[prop] = true; }; /** * Returns a QueryCollection implementation matching all instances * of this entity in the database */ Entity.all = function(session) { var args = argspec.getArgs(arguments, [ { name: 'session', optional: true, check: persistence.isSession, defaultValue: persistence } ]); session = args.session; return session.uniqueQueryCollection(new AllDbQueryCollection(session, entityName)); }; Entity.fromSelectJSON = function(session, tx, jsonObj, callback) { var args = argspec.getArgs(arguments, [ { name: 'session', optional: true, check: persistence.isSession, defaultValue: persistence }, { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, { name: 'jsonObj', optional: false }, { name: 'callback', optional: false, check: argspec.isCallback() } ]); session = args.session; tx = args.tx; jsonObj = args.jsonObj; callback = args.callback; if(!tx) { session.transaction(function(tx) { Entity.fromSelectJSON(session, tx, jsonObj, callback); }); return; } if(typeof jsonObj === 'string') { jsonObj = JSON.parse(jsonObj); } if(!jsonObj) { callback(null); return; } function loadedObj(obj) { if(!obj) { obj = new Entity(session); if(jsonObj.id) { obj.id = jsonObj.id; } } session.add(obj); var expensiveProperties = []; for(var p in jsonObj) { if(jsonObj.hasOwnProperty(p)) { if(p === 'id') { continue; } else if(meta.fields[p]) { // regular field persistence.set(obj, p, persistence.jsonToEntityVal(jsonObj[p], meta.fields[p])); } else if(meta.hasOne[p] || meta.hasMany[p]){ expensiveProperties.push(p); } } } persistence.asyncForEach(expensiveProperties, function(p, callback) { if(meta.hasOne[p]) { meta.hasOne[p].type.fromSelectJSON(session, tx, jsonObj[p], function(result) { persistence.set(obj, p, result); callback(); }); } else if(meta.hasMany[p]) { var coll = persistence.get(obj, p); var ar = jsonObj[p].slice(0); var PropertyEntity = meta.hasMany[p].type; // get all current items coll.list(tx, function(currentItems) { persistence.asyncForEach(ar, function(item, callback) { PropertyEntity.fromSelectJSON(session, tx, item, function(result) { // Check if not already in collection for(var i = 0; i < currentItems.length; i++) { if(currentItems[i].id === result.id) { callback(); return; } } coll.add(result); callback(); }); }, function() { callback(); }); }); } }, function() { callback(obj); }); } if(jsonObj.id) { Entity.load(session, tx, jsonObj.id, loadedObj); } else { loadedObj(new Entity(session)); } }; Entity.load = function(session, tx, id, callback) { var args = argspec.getArgs(arguments, [ { name: 'session', optional: true, check: persistence.isSession, defaultValue: persistence }, { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, { name: 'id', optional: false, check: argspec.hasType('string') }, { name: 'callback', optional: true, check: argspec.isCallback(), defaultValue: function(){} } ]); Entity.findBy(args.session, args.tx, "id", args.id, args.callback); }; Entity.findBy = function(session, tx, property, value, callback) { var args = argspec.getArgs(arguments, [ { name: 'session', optional: true, check: persistence.isSession, defaultValue: persistence }, { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, { name: 'property', optional: false, check: argspec.hasType('string') }, { name: 'value', optional: false }, { name: 'callback', optional: true, check: argspec.isCallback(), defaultValue: function(){} } ]); session = args.session; tx = args.tx; property = args.property; value = args.value; callback = args.callback; if(property === 'id' && value in session.trackedObjects) { callback(session.trackedObjects[value]); return; } if(!tx) { session.transaction(function(tx) { Entity.findBy(session, tx, property, value, callback); }); return; } Entity.all(session).filter(property, "=", value).one(tx, function(obj) { callback(obj); }); } Entity.index = function(cols,options) { var opts = options || {}; if (typeof cols=="string") { cols = [cols]; } opts.columns = cols; meta.indexes.push(opts); }; /** * Declares a one-to-many or many-to-many relationship to another entity * Whether 1:N or N:M is chosed depends on the inverse declaration * @param collName the name of the collection (becomes a property of * Entity instances * @param otherEntity the constructor function of the entity to define * the relation to * @param inverseRel the name of the inverse property (to be) defined on otherEntity */ Entity.hasMany = function (collName, otherEntity, invRel) { var otherMeta = otherEntity.meta; if (otherMeta.hasMany[invRel]) { // other side has declared it as a one-to-many relation too -> it's in // fact many-to-many var tableName = meta.name + "_" + collName + "_" + otherMeta.name; var inverseTableName = otherMeta.name + '_' + invRel + '_' + meta.name; if (tableName > inverseTableName) { // Some arbitrary way to deterministically decide which table to generate tableName = inverseTableName; } meta.hasMany[collName] = { type: otherEntity, inverseProperty: invRel, manyToMany: true, tableName: tableName }; otherMeta.hasMany[invRel] = { type: Entity, inverseProperty: collName, manyToMany: true, tableName: tableName }; delete meta.hasOne[collName]; delete meta.fields[collName + "_class"]; // in case it existed } else { meta.hasMany[collName] = { type: otherEntity, inverseProperty: invRel }; otherMeta.hasOne[invRel] = { type: Entity, inverseProperty: collName }; if (meta.isMixin) otherMeta.fields[invRel + "_class"] = persistence.typeMapper ? persistence.typeMapper.classNameType : "TEXT"; } } Entity.hasOne = function (refName, otherEntity, inverseProperty) { meta.hasOne[refName] = { type: otherEntity, inverseProperty: inverseProperty }; if (otherEntity.meta.isMixin) meta.fields[refName + "_class"] = persistence.typeMapper ? persistence.typeMapper.classNameType : "TEXT"; }; Entity.is = function(mixin){ var mixinMeta = mixin.meta; if (!mixinMeta.isMixin) throw new Error("not a mixin: " + mixin); mixin.meta.mixedIns = mixin.meta.mixedIns || []; mixin.meta.mixedIns.push(meta); for (var field in mixinMeta.fields) { if (mixinMeta.fields.hasOwnProperty(field)) meta.fields[field] = mixinMeta.fields[field]; } for (var it in mixinMeta.hasOne) { if (mixinMeta.hasOne.hasOwnProperty(it)) meta.hasOne[it] = mixinMeta.hasOne[it]; } for (var it in mixinMeta.hasMany) { if (mixinMeta.hasMany.hasOwnProperty(it)) { mixinMeta.hasMany[it].mixin = mixin; meta.hasMany[it] = mixinMeta.hasMany[it]; } } } // Allow decorator functions to add more stuff var fns = persistence.entityDecoratorHooks; for(var i = 0; i < fns.length; i++) { fns[i](Entity); } entityClassCache[entityName] = Entity; return Entity; } persistence.jsonToEntityVal = function(value, type) { if(type) { switch(type) { case 'DATE': if(typeof value === 'number') { if (value > 1000000000000) { // it's in milliseconds return new Date(value); } else { return new Date(value * 1000); } } else { return null; } break; default: return value; } } else { return value; } }; persistence.entityValToJson = function(value, type) { if(type) { switch(type) { case 'DATE': if(value) { value = new Date(value); return Math.round(value.getTime() / 1000); } else { return null; } break; default: return value; } } else { return value; } }; /** * Dumps the entire database into an object (that can be serialized to JSON for instance) * @param tx transaction to use, use `null` to start a new one * @param entities a list of entity constructor functions to serialize, use `null` for all * @param callback (object) the callback function called with the results. */ persistence.dump = function(tx, entities, callback) { var args = argspec.getArgs(arguments, [ { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, { name: 'entities', optional: true, check: function(obj) { return !obj || (obj && obj.length && !obj.apply); }, defaultValue: null }, { name: 'callback', optional: false, check: argspec.isCallback(), defaultValue: function(){} } ]); tx = args.tx; entities = args.entities; callback = args.callback; if(!entities) { // Default: all entity types entities = []; for(var e in entityClassCache) { if(entityClassCache.hasOwnProperty(e)) { entities.push(entityClassCache[e]); } } } var result = {}; persistence.asyncParForEach(entities, function(Entity, callback) { Entity.all().list(tx, function(all) { var items = []; persistence.asyncParForEach(all, function(e, callback) { var rec = {}; var fields = Entity.meta.fields; for(var f in fields) { if(fields.hasOwnProperty(f)) { rec[f] = persistence.entityValToJson(e._data[f], fields[f]); } } var refs = Entity.meta.hasOne; for(var r in refs) { if(refs.hasOwnProperty(r)) { rec[r] = e._data[r]; } } var colls = Entity.meta.hasMany; var collArray = []; for(var coll in colls) { if(colls.hasOwnProperty(coll)) { collArray.push(coll); } } persistence.asyncParForEach(collArray, function(collP, callback) { var coll = persistence.get(e, collP); coll.list(tx, function(results) { rec[collP] = results.map(function(r) { return r.id; }); callback(); }); }, function() { rec.id = e.id; items.push(rec); callback(); }); }, function() { result[Entity.meta.name] = items; callback(); }); }); }, function() { callback(result); }); }; /** * Loads a set of entities from a dump object * @param tx transaction to use, use `null` to start a new one * @param dump the dump object * @param callback the callback function called when done. */ persistence.load = function(tx, dump, callback) { var args = argspec.getArgs(arguments, [ { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, { name: 'dump', optional: false }, { name: 'callback', optional: true, check: argspec.isCallback(), defaultValue: function(){} } ]); tx = args.tx; dump = args.dump; callback = args.callback; var finishedCount = 0; var collItemsToAdd = []; var session = this; for(var entityName in dump) { if(dump.hasOwnProperty(entityName)) { var Entity = getEntity(entityName); var fields = Entity.meta.fields; var instances = dump[entityName]; for(var i = 0; i < instances.length; i++) { var instance = instances[i]; var ent = new Entity(); ent.id = instance.id; for(var p in instance) { if(instance.hasOwnProperty(p)) { if (persistence.isImmutable(p)) { ent[p] = instance[p]; } else if(Entity.meta.hasMany[p]) { // collection var many = Entity.meta.hasMany[p]; if(many.manyToMany && Entity.meta.name < many.type.meta.name) { // Arbitrary way to avoid double adding continue; } var coll = persistence.get(ent, p); if(instance[p].length > 0) { instance[p].forEach(function(it) { collItemsToAdd.push({Entity: Entity, coll: coll, id: it}); }); } } else { persistence.set(ent, p, persistence.jsonToEntityVal(instance[p], fields[p])); } } } this.add(ent); } } } session.flush(tx, function() { persistence.asyncForEach(collItemsToAdd, function(collItem, callback) { collItem.Entity.load(session, tx, collItem.id, function(obj) { collItem.coll.add(obj); callback(); }); }, function() { session.flush(tx, callback); }); }); }; /** * Dumps the entire database to a JSON string * @param tx transaction to use, use `null` to start a new one * @param entities a list of entity constructor functions to serialize, use `null` for all * @param callback (jsonDump) the callback function called with the results. */ persistence.dumpToJson = function(tx, entities, callback) { var args = argspec.getArgs(arguments, [ { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, { name: 'entities', optional: true, check: function(obj) { return obj && obj.length && !obj.apply; }, defaultValue: null }, { name: 'callback', optional: false, check: argspec.isCallback(), defaultValue: function(){} } ]); tx = args.tx; entities = args.entities; callback = args.callback; this.dump(tx, entities, function(obj) { callback(JSON.stringify(obj)); }); }; /** * Loads data from a JSON string (as dumped by `dumpToJson`) * @param tx transaction to use, use `null` to start a new one * @param jsonDump JSON string * @param callback the callback function called when done. */ persistence.loadFromJson = function(tx, jsonDump, callback) { var args = argspec.getArgs(arguments, [ { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, { name: 'jsonDump', optional: false }, { name: 'callback', optional: true, check: argspec.isCallback(), defaultValue: function(){} } ]); tx = args.tx; jsonDump = args.jsonDump; callback = args.callback; this.load(tx, JSON.parse(jsonDump), callback); }; /** * Generates a UUID according to http://www.ietf.org/rfc/rfc4122.txt */ function createUUID () { if(persistence.typeMapper && persistence.typeMapper.newUuid) { return persistence.typeMapper.newUuid(); } var s = []; var hexDigits = "0123456789ABCDEF"; for ( var i = 0; i < 32; i++) { s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1); } s[12] = "4"; s[16] = hexDigits.substr((s[16] & 0x3) | 0x8, 1); var uuid = s.join(""); return uuid; } persistence.createUUID = createUUID; function defaultValue(type) { if(persistence.typeMapper && persistence.typeMapper.defaultValue) { return persistence.typeMapper.defaultValue(type); } switch(type) { case "TEXT": return ""; case "BOOL": return false; default: if(type.indexOf("INT") !== -1) { return 0; } else if(type.indexOf("CHAR") !== -1) { return ""; } else { return null; } } } function arrayContains(ar, item) { var l = ar.length; for(var i = 0; i < l; i++) { var el = ar[i]; if(el.equals && el.equals(item)) { return true; } else if(el === item) { return true; } } return false; } function arrayRemove(ar, item) { var l = ar.length; for(var i = 0; i < l; i++) { var el = ar[i]; if(el.equals && el.equals(item)) { ar.splice(i, 1); return; } else if(el === item) { ar.splice(i, 1); return; } } } ////////////////// QUERY COLLECTIONS \\\\\\\\\\\\\\\\\\\\\\\ function Subscription(obj, eventType, fn) { this.obj = obj; this.eventType = eventType; this.fn = fn; } Subscription.prototype.unsubscribe = function() { this.obj.removeEventListener(this.eventType, this.fn); }; /** * Simple observable function constructor * @constructor */ function Observable() { this.subscribers = {}; } Observable.prototype.addEventListener = function (eventType, fn) { if (!this.subscribers[eventType]) { this.subscribers[eventType] = []; } this.subscribers[eventType].push(fn); return new Subscription(this, eventType, fn); }; Observable.prototype.removeEventListener = function(eventType, fn) { var subscribers = this.subscribers[eventType]; for ( var i = 0; i < subscribers.length; i++) { if(subscribers[i] == fn) { this.subscribers[eventType].splice(i, 1); return true; } } return false; }; Observable.prototype.triggerEvent = function (eventType) { if (!this.subscribers[eventType]) { // No subscribers to this event type return; } var subscribers = this.subscribers[eventType].slice(0); for(var i = 0; i < subscribers.length; i++) { subscribers[i].apply(null, arguments); } }; /* * Each filter has 4 methods: * - sql(prefix, values) -- returns a SQL representation of this filter, * possibly pushing additional query arguments to `values` if ?'s are used * in the query * - match(o) -- returns whether the filter matches the object o. * - makeFit(o) -- attempts to adapt the object o in such a way that it matches * this filter. * - makeNotFit(o) -- the oppositive of makeFit, makes the object o NOT match * this filter */ /** * Default filter that does not filter on anything * currently it generates a 1=1 SQL query, which is kind of ugly */ function NullFilter () { } NullFilter.prototype.match = function (o) { return true; }; NullFilter.prototype.makeFit = function(o) { }; NullFilter.prototype.makeNotFit = function(o) { }; NullFilter.prototype.toUniqueString = function() { return "NULL"; }; NullFilter.prototype.subscribeGlobally = function() { }; NullFilter.prototype.unsubscribeGlobally = function() { }; /** * Filter that makes sure that both its left and right filter match * @param left left-hand filter object * @param right right-hand filter object */ function AndFilter (left, right) { this.left = left; this.right = right; } AndFilter.prototype.match = function (o) { return this.left.match(o) && this.right.match(o); }; AndFilter.prototype.makeFit = function(o) { this.left.makeFit(o); this.right.makeFit(o); }; AndFilter.prototype.makeNotFit = function(o) { this.left.makeNotFit(o); this.right.makeNotFit(o); }; AndFilter.prototype.toUniqueString = function() { return this.left.toUniqueString() + " AND " + this.right.toUniqueString(); }; AndFilter.prototype.subscribeGlobally = function(coll, entityName) { this.left.subscribeGlobally(coll, entityName); this.right.subscribeGlobally(coll, entityName); }; AndFilter.prototype.unsubscribeGlobally = function(coll, entityName) { this.left.unsubscribeGlobally(coll, entityName); this.right.unsubscribeGlobally(coll, entityName); }; /** * Filter that makes sure that either its left and right filter match * @param left left-hand filter object * @param right right-hand filter object */ function OrFilter (left, right) { this.left = left; this.right = right; } OrFilter.prototype.match = function (o) { return this.left.match(o) || this.right.match(o); }; OrFilter.prototype.makeFit = function(o) { this.left.makeFit(o); this.right.makeFit(o); }; OrFilter.prototype.makeNotFit = function(o) { this.left.makeNotFit(o); this.right.makeNotFit(o); }; OrFilter.prototype.toUniqueString = function() { return this.left.toUniqueString() + " OR " + this.right.toUniqueString(); }; OrFilter.prototype.subscribeGlobally = function(coll, entityName) { this.left.subscribeGlobally(coll, entityName); this.right.subscribeGlobally(coll, entityName); }; OrFilter.prototype.unsubscribeGlobally = function(coll, entityName) { this.left.unsubscribeGlobally(coll, entityName); this.right.unsubscribeGlobally(coll, entityName); }; /** * Filter that checks whether a certain property matches some value, based on an * operator. Supported operators are '=', '!=', '<', '<=', '>' and '>='. * @param property the property name * @param operator the operator to compare with * @param value the literal value to compare to */ function PropertyFilter (property, operator, value) { this.property = property; this.operator = operator.toLowerCase(); this.value = value; } PropertyFilter.prototype.match = function (o) { var value = this.value; var propValue = persistence.get(o, this.property); if(value && value.getTime) { // DATE // TODO: Deal with arrays of dates for 'in' and 'not in' value = Math.round(value.getTime() / 1000) * 1000; // Deal with precision if(propValue && propValue.getTime) { // DATE propValue = Math.round(propValue.getTime() / 1000) * 1000; // Deal with precision } } switch (this.operator) { case '=': return propValue === value; break; case '!=': return propValue !== value; break; case '<': return propValue < value; break; case '<=': return propValue <= value; break; case '>': return propValue > value; break; case '>=': return propValue >= value; break; case 'in': return arrayContains(value, propValue); break; case 'not in': return !arrayContains(value, propValue); break; } }; PropertyFilter.prototype.makeFit = function(o) { if(this.operator === '=') { persistence.set(o, this.property, this.value); } else { throw new Error("Sorry, can't perform makeFit for other filters than ="); } }; PropertyFilter.prototype.makeNotFit = function(o) { if(this.operator === '=') { persistence.set(o, this.property, null); } else { throw new Error("Sorry, can't perform makeNotFit for other filters than ="); } }; PropertyFilter.prototype.subscribeGlobally = function(coll, entityName) { persistence.subscribeToGlobalPropertyListener(coll, entityName, this.property); }; PropertyFilter.prototype.unsubscribeGlobally = function(coll, entityName) { persistence.unsubscribeFromGlobalPropertyListener(coll, entityName, this.property); }; PropertyFilter.prototype.toUniqueString = function() { var val = this.value; if(val && val._type) { val = val.id; } return this.property + this.operator + val; }; persistence.NullFilter = NullFilter; persistence.AndFilter = AndFilter; persistence.OrFilter = OrFilter; persistence.PropertyFilter = PropertyFilter; /** * Ensure global uniqueness of query collection object */ persistence.uniqueQueryCollection = function(coll) { var entityName = coll._entityName; if(coll._items) { // LocalQueryCollection return coll; } if(!this.queryCollectionCache[entityName]) { this.queryCollectionCache[entityName] = {}; } var uniqueString = coll.toUniqueString(); if(!this.queryCollectionCache[entityName][uniqueString]) { this.queryCollectionCache[entityName][uniqueString] = coll; } return this.queryCollectionCache[entityName][uniqueString]; } /** * The constructor function of the _abstract_ QueryCollection * DO NOT INSTANTIATE THIS * @constructor */ function QueryCollection () { } QueryCollection.prototype = new Observable(); QueryCollection.prototype.oldAddEventListener = QueryCollection.prototype.addEventListener; QueryCollection.prototype.setupSubscriptions = function() { this._filter.subscribeGlobally(this, this._entityName); }; QueryCollection.prototype.teardownSubscriptions = function() { this._filter.unsubscribeGlobally(this, this._entityName); }; QueryCollection.prototype.addEventListener = function(eventType, fn) { var that = this; var subscription = this.oldAddEventListener(eventType, fn); if(this.subscribers[eventType].length === 1) { // first subscriber this.setupSubscriptions(); } subscription.oldUnsubscribe = subscription.unsubscribe; subscription.unsubscribe = function() { this.oldUnsubscribe(); if(that.subscribers[eventType].length === 0) { // last subscriber that.teardownSubscriptions(); } }; return subscription; }; /** * Function called when session is flushed, returns list of SQL queries to execute * (as [query, arg] tuples) */ QueryCollection.prototype.persistQueries = function() { return []; }; /** * Invoked by sub-classes to initialize the query collection */ QueryCollection.prototype.init = function (session, entityName, constructor) { this._filter = new NullFilter(); this._orderColumns = []; // tuples of [column, ascending] this._prefetchFields = []; this._entityName = entityName; this._constructor = constructor; this._limit = -1; this._skip = 0; this._reverse = false; this._session = session || persistence; // For observable this.subscribers = {}; } QueryCollection.prototype.toUniqueString = function() { var s = this._constructor.name + ": " + this._entityName; s += '|Filter:'; var values = []; s += this._filter.toUniqueString(); s += '|Values:'; for(var i = 0; i < values.length; i++) { s += values + "|^|"; } s += '|Order:'; for(var i = 0; i < this._orderColumns.length; i++) { var col = this._orderColumns[i]; s += col[0] + ", " + col[1] + ", " + col[2]; } s += '|Prefetch:'; for(var i = 0; i < this._prefetchFields.length; i++) { s += this._prefetchFields[i]; } s += '|Limit:'; s += this._limit; s += '|Skip:'; s += this._skip; s += '|Reverse:'; s += this._reverse; return s; }; /** * Creates a clone of this query collection * @return a clone of the collection */ QueryCollection.prototype.clone = function (cloneSubscribers) { var c = new (this._constructor)(this._session, this._entityName); c._filter = this._filter; c._prefetchFields = this._prefetchFields.slice(0); // clone c._orderColumns = this._orderColumns.slice(0); c._limit = this._limit; c._skip = this._skip; c._reverse = this._reverse; if(cloneSubscribers) { var subscribers = {}; for(var eventType in this.subscribers) { if(this.subscribers.hasOwnProperty(eventType)) { subscribers[eventType] = this.subscribers[eventType].slice(0); } } c.subscribers = subscribers; //this.subscribers; } else { c.subscribers = this.subscribers; } return c; }; /** * Returns a new query collection with a property filter condition added * @param property the property to filter on * @param operator the operator to use * @param value the literal value that the property should match * @return the query collection with the filter added */ QueryCollection.prototype.filter = function (property, operator, value) { var c = this.clone(true); c._filter = new AndFilter(this._filter, new PropertyFilter(property, operator, value)); // Add global listener (TODO: memory leak waiting to happen!) var session = this._session; c = session.uniqueQueryCollection(c); //session.subscribeToGlobalPropertyListener(c, this._entityName, property); return session.uniqueQueryCollection(c); }; /** * Returns a new query collection with an OR condition between the * current filter and the filter specified as argument * @param filter the other filter * @return the new query collection */ QueryCollection.prototype.or = function (filter) { var c = this.clone(true); c._filter = new OrFilter(this._filter, filter); return this._session.uniqueQueryCollection(c); }; /** * Returns a new query collection with an AND condition between the * current filter and the filter specified as argument * @param filter the other filter * @return the new query collection */ QueryCollection.prototype.and = function (filter) { var c = this.clone(true); c._filter = new AndFilter(this._filter, filter); return this._session.uniqueQueryCollection(c); }; /** * Returns a new query collection with an ordering imposed on the collection * @param property the property to sort on * @param ascending should the order be ascending (= true) or descending (= false) * @param caseSensitive should the order be case sensitive (= true) or case insensitive (= false) * note: using case insensitive ordering for anything other than TEXT fields yields * undefinded behavior * @return the query collection with imposed ordering */ QueryCollection.prototype.order = function (property, ascending, caseSensitive) { ascending = ascending === undefined ? true : ascending; caseSensitive = caseSensitive === undefined ? true : caseSensitive; var c = this.clone(); c._orderColumns.push( [ property, ascending, caseSensitive ]); return this._session.uniqueQueryCollection(c); }; /** * Returns a new query collection will limit its size to n items * @param n the number of items to limit it to * @return the limited query collection */ QueryCollection.prototype.limit = function(n) { var c = this.clone(); c._limit = n; return this._session.uniqueQueryCollection(c); }; /** * Returns a new query collection which will skip the first n results * @param n the number of results to skip * @return the query collection that will skip n items */ QueryCollection.prototype.skip = function(n) { var c = this.clone(); c._skip = n; return this._session.uniqueQueryCollection(c); }; /** * Returns a new query collection which reverse the order of the result set * @return the query collection that will reverse its items */ QueryCollection.prototype.reverse = function() { var c = this.clone(); c._reverse = true; return this._session.uniqueQueryCollection(c); }; /** * Returns a new query collection which will prefetch a certain object relationship. * Only works with 1:1 and N:1 relations. * Relation must target an entity, not a mix-in. * @param rel the relation name of the relation to prefetch * @return the query collection prefetching `rel` */ QueryCollection.prototype.prefetch = function (rel) { var c = this.clone(); c._prefetchFields.push(rel); return this._session.uniqueQueryCollection(c); }; /** * Select a subset of data, represented by this query collection as a JSON * structure (Javascript object) * * @param tx database transaction to use, leave out to start a new one * @param props a property specification * @param callback(result) */ QueryCollection.prototype.selectJSON = function(tx, props, callback) { var args = argspec.getArgs(arguments, [ { name: "tx", optional: true, check: persistence.isTransaction, defaultValue: null }, { name: "props", optional: false }, { name: "callback", optional: false } ]); var session = this._session; var that = this; tx = args.tx; props = args.props; callback = args.callback; if(!tx) { session.transaction(function(tx) { that.selectJSON(tx, props, callback); }); return; } var Entity = getEntity(this._entityName); // TODO: This could do some clever prefetching to make it more efficient this.list(function(items) { var resultArray = []; persistence.asyncForEach(items, function(item, callback) { item.selectJSON(tx, props, function(obj) { resultArray.push(obj); callback(); }); }, function() { callback(resultArray); }); }); }; /** * Adds an object to a collection * @param obj the object to add */ QueryCollection.prototype.add = function(obj) { if(!obj.id || !obj._type) { throw new Error("Cannot add object of non-entity type onto collection."); } this._session.add(obj); this._filter.makeFit(obj); this.triggerEvent('add', this, obj); this.triggerEvent('change', this, obj); } /** * Adds an an array of objects to a collection * @param obj the object to add */ QueryCollection.prototype.addAll = function(objs) { for(var i = 0; i < objs.length; i++) { var obj = objs[i]; this._session.add(obj); this._filter.makeFit(obj); this.triggerEvent('add', this, obj); } this.triggerEvent('change', this); } /** * Removes an object from a collection * @param obj the object to remove from the collection */ QueryCollection.prototype.remove = function(obj) { if(!obj.id || !obj._type) { throw new Error("Cannot remove object of non-entity type from collection."); } this._filter.makeNotFit(obj); this.triggerEvent('remove', this, obj); this.triggerEvent('change', this, obj); } /** * A database implementation of the QueryCollection * @param entityName the name of the entity to create the collection for * @constructor */ function DbQueryCollection (session, entityName) { this.init(session, entityName, DbQueryCollection); } /** * Execute a function for each item in the list * @param tx the transaction to use (or null to open a new one) * @param eachFn (elem) the function to be executed for each item */ QueryCollection.prototype.each = function (tx, eachFn) { var args = argspec.getArgs(arguments, [ { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, { name: 'eachFn', optional: true, check: argspec.isCallback() } ]); tx = args.tx; eachFn = args.eachFn; this.list(tx, function(results) { for(var i = 0; i < results.length; i++) { eachFn(results[i]); } }); } // Alias QueryCollection.prototype.forEach = QueryCollection.prototype.each; QueryCollection.prototype.one = function (tx, oneFn) { var args = argspec.getArgs(arguments, [ { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, { name: 'oneFn', optional: false, check: argspec.isCallback() } ]); tx = args.tx; oneFn = args.oneFn; var that = this; this.limit(1).list(tx, function(results) { if(results.length === 0) { oneFn(null); } else { oneFn(results[0]); } }); } DbQueryCollection.prototype = new QueryCollection(); /** * An implementation of QueryCollection, that is used * to represent all instances of an entity type * @constructor */ function AllDbQueryCollection (session, entityName) { this.init(session, entityName, AllDbQueryCollection); } AllDbQueryCollection.prototype = new DbQueryCollection(); AllDbQueryCollection.prototype.add = function(obj) { this._session.add(obj); this.triggerEvent('add', this, obj); this.triggerEvent('change', this, obj); }; AllDbQueryCollection.prototype.remove = function(obj) { this._session.remove(obj); this.triggerEvent('remove', this, obj); this.triggerEvent('change', this, obj); }; /** * A ManyToMany implementation of QueryCollection * @constructor */ function ManyToManyDbQueryCollection (session, entityName) { this.init(session, entityName, persistence.ManyToManyDbQueryCollection); this._localAdded = []; this._localRemoved = []; } ManyToManyDbQueryCollection.prototype = new DbQueryCollection(); ManyToManyDbQueryCollection.prototype.initManyToMany = function(obj, coll) { this._obj = obj; this._coll = coll; }; ManyToManyDbQueryCollection.prototype.add = function(obj) { if(!arrayContains(this._localAdded, obj)) { this._session.add(obj); this._localAdded.push(obj); this.triggerEvent('add', this, obj); this.triggerEvent('change', this, obj); } }; ManyToManyDbQueryCollection.prototype.addAll = function(objs) { for(var i = 0; i < objs.length; i++) { var obj = objs[i]; if(!arrayContains(this._localAdded, obj)) { this._session.add(obj); this._localAdded.push(obj); this.triggerEvent('add', this, obj); } } this.triggerEvent('change', this); } ManyToManyDbQueryCollection.prototype.clone = function() { var c = DbQueryCollection.prototype.clone.call(this); c._localAdded = this._localAdded; c._localRemoved = this._localRemoved; c._obj = this._obj; c._coll = this._coll; return c; }; ManyToManyDbQueryCollection.prototype.remove = function(obj) { if(arrayContains(this._localAdded, obj)) { // added locally, can just remove it from there arrayRemove(this._localAdded, obj); } else if(!arrayContains(this._localRemoved, obj)) { this._localRemoved.push(obj); } this.triggerEvent('remove', this, obj); this.triggerEvent('change', this, obj); }; ////////// Local implementation of QueryCollection \\\\\\\\\\\\\\\\ function LocalQueryCollection(initialArray) { this.init(persistence, null, LocalQueryCollection); this._items = initialArray || []; } LocalQueryCollection.prototype = new QueryCollection(); LocalQueryCollection.prototype.clone = function() { var c = DbQueryCollection.prototype.clone.call(this); c._items = this._items; return c; }; LocalQueryCollection.prototype.add = function(obj) { if(!arrayContains(this._items, obj)) { this._session.add(obj); this._items.push(obj); this.triggerEvent('add', this, obj); this.triggerEvent('change', this, obj); } }; LocalQueryCollection.prototype.addAll = function(objs) { for(var i = 0; i < objs.length; i++) { var obj = objs[i]; if(!arrayContains(this._items, obj)) { this._session.add(obj); this._items.push(obj); this.triggerEvent('add', this, obj); } } this.triggerEvent('change', this); } LocalQueryCollection.prototype.remove = function(obj) { var items = this._items; for(var i = 0; i < items.length; i++) { if(items[i] === obj) { this._items.splice(i, 1); this.triggerEvent('remove', this, obj); this.triggerEvent('change', this, obj); } } }; LocalQueryCollection.prototype.list = function(tx, callback) { var args = argspec.getArgs(arguments, [ { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, { name: 'callback', optional: true, check: argspec.isCallback() } ]); callback = args.callback; if(!callback || callback.executeSql) { // first argument is transaction callback = arguments[1]; // set to second argument } var array = this._items.slice(0); var that = this; var results = []; for(var i = 0; i < array.length; i++) { if(this._filter.match(array[i])) { results.push(array[i]); } } results.sort(function(a, b) { for(var i = 0; i < that._orderColumns.length; i++) { var col = that._orderColumns[i][0]; var asc = that._orderColumns[i][1]; var sens = that._orderColumns[i][2]; var aVal = persistence.get(a, col); var bVal = persistence.get(b, col); if (!sens) { aVal = aVal.toLowerCase(); bVal = bVal.toLowerCase(); } if(aVal < bVal) { return asc ? -1 : 1; } else if(aVal > bVal) { return asc ? 1 : -1; } } return 0; }); if(this._skip) { results.splice(0, this._skip); } if(this._limit > -1) { results = results.slice(0, this._limit); } if(this._reverse) { results.reverse(); } if(callback) { callback(results); } else { return results; } }; LocalQueryCollection.prototype.destroyAll = function(callback) { if(!callback || callback.executeSql) { // first argument is transaction callback = arguments[1]; // set to second argument } this._items = []; this.triggerEvent('change', this); if(callback) callback(); }; LocalQueryCollection.prototype.count = function(tx, callback) { var args = argspec.getArgs(arguments, [ { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, { name: 'callback', optional: true, check: argspec.isCallback() } ]); tx = args.tx; callback = args.callback; var result = this.list(); if(callback) { callback(result.length); } else { return result.length; } }; persistence.QueryCollection = QueryCollection; persistence.DbQueryCollection = DbQueryCollection; persistence.ManyToManyDbQueryCollection = ManyToManyDbQueryCollection; persistence.LocalQueryCollection = LocalQueryCollection; persistence.Observable = Observable; persistence.Subscription = Subscription; persistence.AndFilter = AndFilter; persistence.OrFilter = OrFilter; persistence.PropertyFilter = PropertyFilter; }()); // ArgSpec.js library: http://github.com/zefhemel/argspecjs var argspec = {}; (function() { argspec.getArgs = function(args, specs) { var argIdx = 0; var specIdx = 0; var argObj = {}; while(specIdx < specs.length) { var s = specs[specIdx]; var a = args[argIdx]; if(s.optional) { if(a !== undefined && s.check(a)) { argObj[s.name] = a; argIdx++; specIdx++; } else { if(s.defaultValue !== undefined) { argObj[s.name] = s.defaultValue; } specIdx++; } } else { if(s.check && !s.check(a)) { throw new Error("Invalid value for argument: " + s.name + " Value: " + a); } argObj[s.name] = a; specIdx++; argIdx++; } } return argObj; } argspec.hasProperty = function(name) { return function(obj) { return obj && obj[name] !== undefined; }; } argspec.hasType = function(type) { return function(obj) { return typeof obj === type; }; } argspec.isCallback = function() { return function(obj) { return obj && obj.apply; }; } }()); persistence.argspec = argspec; return persistence; } // end of createPersistence // JSON2 library, source: http://www.JSON.org/js.html // Most modern browsers already support this natively, but mobile // browsers often don't, hence this implementation // Relevant APIs: // JSON.stringify(value, replacer, space) // JSON.parse(text, reviver) if(typeof JSON === 'undefined') { JSON = {}; } //var JSON = typeof JSON === 'undefined' ? window.JSON : {}; if (!JSON.stringify) { (function () { function f(n) { return n < 10 ? '0' + n : n; } if (typeof Date.prototype.toJSON !== 'function') { Date.prototype.toJSON = function (key) { return isFinite(this.valueOf()) ? this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z' : null; }; String.prototype.toJSON = Number.prototype.toJSON = Boolean.prototype.toJSON = function (key) { return this.valueOf(); }; } var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, gap, indent, meta = { '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"', '\\': '\\\\' }, rep; function quote(string) { escapable.lastIndex = 0; return escapable.test(string) ? '"' + string.replace(escapable, function (a) { var c = meta[a]; return typeof c === 'string' ? c : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }) + '"' : '"' + string + '"'; } function str(key, holder) { var i, k, v, length, mind = gap, partial, value = holder[key]; if (value && typeof value === 'object' && typeof value.toJSON === 'function') { value = value.toJSON(key); } if (typeof rep === 'function') { value = rep.call(holder, key, value); } switch (typeof value) { case 'string': return quote(value); case 'number': return isFinite(value) ? String(value) : 'null'; case 'boolean': case 'null': return String(value); case 'object': if (!value) { return 'null'; } gap += indent; partial = []; if (Object.prototype.toString.apply(value) === '[object Array]') { length = value.length; for (i = 0; i < length; i += 1) { partial[i] = str(i, value) || 'null'; } v = partial.length === 0 ? '[]' : gap ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : '[' + partial.join(',') + ']'; gap = mind; return v; } if (rep && typeof rep === 'object') { length = rep.length; for (i = 0; i < length; i += 1) { k = rep[i]; if (typeof k === 'string') { v = str(k, value); if (v) { partial.push(quote(k) + (gap ? ': ' : ':') + v); } } } } else { for (k in value) { if (Object.hasOwnProperty.call(value, k)) { v = str(k, value); if (v) { partial.push(quote(k) + (gap ? ': ' : ':') + v); } } } } v = partial.length === 0 ? '{}' : gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : '{' + partial.join(',') + '}'; gap = mind; return v; } } if (typeof JSON.stringify !== 'function') { JSON.stringify = function (value, replacer, space) { var i; gap = ''; indent = ''; if (typeof space === 'number') { for (i = 0; i < space; i += 1) { indent += ' '; } } else if (typeof space === 'string') { indent = space; } rep = replacer; if (replacer && typeof replacer !== 'function' && (typeof replacer !== 'object' || typeof replacer.length !== 'number')) { throw new Error('JSON.stringify'); } return str('', {'': value}); }; } if (typeof JSON.parse !== 'function') { JSON.parse = function (text, reviver) { var j; function walk(holder, key) { var k, v, value = holder[key]; if (value && typeof value === 'object') { for (k in value) { if (Object.hasOwnProperty.call(value, k)) { v = walk(value, k); if (v !== undefined) { value[k] = v; } else { delete value[k]; } } } } return reviver.call(holder, key, value); } cx.lastIndex = 0; if (cx.test(text)) { text = text.replace(cx, function (a) { return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }); } if (/^[\],:{}\s]*$/. test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@'). replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'). replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { j = eval('(' + text + ')'); return typeof reviver === 'function' ? walk({'': j}, '') : j; } throw new SyntaxError('JSON.parse'); }; } }()); } ================================================ FILE: lib/persistence.migrations.js ================================================ /** * @license * Copyright (c) 2010 Fábio Rehm * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ if(!window.persistence) { // persistence.js not loaded! throw new Error("persistence.js should be loaded before persistence.migrations.js"); } (function() { var Migrator = { migrations: [], version: function(callback) { persistence.transaction(function(t){ t.executeSql('SELECT current_version FROM schema_version', null, function(result){ if (result.length == 0) { t.executeSql('INSERT INTO schema_version VALUES (0)', null, function(){ callback(0); }); } else { callback(result[0].current_version); } }); }); }, setVersion: function(v, callback) { persistence.transaction(function(t){ t.executeSql('UPDATE schema_version SET current_version = ?', [v], function(){ Migrator._version = v; if (callback) callback(); }); }); }, setup: function(callback) { persistence.transaction(function(t){ t.executeSql('CREATE TABLE IF NOT EXISTS schema_version (current_version INTEGER)', null, function(){ // Creates a dummy migration just to force setting schema version when cleaning DB Migrator.migration(0, { up: function() { }, down: function() { } }); if (callback) callback(); }); }); }, // Method should only be used for testing reset: function(callback) { // Creates a dummy migration just to force setting schema version when cleaning DB Migrator.migrations = []; Migrator.migration(0, { up: function() { }, down: function() { } }); Migrator.setVersion(0, callback); }, migration: function(version, actions) { Migrator.migrations[version] = new Migration(version, actions); return Migrator.migrations[version]; }, migrateUpTo: function(version, callback) { var migrationsToRun = []; function migrateOne() { var migration = migrationsToRun.pop(); if (!migration) callback(); migration.up(function(){ if (migrationsToRun.length > 0) { migrateOne(); } else if (callback) { callback(); } }); } this.version(function(currentVersion){ for (var v = currentVersion+1; v <= version; v++) migrationsToRun.unshift(Migrator.migrations[v]); if (migrationsToRun.length > 0) { migrateOne(); } else if (callback) { callback(); } }); }, migrateDownTo: function(version, callback) { var migrationsToRun = []; function migrateOne() { var migration = migrationsToRun.pop(); if (!migration) callback(); migration.down(function(){ if (migrationsToRun.length > 0) { migrateOne(); } else if (callback) { callback(); } }); } this.version(function(currentVersion){ for (var v = currentVersion; v > version; v--) migrationsToRun.unshift(Migrator.migrations[v]); if (migrationsToRun.length > 0) { migrateOne(); } else if (callback) { callback(); } }); }, migrate: function(version, callback) { if ( arguments.length === 1 ) { callback = version; version = this.migrations.length-1; } this.version(function(curVersion){ if (curVersion < version) Migrator.migrateUpTo(version, callback); else if (curVersion > version) Migrator.migrateDownTo(version, callback); else callback(); }); } } var Migration = function(version, body) { this.version = version; // TODO check if actions contains up and down methods this.body = body; this.actions = []; }; Migration.prototype.executeActions = function(callback, customVersion) { var actionsToRun = this.actions; var version = (customVersion!==undefined) ? customVersion : this.version; persistence.transaction(function(tx){ function nextAction() { if (actionsToRun.length == 0) Migrator.setVersion(version, callback); else { var action = actionsToRun.pop(); action(tx, nextAction); } } nextAction(); }); } Migration.prototype.up = function(callback) { if (this.body.up) this.body.up.apply(this); this.executeActions(callback); } Migration.prototype.down = function(callback) { if (this.body.down) this.body.down.apply(this); this.executeActions(callback, this.version-1); } Migration.prototype.createTable = function(tableName, callback) { var table = new ColumnsHelper(); if (callback) callback(table); var column; var sql = 'CREATE TABLE ' + tableName + ' (id VARCHAR(32) PRIMARY KEY'; while (column = table.columns.pop()) sql += ', ' + column; this.executeSql(sql + ')'); } Migration.prototype.dropTable = function(tableName) { var sql = 'DROP TABLE ' + tableName; this.executeSql(sql); } Migration.prototype.addColumn = function(tableName, columnName, columnType) { var sql = 'ALTER TABLE ' + tableName + ' ADD ' + columnName + ' ' + columnType; this.executeSql(sql); } Migration.prototype.removeColumn = function(tableName, columnName) { this.action(function(tx, nextCommand){ var sql = 'select sql from sqlite_master where type = "table" and name == "'+tableName+'"'; tx.executeSql(sql, null, function(result){ var columns = new RegExp("CREATE TABLE `\\w+` |\\w+ \\((.+)\\)").exec(result[0].sql)[1].split(', '); var selectColumns = []; var columnsSql = []; for (var i = 0; i < columns.length; i++) { var colName = new RegExp("((`\\w+`)|(\\w+)) .+").exec(columns[i])[1]; if (colName == columnName) continue; columnsSql.push(columns[i]); selectColumns.push(colName); } columnsSql = columnsSql.join(', '); selectColumns = selectColumns.join(', '); var queries = []; queries.unshift(["ALTER TABLE " + tableName + " RENAME TO " + tableName + "_bkp;", null]); queries.unshift(["CREATE TABLE " + tableName + " (" + columnsSql + ");", null]); queries.unshift(["INSERT INTO " + tableName + " SELECT " + selectColumns + " FROM " + tableName + "_bkp;", null]); queries.unshift(["DROP TABLE " + tableName + "_bkp;", null]); persistence.executeQueriesSeq(tx, queries, nextCommand); }); }); } Migration.prototype.addIndex = function(tableName, columnName, unique) { var sql = 'CREATE ' + (unique === true ? 'UNIQUE' : '') + ' INDEX ' + tableName + '_' + columnName + ' ON ' + tableName + ' (' + columnName + ')'; this.executeSql(sql); } Migration.prototype.removeIndex = function(tableName, columnName) { var sql = 'DROP INDEX ' + tableName + '_' + columnName; this.executeSql(sql); } Migration.prototype.executeSql = function(sql, args) { this.action(function(tx, nextCommand){ tx.executeSql(sql, args, nextCommand); }); } Migration.prototype.action = function(callback) { this.actions.unshift(callback); } var ColumnsHelper = function() { this.columns = []; } ColumnsHelper.prototype.text = function(columnName) { this.columns.unshift(columnName + ' TEXT'); } ColumnsHelper.prototype.integer = function(columnName) { this.columns.unshift(columnName + ' INT'); } ColumnsHelper.prototype.real = function(columnName) { this.columns.unshift(columnName + ' REAL'); } ColumnsHelper.prototype['boolean'] = function(columnName) { this.columns.unshift(columnName + ' BOOL'); } ColumnsHelper.prototype.date = function(columnName) { this.columns.unshift(columnName + ' DATE'); } ColumnsHelper.prototype.json = function(columnName) { this.columns.unshift(columnName + ' TEXT'); } // Makes Migrator and Migration available to tests persistence.migrations = {}; persistence.migrations.Migrator = Migrator; persistence.migrations.Migration = Migration; persistence.migrations.init = function() { Migrator.setup.apply(Migrator, Array.prototype.slice.call(arguments, 0))}; persistence.migrate = function() { Migrator.migrate.apply(Migrator, Array.prototype.slice.call(arguments, 0))}; persistence.defineMigration = function() { Migrator.migration.apply(Migrator, Array.prototype.slice.call(arguments, 0))}; }()); ================================================ FILE: lib/persistence.pool.js ================================================ var sys = require('sys'); var mysql = require('mysql'); function log(o) { sys.print(sys.inspect(o) + "\n"); } function ConnectionPool(getSession, initialPoolSize) { this.newConnection = getSession; this.pool = []; for(var i = 0; i < initialPoolSize; i++) { this.pool.push({available: true, session: getSession()}); } } ConnectionPool.prototype.obtain = function() { var session = null; for(var i = 0; i < this.pool.length; i++) { if(this.pool[i].available) { var pool = this.pool[i]; session = pool.session; pool.available = false; pool.claimed = new Date(); break; } } if(!session) { session = getSession(); this.pool.push({available: false, session: session, claimed: new Date() }); } }; ConnectionPool.prototype.release = function(session) { for(var i = 0; i < this.pool.length; i++) { if(this.pool[i].session === session) { var pool = this.pool[i]; pool.available = true; pool.claimed = null; return; } } return false; }; exports.ConnectionPool = ConnectionPool; ================================================ FILE: lib/persistence.search.js ================================================ /** * @license * Copyright (c) 2010 Zef Hemel * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ try { if(!window) { window = {}; } } catch(e) { window = {}; exports.console = console; } var persistence = (window && window.persistence) ? window.persistence : {}; persistence.search = {}; persistence.search.config = function(persistence, dialect) { var filteredWords = {'and':true, 'the': true, 'are': true}; var argspec = persistence.argspec; function normalizeWord(word, filterShortWords) { if(!(word in filteredWords || (filterShortWords && word.length < 3))) { word = word.replace(/ies$/, 'y'); word = word.length > 3 ? word.replace(/s$/, '') : word; return word; } else { return false; } } /** * Does extremely basic tokenizing of text. Also includes some basic stemming. */ function searchTokenizer(text) { var words = text.toLowerCase().split(/[^\w\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+/); var wordDict = {}; // Prefixing words with _ to also index Javascript keywords and special fiels like 'constructor' for(var i = 0; i < words.length; i++) { var normalizedWord = normalizeWord(words[i]); if(normalizedWord) { var word = '_' + normalizedWord; // Some extremely basic stemming if(word in wordDict) { wordDict[word]++; } else { wordDict[word] = 1; } } } return wordDict; } /** * Parses a search query and returns it as list SQL parts later to be OR'ed or AND'ed. */ function searchPhraseParser(query, indexTbl, prefixByDefault) { query = query.toLowerCase().replace(/['"]/, '').replace(/(^\s+|\s+$)/g, ''); var words = query.split(/\s+/); var sqlParts = []; var restrictedToColumn = null; for(var i = 0; i < words.length; i++) { var word = normalizeWord(words[i]); if(!word) { continue; } if(word.search(/:$/) !== -1) { restrictedToColumn = word.substring(0, word.length-1); continue; } var sql = '('; if(word.search(/\*/) !== -1) { sql += "`" + indexTbl + "`.`word` LIKE '" + word.replace(/\*/g, '%') + "'"; } else if(prefixByDefault) { sql += "`" + indexTbl + "`.`word` LIKE '" + word + "%'"; } else { sql += "`" + indexTbl + "`.`word` = '" + word + "'"; } if(restrictedToColumn) { sql += ' AND `' + indexTbl + "`.`prop` = '" + restrictedToColumn + "'"; } sql += ')'; sqlParts.push(sql); } return sqlParts.length === 0 ? ["1=1"] : sqlParts; } var queryCollSubscribers = {}; // entityName -> subscription obj persistence.searchQueryCollSubscribers = queryCollSubscribers; function SearchFilter(query, entityName) { this.query = query; this.entityName = entityName; } SearchFilter.prototype.match = function (o) { var meta = persistence.getMeta(this.entityName); var query = this.query.toLowerCase(); var text = ''; for(var p in o) { if(meta.textIndex.hasOwnProperty(p)) { if(o[p]) { text += o[p]; } } } text = text.toLowerCase(); return text && text.indexOf(query) !== -1; } SearchFilter.prototype.sql = function (o) { return "1=1"; } SearchFilter.prototype.subscribeGlobally = function(coll, entityName) { var meta = persistence.getMeta(entityName); for(var p in meta.textIndex) { if(meta.textIndex.hasOwnProperty(p)) { persistence.subscribeToGlobalPropertyListener(coll, entityName, p); } } }; SearchFilter.prototype.unsubscribeGlobally = function(coll, entityName) { var meta = persistence.getMeta(entityName); for(var p in meta.textIndex) { if(meta.textIndex.hasOwnProperty(p)) { persistence.unsubscribeFromGlobalPropertyListener(coll, entityName, p); } } }; SearchFilter.prototype.toUniqueString = function() { return "SEARCH: " + this.query; } function SearchQueryCollection(session, entityName, query, prefixByDefault) { this.init(session, entityName, SearchQueryCollection); this.subscribers = queryCollSubscribers[entityName]; this._filter = new SearchFilter(query, entityName); if(query) { this._additionalJoinSqls.push(', `' + entityName + '_Index`'); this._additionalWhereSqls.push('`root`.id = `' + entityName + '_Index`.`entityId`'); this._additionalWhereSqls.push('(' + searchPhraseParser(query, entityName + '_Index', prefixByDefault).join(' OR ') + ')'); this._additionalGroupSqls.push(' GROUP BY (`' + entityName + '_Index`.`entityId`)'); this._additionalGroupSqls.push(' ORDER BY SUM(`' + entityName + '_Index`.`occurrences`) DESC'); } } SearchQueryCollection.prototype = new persistence.DbQueryCollection(); SearchQueryCollection.prototype.oldClone = SearchQueryCollection.prototype.clone; SearchQueryCollection.prototype.clone = function() { var clone = this.oldClone(false); var entityName = this._entityName; clone.subscribers = queryCollSubscribers[entityName]; return clone; }; SearchQueryCollection.prototype.order = function() { throw new Error("Imposing additional orderings is not support for search query collections."); }; /* SearchQueryCollection.prototype.filter = function (property, operator, value) { var c = this.clone(); c._filter = new persistence.AndFilter(this._filter, new persistence.PropertyFilter(property, operator, value)); // Add global listener (TODO: memory leak waiting to happen!) //session.subscribeToGlobalPropertyListener(c, this._entityName, property); return c; }; */ persistence.entityDecoratorHooks.push(function(Entity) { /** * Declares a property to be full-text indexed. */ Entity.textIndex = function(prop) { if(!Entity.meta.textIndex) { Entity.meta.textIndex = {}; } Entity.meta.textIndex[prop] = true; // Subscribe var entityName = Entity.meta.name; if(!queryCollSubscribers[entityName]) { queryCollSubscribers[entityName] = {}; } }; /** * Returns a query collection representing the result of a search * @param query an object with the following fields: */ Entity.search = function(session, query, prefixByDefault) { var args = argspec.getArgs(arguments, [ { name: 'session', optional: true, check: function(obj) { return obj.schemaSync; }, defaultValue: persistence }, { name: 'query', optional: false, check: argspec.hasType('string') }, { name: 'prefixByDefault', optional: false } ]); session = args.session; query = args.query; prefixByDefault = args.prefixByDefault; return session.uniqueQueryCollection(new SearchQueryCollection(session, Entity.meta.name, query, prefixByDefault)); }; }); persistence.schemaSyncHooks.push(function(tx) { var entityMeta = persistence.getEntityMeta(); var queries = []; for(var entityName in entityMeta) { var meta = entityMeta[entityName]; if(meta.textIndex) { queries.push([dialect.createTable(entityName + '_Index', [['entityId', 'VARCHAR(32)'], ['prop', 'VARCHAR(30)'], ['word', 'VARCHAR(100)'], ['occurrences', 'INT']]), null]); queries.push([dialect.createIndex(entityName + '_Index', ['prop', 'word']), null]); queries.push([dialect.createIndex(entityName + '_Index', ['word']), null]); persistence.generatedTables[entityName + '_Index'] = true; } } queries.reverse(); persistence.executeQueriesSeq(tx, queries); }); persistence.flushHooks.push(function(session, tx, callback) { var queries = []; for (var id in session.getTrackedObjects()) { if (session.getTrackedObjects().hasOwnProperty(id)) { var obj = session.getTrackedObjects()[id]; var meta = session.define(obj._type).meta; var indexTbl = obj._type + '_Index'; if(meta.textIndex) { for ( var p in obj._dirtyProperties) { if (obj._dirtyProperties.hasOwnProperty(p) && p in meta.textIndex) { queries.push(['DELETE FROM `' + indexTbl + '` WHERE `entityId` = ? AND `prop` = ?', [id, p]]); var occurrences = searchTokenizer(obj._data[p]); for(var word in occurrences) { if(occurrences.hasOwnProperty(word)) { queries.push(['INSERT INTO `' + indexTbl + '` VALUES (?, ?, ?, ?)', [obj.id, p, word.substring(1), occurrences[word]]]); } } } } } } } for (var id in persistence.getObjectsToRemove()) { if (persistence.getObjectsToRemove().hasOwnProperty(id)) { var obj = persistence.getObjectsToRemove()[id]; var meta = persistence.getEntityMeta()[obj._type]; if(meta.textIndex) { queries.push(['DELETE FROM `' + obj._type + '_Index` WHERE `entityId` = ?', [id]]); } } } queries.reverse(); persistence.executeQueriesSeq(tx, queries, callback); }); }; if(typeof exports === 'object') { exports.config = persistence.search.config; } ================================================ FILE: lib/persistence.store.appengine.js ================================================ var jdatastore = Packages.com.google.appengine.api.datastore, JDatastoreServiceFactory = jdatastore.DatastoreServiceFactory, JKeyFactory = jdatastore.KeyFactory, JDatastoreService = jdatastore.DatastoreService, JFilterOperator = jdatastore.Query.FilterOperator, JSortDirection = jdatastore.Query.SortDirection, JQuery = jdatastore.Query, JInteger = java.lang.Integer; exports.config = function(persistence) { var argspec = persistence.argspec; exports.getSession = function() { var session = new persistence.Session(); session.dsService = JDatastoreServiceFactory.getDatastoreService(); session.transaction = function (fn) { fn({executeSql: function() {}}); }; session.close = function() { }; return session; }; /** * Converts a value from the data store to a value suitable for the entity * (also does type conversions, if necessary) */ function dbValToEntityVal(val, type) { if (val === null || val === undefined) { return val; } switch (type) { case 'DATE': // SQL is in seconds and JS in miliseconds if (val > 1000000000000) { // usually in seconds, but sometimes it's milliseconds return new Date(parseInt(val, 10)); } else { return new Date(parseInt(val, 10) * 1000); } case 'BOOL': return val === 1 || val === '1'; break; case 'INT': return +val; break; case 'BIGINT': return +val; break; case 'JSON': if (val) { return JSON.parse(val); } else { return val; } break; default: return val; } } /** * Converts an entity value to a data store value, inverse of * dbValToEntityVal */ function entityValToDbVal(val, type) { if (val === undefined || val === null) { return null; } else if (type === 'JSON' && val) { return JSON.stringify(val); } else if (val.id) { return val.id; } else if (type === 'BOOL') { return (val === 'false') ? 0 : (val ? 1 : 0); } else if (type === 'DATE' || val.getTime) { val = new Date(val); return new JInteger(Math.round(val.getTime() / 1000)); } else { return val; } } /** * Converts a data store entity to an entity object */ function aeEntityToEntity(session, aeEnt, Entity) { if(!aeEnt) return null; var o = new Entity(session); var meta = Entity.meta; o.id = aeEnt.key.name; var propMap = aeEnt.properties; for(var prop in Iterator(propMap.keySet())) { persistence.set(o, prop, dbValToEntityVal(propMap.get(prop), meta.fields[prop])); } return o; } /** * Converts a data store entity to an entity object */ function entityToAEEntity(meta, o) { var ent = new jdatastore.Entity(o._type, o.id); for(var k in meta.fields) { if(meta.fields.hasOwnProperty(k)) { ent.setProperty(k, entityValToDbVal(o._data[k], meta.fields[k])); } } for(var k in meta.hasOne) { if(meta.hasOne.hasOwnProperty(k)) { ent.setProperty(k, entityValToDbVal(o._data[k], meta.fields[k])); } } return ent; } var allEntities = []; persistence.schemaSync = function (tx, callback, emulate) { var args = argspec.getArgs(arguments, [ { name: "tx", optional: true, check: persistence.isTransaction, defaultValue: null }, { name: "callback", optional: true, check: argspec.isCallback(), defaultValue: function(){} }, { name: "emulate", optional: true, check: argspec.hasType('boolean') } ]); tx = args.tx; callback = args.callback; var entityMeta = persistence.getEntityMeta(); for (var entityName in entityMeta) { if (entityMeta.hasOwnProperty(entityName)) { allEntities.push(persistence.define(entityName)); } } callback(); }; persistence.flush = function (tx, callback) { var args = argspec.getArgs(arguments, [ { name: "tx", optional: true, check: persistence.isTransaction }, { name: "callback", optional: true, check: argspec.isCallback(), defaultValue: null } ]); tx = args.tx; callback = args.callback; var session = this; var fns = persistence.flushHooks; persistence.asyncForEach(fns, function(fn, callback) { fn(session, tx, callback); }, function() { // After applying the hooks var entitiesToPersist = []; var removeArrayList = new java.util.ArrayList(); for (var id in session.trackedObjects) { if (session.trackedObjects.hasOwnProperty(id)) { var ent = prepareDbEntity(session.trackedObjects[id]); if(ent) { entitiesToPersist.push(ent); } } } for (var id in session.objectsToRemove) { if (session.objectsToRemove.hasOwnProperty(id)) { removeArrayList.add(JKeyFactory.createKey(session.trackedObjects[id]._type, id)); delete session.trackedObjects[id]; // Stop tracking } } if(entitiesToPersist.length > 0) { session.dsService.put(entitiesToPersist); } if(removeArrayList.size() > 0) { session.dsService['delete'](removeArrayList); } callback(); }); }; persistence.reset = function (tx, callback) { var args = argspec.getArgs(arguments, [ { name: "tx", optional: true, check: persistence.isTransaction, defaultValue: null }, { name: "callback", optional: true, check: argspec.isCallback(), defaultValue: function(){} } ]); callback = args.callback; var session = this; persistence.asyncParForEach(allEntities, function(Entity, callback) { Entity.all(session).destroyAll(callback); }, callback); }; /** * Internal function to persist an object to the database * this function is invoked by persistence.flush() */ function prepareDbEntity(obj) { var meta = persistence.getMeta(obj._type); var isDirty = obj._new; if(Object.keys(obj._dirtyProperties).length > 0) { isDirty = true; } if(isDirty) { return entityToAEEntity(meta, obj); } else { return null; // no saving required } } /////////////////////////// QueryCollection patches to work in AppEngine environment persistence.NullFilter.prototype.addFilter = function (meta, query) { }; persistence.AndFilter.prototype.addFilter = function (meta, query) { this.left.addFilter(meta, query); this.right.addFilter(meta, query); }; persistence.OrFilter.prototype.addFilter = function (meta, query) { throw new Error("OrFilter Not supported"); }; persistence.PropertyFilter.prototype.addFilter = function (meta, query) { var filterOp; var value = this.value; switch(this.operator) { case '=': filterOp = JFilterOperator.EQUAL; break; case '!=': filterOp = JFilterOperator.NOT_EQUAL; break; case '>': filterOp = JFilterOperator.GREATER_THAN; break; case '>=': filterOp = JFilterOperator.GREATER_THAN_OR_EQUAL; break; case '<': filterOp = JFilterOperator.LESS_THAN; break; case '<=': filterOp = JFilterOperator.LESS_THAN_OR_EQUAL; break; case 'in': var values = new java.util.ArrayList(); var type = meta.fields[this.property]; for(var i = 0; i < value.length; i++) { values.add(entityValToDbVal(value[i], type)); } value = values; filterOp = JFilterOperator.IN; break; }; query.addFilter(this.property, filterOp, entityValToDbVal(value, meta.fields[this.property])); }; function prepareQuery(coll, callback) { var session = coll._session; var entityName = coll._entityName; var meta = persistence.getMeta(entityName); // handles mixin case -- this logic is generic and could be in persistence. if (meta.isMixin) { var result = []; persistence.asyncForEach(meta.mixedIns, function(realMeta, next) { var query = coll.clone(); query._entityName = realMeta.name; query.list(tx, function(array) { result = result.concat(array); next(); }); }, function() { var query = new persistence.LocalQueryCollection(result); query._orderColumns = coll._orderColumns; query._reverse = coll._reverse; // TODO: handle skip and limit -- do we really want to do it? query.list(null, callback); }); return; } var query = new JQuery(entityName); // Not tbe confused with jQuery coll._filter.addFilter(meta, query); coll._orderColumns.forEach(function(col) { query.addSort(col[0], col[1] ? JSortDirection.ASCENDING : JSortDirection.DESCENDING); }); callback(session.dsService.prepare(query)); } /** * Asynchronous call to actually fetch the items in the collection * @param tx transaction to use * @param callback function to be called taking an array with * result objects as argument */ persistence.DbQueryCollection.prototype.list = function (tx, callback) { var args = argspec.getArgs(arguments, [ { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, { name: 'callback', optional: false, check: argspec.isCallback() } ]); callback = args.callback; var that = this; var entityName = this._entityName; var session = this._session; // TODO: Check if filtering for 'id' property, then use key lookup // if(this._filter.right && this._filter.right.property && this._filter.right.property === 'id') { var idToLoad = this._filter.right.value; var obj; try { obj = session.dsService.get(JKeyFactory.createKey(entityName, idToLoad)); } catch(e) { // com.google.appengine.api.datastore.EntityNotFoundException obj = null; } if(obj) { callback([aeEntityToEntity(session, obj, persistence.define(entityName))]); } else { callback([]); } } else { session.flush(function() { prepareQuery(that, function(preparedQuery) { if(that._limit === 1) { var row = preparedQuery.asSingleEntity(); var e = aeEntityToEntity(session, row, persistence.define(entityName)); callback([e]); } else { var rows = preparedQuery.asList(jdatastore.FetchOptions.Builder.withLimit(that._limit === -1 ? 1000 : that._limit).offset(that._skip)); var results = []; var Entity = persistence.define(entityName); for (var i = 0; i < rows.size(); i++) { var r = rows.get(i); var e = aeEntityToEntity(session, r, Entity); results.push(e); session.add(e); } if(that._reverse) { results.reverse(); } that.triggerEvent('list', that, results); callback(results); } }); }); } }; persistence.DbQueryCollection.prototype.destroyAll = function (tx, callback) { var args = argspec.getArgs(arguments, [ { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, { name: 'callback', optional: true, check: argspec.isCallback(), defaultValue: function(){} } ]); callback = args.callback; var that = this; var session = this._session; session.flush(function() { prepareQuery(that, function(preparedQuery) { var rows = preparedQuery.asList(jdatastore.FetchOptions.Builder.withLimit(that._limit === -1 ? 1000 : that._limit).offset(that._skip)); var keys = new java.util.ArrayList(); for (var i = 0; i < rows.size(); i++) { var r = rows.get(i); keys.add(r.getKey()); } that._session.dsService['delete'](keys); that.triggerEvent('change', that); callback(); }); }); }; persistence.DbQueryCollection.prototype.count = function (tx, callback) { var args = argspec.getArgs(arguments, [ { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, { name: 'callback', optional: false, check: argspec.isCallback() } ]); tx = args.tx; callback = args.callback; var that = this; var session = this._session; session.flush(function() { prepareQuery(that, function(preparedQuery) { var n = preparedQuery.countEntities(jdatastore.FetchOptions.Builder.withDefaults()); callback(n); }); }); }; persistence.isSession = function(obj) { var isSession = !obj || (obj && obj.schemaSync); if(!isSession) { throw Error("No session argument passed, you should!"); } return isSession; }; }; ================================================ FILE: lib/persistence.store.config.js ================================================ exports.init = function(persistence, config) { var persistenceStore; switch (config.adaptor) { case 'memory': persistenceStore = require('./persistence.store.memory'); break; case 'mysql': persistenceStore = require('./persistence.store.mysql'); break; case 'sqlite3': persistenceStore = require('./persistence.store.sqlite3'); break; case 'react-native': persistenceStore = require('./persistence.store.react-native'); break; default: persistenceStore = require('./persistence.store.mysql'); break; } if (config.username) config.user = config.username; if (config.hostname) config.host = config.hostname; persistenceStore.config(persistence, config.host, config.port, config.database, config.user, config.password); return persistenceStore; }; ================================================ FILE: lib/persistence.store.cordovasql.js ================================================ try { if (!window) { window = {}; //exports.console = console; } } catch (e) { window = {}; exports.console = console; } var persistence = (window && window.persistence) ? window.persistence : {}; if (!persistence.store) { persistence.store = {}; } persistence.store.cordovasql = {}; /** * Configure the database connection (either sqliteplugin or websql) * * @param persistence * @param dbname * @param dbversion * @param description * @param size * @param backgroundProcessing * @param iOSLocation */ persistence.store.cordovasql.config = function (persistence, dbname, dbversion, description, size, backgroundProcessing, iOSLocation) { var conn = null; /** * Create a transaction * * @param callback * the callback function to be invoked when the transaction * starts, taking the transaction object as argument */ persistence.transaction = function (callback) { if (!conn) { throw new Error("No ongoing database connection, please connect first."); } else { conn.transaction(callback); } }; persistence.db = persistence.db || {}; persistence.db.implementation = "unsupported"; persistence.db.conn = null; /* Find out if sqliteplugin is loaded. Otherwise, we'll fall back to WebSql */ if (window && 'sqlitePlugin' in window) { persistence.db.implementation = 'sqliteplugin'; } else if (window && window.openDatabase) { persistence.db.implementation = "websql"; } else { // Well, we are stuck! } /* * Cordova SqlitePlugin */ persistence.db.sqliteplugin = {}; /** * Connect to Sqlite plugin database * * @param dbname * @param backgroundProcessing * @param iOSLocation * @returns {{}} */ persistence.db.sqliteplugin.connect = function (dbname, backgroundProcessing, iOSLocation) { var that = {}; var conn = window.sqlitePlugin.openDatabase({name: dbname, bgType: backgroundProcessing, location: (iOSLocation || 0)}); that.transaction = function (fn) { return conn.transaction(function (sqlt) { return fn(persistence.db.websql.transaction(sqlt)); }); }; return that; }; /** * Run transaction on Sqlite plugin database * * @param t * @returns {{}} */ persistence.db.sqliteplugin.transaction = function (t) { var that = {}; that.executeSql = function (query, args, successFn, errorFn) { if (persistence.debug) { console.log(query, args); } t.executeSql(query, args, function (_, result) { if (successFn) { var results = []; for (var i = 0; i < result.rows.length; i++) { results.push(result.rows.item(i)); } successFn(results); } }, errorFn); }; return that; }; /* * WebSQL */ persistence.db.websql = {}; /** * Connect to the default WebSQL database * * @param dbname * @param dbversion * @param description * @param size * @returns {{}} */ persistence.db.websql.connect = function (dbname, dbversion, description, size) { var that = {}; var conn = openDatabase(dbname, dbversion, description, size); that.transaction = function (fn) { return conn.transaction(function (sqlt) { return fn(persistence.db.websql.transaction(sqlt)); }); }; return that; }; /** * Run transaction on WebSQL database * * @param t * @returns {{}} */ persistence.db.websql.transaction = function (t) { var that = {}; that.executeSql = function (query, args, successFn, errorFn) { if (persistence.debug) { console.log(query, args); } t.executeSql(query, args, function (_, result) { if (successFn) { var results = []; for (var i = 0; i < result.rows.length; i++) { results.push(result.rows.item(i)); } successFn(results); } }, errorFn); }; return that; }; /** * Connect() wrapper * * @param dbname * @param dbversion * @param description * @param size * @param backgroundProcessing * @param iOSLocation * @returns {*} */ persistence.db.connect = function (dbname, dbversion, description, size, backgroundProcessing, iOSLocation) { if (persistence.db.implementation == "sqliteplugin") { return persistence.db.sqliteplugin.connect(dbname, backgroundProcessing, iOSLocation); } else if (persistence.db.implementation == "websql") { return persistence.db.websql.connect(dbname, dbversion, description, size); } return null; }; /** * Set the sqlite dialect * * @type {{createTable: createTable, createIndex: createIndex}} */ persistence.store.cordovasql.sqliteDialect = { /** * columns is an array of arrays, e.g. [["id", "VARCHAR(32)", "PRIMARY KEY"], ["name", "TEXT"]] * * @param tableName * @param columns * @returns {string} */ createTable: function (tableName, columns) { var tm = persistence.typeMapper; var sql = "CREATE TABLE IF NOT EXISTS `" + tableName + "` ("; var defs = []; for (var i = 0; i < columns.length; i++) { var column = columns[i]; defs.push("`" + column[0] + "` " + tm.columnType(column[1]) + (column[2] ? " " + column[2] : "")); } sql += defs.join(", "); sql += ')'; return sql; }, /** * columns is array of column names, e.g. ["id"] * @param tableName * @param columns * @param options * @returns {string} */ createIndex: function (tableName, columns, options) { options = options || {}; return "CREATE " + (options.unique ? "UNIQUE " : "") + "INDEX IF NOT EXISTS `" + tableName + "__" + columns.join("_") + "` ON `" + tableName + "` (" + columns.map(function (col) { return "`" + col + "`"; }).join(", ") + ")"; } }; // Configure persistence for generic sql persistence, using sqliteDialect persistence.store.sql.config(persistence, persistence.store.cordovasql.sqliteDialect); // Make the connection conn = persistence.db.connect(dbname, dbversion, description, size, backgroundProcessing, iOSLocation); if (!conn) { throw new Error("No supported database found in this browser."); } }; try { exports.persistence = persistence; } catch (e) { } ================================================ FILE: lib/persistence.store.memory.js ================================================ try { if(!window) { window = {}; //exports.console = console; } } catch(e) { window = {}; exports.console = console; } var persistence = (window && window.persistence) ? window.persistence : {}; if(!persistence.store) { persistence.store = {}; } persistence.store.memory = {}; persistence.store.memory.config = function(persistence, dbname) { var argspec = persistence.argspec; dbname = dbname || 'persistenceData'; var allObjects = {}; // entityName -> LocalQueryCollection persistence.getAllObjects = function() { return allObjects; }; var defaultAdd = persistence.add; persistence.add = function(obj) { if(!this.trackedObjects[obj.id]) { defaultAdd.call(this, obj); var entityName = obj._type; if(!allObjects[entityName]) { allObjects[entityName] = new persistence.LocalQueryCollection(); allObjects[entityName]._session = persistence; } allObjects[entityName].add(obj); } return this; }; var defaultRemove = persistence.remove; persistence.remove = function(obj) { defaultRemove.call(this, obj); var entityName = obj._type; allObjects[entityName].remove(obj); }; persistence.schemaSync = function (tx, callback, emulate) { var args = argspec.getArgs(arguments, [ { name: "tx", optional: true, check: persistence.isTransaction, defaultValue: null }, { name: "callback", optional: true, check: argspec.isCallback(), defaultValue: function(){} }, { name: "emulate", optional: true, check: argspec.hasType('boolean') } ]); args.callback(); }; persistence.flush = function (tx, callback) { var args = argspec.getArgs(arguments, [ { name: "tx", optional: true, check: persistence.isTransaction }, { name: "callback", optional: true, check: argspec.isCallback(), defaultValue: function(){} } ]); var fns = persistence.flushHooks; var session = this; persistence.asyncForEach(fns, function(fn, callback) { fn(session, tx, callback); }, function() { var trackedObjects = persistence.trackedObjects; for(var id in trackedObjects) { if(trackedObjects.hasOwnProperty(id)) { if (persistence.objectsToRemove.hasOwnProperty(id)) { delete trackedObjects[id]; } else { trackedObjects[id]._dirtyProperties = {}; } } } args.callback(); }); }; persistence.transaction = function(callback) { setTimeout(function() { callback({executeSql: function() {} }); }, 0); }; persistence.loadFromLocalStorage = function(callback) { var dump = window.localStorage.getItem(dbname); if(dump) { this.loadFromJson(dump, callback); } else { callback && callback(); } }; persistence.saveToLocalStorage = function(callback) { this.dumpToJson(function(dump) { window.localStorage.setItem(dbname, dump); if(callback) { callback(); } }); }; /** * Remove all tables in the database (as defined by the model) */ persistence.reset = function (tx, callback) { var args = argspec.getArgs(arguments, [ { name: "tx", optional: true, check: persistence.isTransaction, defaultValue: null }, { name: "callback", optional: true, check: argspec.isCallback(), defaultValue: function(){} } ]); tx = args.tx; callback = args.callback; allObjects = {}; this.clean(); callback(); }; /** * Dummy */ persistence.close = function() {}; // QueryCollection's list function makeLocalClone(otherColl) { var coll = allObjects[otherColl._entityName]; if(!coll) { coll = new persistence.LocalQueryCollection(); } coll = coll.clone(); coll._filter = otherColl._filter; coll._prefetchFields = otherColl._prefetchFields; coll._orderColumns = otherColl._orderColumns; coll._limit = otherColl._limit; coll._skip = otherColl._skip; coll._reverse = otherColl._reverse; return coll; } /** * Asynchronous call to actually fetch the items in the collection * @param tx transaction to use * @param callback function to be called taking an array with * result objects as argument */ persistence.DbQueryCollection.prototype.list = function (tx, callback) { var args = argspec.getArgs(arguments, [ { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, { name: 'callback', optional: false, check: argspec.isCallback() } ]); tx = args.tx; callback = args.callback; var coll = makeLocalClone(this); coll.list(null, callback); }; /** * Asynchronous call to remove all the items in the collection. * Note: does not only remove the items from the collection, but * the items themselves. * @param tx transaction to use * @param callback function to be called when clearing has completed */ persistence.DbQueryCollection.prototype.destroyAll = function (tx, callback) { var args = argspec.getArgs(arguments, [ { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, { name: 'callback', optional: true, check: argspec.isCallback(), defaultValue: function(){} } ]); tx = args.tx; callback = args.callback; var coll = makeLocalClone(this); coll.destroyAll(null, callback); }; /** * Asynchronous call to count the number of items in the collection. * @param tx transaction to use * @param callback function to be called when clearing has completed */ persistence.DbQueryCollection.prototype.count = function (tx, callback) { var args = argspec.getArgs(arguments, [ { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, { name: 'callback', optional: false, check: argspec.isCallback() } ]); tx = args.tx; callback = args.callback; var coll = makeLocalClone(this); coll.count(null, callback); }; persistence.ManyToManyDbQueryCollection = function(session, entityName) { this.init(session, entityName, persistence.ManyToManyDbQueryCollection); this._items = []; }; persistence.ManyToManyDbQueryCollection.prototype = new persistence.LocalQueryCollection(); persistence.ManyToManyDbQueryCollection.prototype.initManyToMany = function(obj, coll) { this._obj = obj; this._coll = coll; // column name }; persistence.ManyToManyDbQueryCollection.prototype.add = function(item, recursing) { persistence.LocalQueryCollection.prototype.add.call(this, item); if(!recursing) { // prevent recursively adding to one another // Let's find the inverse collection var meta = persistence.getMeta(this._obj._type); var inverseProperty = meta.hasMany[this._coll].inverseProperty; persistence.get(item, inverseProperty).add(this._obj, true); } }; persistence.ManyToManyDbQueryCollection.prototype.remove = function(item, recursing) { persistence.LocalQueryCollection.prototype.remove.call(this, item); if(!recursing) { // prevent recursively adding to one another // Let's find the inverse collection var meta = persistence.getMeta(this._obj._type); var inverseProperty = meta.hasMany[this._coll].inverseProperty; persistence.get(item, inverseProperty).remove(this._obj, true); } }; }; try { exports.config = persistence.store.memory.config; exports.getSession = function() { return persistence; }; } catch(e) {} ================================================ FILE: lib/persistence.store.mysql.js ================================================ /** * This back-end depends on the node.js asynchronous MySQL driver as found on: * http://github.com/felixge/node-mysql/ * Easy install using npm: * npm install mysql */ var sys = require('sys'); var sql = require('./persistence.store.sql'); var mysql = require('mysql'); var db, username, password; function log(o) { sys.print(sys.inspect(o) + "\n"); } exports.config = function(persistence, hostname, port, db, username, password) { exports.getSession = function(cb) { var that = {}; var opts = { host: hostname, port: port, database: db, user: username, password: password }; var client; function handleDisconnect() { connection = mysql.createConnection(opts); } if(typeof(mysql.createConnection)=='undefined'){ client = mysql.createClient(opts); }else{ client = new mysql.createConnection(opts); client.connect(function(err) { if(err){ console.error(err); setTimeout(handleDisconnect, 2000); } }); client.on('error', function(err) { console.error(err); if(err.code === 'PROTOCOL_CONNECTION_LOST') { handleDisconnect(); } else { throw err; } }); } var session = new persistence.Session(that); session.transaction = function (explicitCommit, fn) { if (typeof arguments[0] === "function") { fn = arguments[0]; explicitCommit = false; } var tx = transaction(client); if (explicitCommit) { tx.executeSql("START TRANSACTION", null, function(){ fn(tx) }); } else fn(tx); }; session.close = function() { client.end(); //conn._connection.destroy(); }; session.client = client; return session; }; function transaction(conn){ var that = {}; if(conn.ending) { throw new Error("Connection has been closed, cannot execute query."); } that.executeSql = function(query, args, successFn, errorFn){ function cb(err, result){ if (err) { log(err.message); that.errorHandler && that.errorHandler(err); errorFn && errorFn(null, err); return; } if (successFn) { successFn(result); } } if (persistence.debug) { sys.print(query + "\n"); args && args.length > 0 && sys.print(args.join(",") + "\n") } if (!args) { conn.query(query, cb); } else { conn.query(query, args, cb); } } that.commit = function(session, callback){ session.flush(that, function(){ that.executeSql("COMMIT", null, callback); }) } that.rollback = function(session, callback){ that.executeSql("ROLLBACK", null, function() { session.clean(); callback && callback(); }); } return that; } exports.mysqlDialect = { // columns is an array of arrays, e.g. // [["id", "VARCHAR(32)", "PRIMARY KEY"], ["name", "TEXT"]] createTable: function(tableName, columns) { var tm = persistence.typeMapper; var sql = "CREATE TABLE IF NOT EXISTS `" + tableName + "` ("; var defs = []; for(var i = 0; i < columns.length; i++) { var column = columns[i]; defs.push("`" + column[0] + "` " + tm.columnType(column[1]) + (column[2] ? " " + column[2] : "")); } sql += defs.join(", "); sql += ') ENGINE=InnoDB DEFAULT CHARSET=utf8'; return sql; }, // columns is array of column names, e.g. // ["id"] createIndex: function(tableName, columns, options) { options = options || {}; return "CREATE "+(options.unique?"UNIQUE ":"")+"INDEX `" + tableName + "__" + columns.join("_") + "` ON `" + tableName + "` (" + columns.map(function(col) { return "`" + col + "`"; }).join(", ") + ")"; } }; sql.config(persistence, exports.mysqlDialect); }; ================================================ FILE: lib/persistence.store.react-native.js ================================================ /** * This module depends on the react-native asynchronous SQLite3 driver as found on: * https://github.com/almost/react-native-sqlite * Easy install using npm: * npm install react-native-sqlite * and follow the instructions provided in the README * @author Lukas Reichart */ var sys = {}; sys.print = console.log; var sql = require('./persistence.store.sql'); var sqlite = require('react-native-sqlite'); var db, username, password; function log(o) { sys.print(o + "\n"); } exports.config = function(persistence, dbPath) { exports.getSession = function(cb) { var that = {}; cb = cb || function() { }; var conn = new sqlite.Database(dbPath, cb); var session = new persistence.Session(that); session.transaction = function (explicitCommit, fn) { if (typeof arguments[0] === "function") { fn = arguments[0]; explicitCommit = false; } var tx = transaction(conn); if (explicitCommit) { tx.executeSql("START TRANSACTION", null, function(){ fn(tx) }); } else fn(tx); }; session.close = function(cb) { cb = cb || function() {}; conn.close(cb); }; return session; }; function transaction(conn){ var that = {}; // TODO: add check for db opened or closed that.executeSql = function(query, args, successFn, errorFn){ var queryResult = []; function cb(err){ if (err) { log(err.message); that.errorHandler && that.errorHandler(err); errorFn && errorFn(null, err); return; } if (successFn) { if( !queryResult ) { queryResult = []; } successFn(queryResult); } } function rowCallback(row) { queryResult.push(row); } if (persistence.debug) { console.log(query + "\n"); //args && args.length > 0 && sys.print(args.join(",") + "\n") } if (!args) { conn.executeSQL(query, [], rowCallback, cb ); } else { conn.executeSQL(query, args, rowCallback, cb ); } } that.commit = function(session, callback){ session.flush(that, function(){ that.executeSQL("COMMIT", [], function(){}, callback); }) } that.rollback = function(session, callback){ that.executeSQL("ROLLBACK", [], function() {}, function() { session.clean(); callback(); }); } return that; } ///////////////////////// SQLite dialect persistence.sqliteDialect = { // columns is an array of arrays, e.g. // [["id", "VARCHAR(32)", "PRIMARY KEY"], ["name", "TEXT"]] createTable: function(tableName, columns) { var tm = persistence.typeMapper; var sql = "CREATE TABLE IF NOT EXISTS `" + tableName + "` ("; var defs = []; for(var i = 0; i < columns.length; i++) { var column = columns[i]; defs.push("`" + column[0] + "` " + tm.columnType(column[1]) + (column[2] ? " " + column[2] : "")); } sql += defs.join(", "); sql += ')'; return sql; }, // columns is array of column names, e.g. // ["id"] createIndex: function(tableName, columns, options) { options = options || {}; return "CREATE "+(options.unique?"UNIQUE ":"")+"INDEX IF NOT EXISTS `" + tableName + "__" + columns.join("_") + "` ON `" + tableName + "` (" + columns.map(function(col) { return "`" + col + "`"; }).join(", ") + ")"; } }; sql.config(persistence, persistence.sqliteDialect); }; ================================================ FILE: lib/persistence.store.sql.js ================================================ /** * Default type mapper. Override to support more types or type options. */ var defaultTypeMapper = { /** * SQL type for ids */ idType: "VARCHAR(32)", /** * SQL type for class names (used by mixins) */ classNameType: "TEXT", /** * Returns SQL type for column definition */ columnType: function(type){ switch(type) { case 'JSON': return 'TEXT'; case 'BOOL': return 'INT'; case 'DATE': return 'INT'; default: return type; } }, inVar: function(str, type){ return str; }, outVar: function(str, type){ return str; }, outId: function(str){ return "'" + str + "'"; }, /** * Converts a value from the database to a value suitable for the entity * (also does type conversions, if necessary) */ dbValToEntityVal: function(val, type){ if (val === null || val === undefined) { return val; } switch (type) { case 'DATE': // SQL is in seconds and JS in miliseconds if (val > 1000000000000) { // usually in seconds, but sometimes it's milliseconds return new Date(parseInt(val, 10)); } else { return new Date(parseInt(val, 10) * 1000); } case 'BOOL': return val === 1 || val === '1'; break; case 'INT': return +val; break; case 'BIGINT': return +val; break; case 'JSON': if (val) { return JSON.parse(val); } else { return val; } break; default: return val; } }, /** * Converts an entity value to a database value, inverse of * dbValToEntityVal */ entityValToDbVal: function(val, type){ if (val === undefined || val === null) { return null; } else if (type === 'JSON' && val) { return JSON.stringify(val); } else if (val.id) { return val.id; } else if (type === 'BOOL') { return (val === 'false') ? 0 : (val ? 1 : 0); } else if (type === 'DATE' || val.getTime) { // In order to make SQLite Date/Time functions work we should store // values in seconds and not as miliseconds as JS Date.getTime() val = new Date(val); return Math.round(val.getTime() / 1000); } else if(type === 'VARCHAR(32)'){ return val.toString(); } else { return val; } }, /** * Shortcut for inVar when type is id -- no need to override */ inIdVar: function(str){ return this.inVar(str, this.idType); }, /** * Shortcut for outVar when type is id -- no need to override */ outIdVar: function(str){ return this.outVar(str, this.idType); }, /** * Shortcut for entityValToDbVal when type is id -- no need to override */ entityIdToDbId: function(id){ return this.entityValToDbVal(id, this.idType); } } function config(persistence, dialect) { var argspec = persistence.argspec; persistence.typeMapper = dialect.typeMapper || defaultTypeMapper; persistence.generatedTables = {}; // set /** * Synchronize the data model with the database, creates table that had not * been defined before * * @param tx * transaction object to use (optional) * @param callback * function to be called when synchronization has completed, * takes started transaction as argument */ persistence.schemaSync = function (tx, callback, emulate) { var args = argspec.getArgs(arguments, [ { name: "tx", optional: true, check: persistence.isTransaction, defaultValue: null }, { name: "callback", optional: true, check: argspec.isCallback(), defaultValue: function(){} }, { name: "emulate", optional: true, check: argspec.hasType('boolean') } ]); tx = args.tx; callback = args.callback; emulate = args.emulate; if(!tx) { var session = this; this.transaction(function(tx) { session.schemaSync(tx, callback, emulate); }); return; } var queries = [], meta, colDefs, otherMeta, tableName; var tm = persistence.typeMapper; var entityMeta = persistence.getEntityMeta(); for (var entityName in entityMeta) { if (entityMeta.hasOwnProperty(entityName)) { meta = entityMeta[entityName]; if (!meta.isMixin) { colDefs = []; for (var prop in meta.fields) { if (meta.fields.hasOwnProperty(prop)) { colDefs.push([prop, meta.fields[prop]]); } } for (var rel in meta.hasOne) { if (meta.hasOne.hasOwnProperty(rel)) { otherMeta = meta.hasOne[rel].type.meta; colDefs.push([rel, tm.idType]); queries.push([dialect.createIndex(meta.name, [rel]), null]); } } for (var i = 0; i < meta.indexes.length; i++) { queries.push([dialect.createIndex(meta.name, meta.indexes[i].columns, meta.indexes[i]), null]); } } for (var rel in meta.hasMany) { if (meta.hasMany.hasOwnProperty(rel) && meta.hasMany[rel].manyToMany) { tableName = meta.hasMany[rel].tableName; if (!persistence.generatedTables[tableName]) { var otherMeta = meta.hasMany[rel].type.meta; var inv = meta.hasMany[rel].inverseProperty; // following test ensures that mixin mtm tables get created with the mixin itself // it seems superfluous because mixin will be processed before entitites that use it // but better be safe than sorry. if (otherMeta.hasMany[inv].type.meta != meta) continue; var p1 = meta.name + "_" + rel; var p2 = otherMeta.name + "_" + inv; queries.push([dialect.createIndex(tableName, [p1]), null]); queries.push([dialect.createIndex(tableName, [p2]), null]); var columns = [[p1, tm.idType], [p2, tm.idType]]; if (meta.isMixin) columns.push([p1 + "_class", tm.classNameType]) if (otherMeta.isMixin) columns.push([p2 + "_class", tm.classNameType]) queries.push([dialect.createTable(tableName, columns), null]); persistence.generatedTables[tableName] = true; } } } if (!meta.isMixin) { colDefs.push(["id", tm.idType, "PRIMARY KEY"]); persistence.generatedTables[meta.name] = true; queries.push([dialect.createTable(meta.name, colDefs), null]); } } } var fns = persistence.schemaSyncHooks; for(var i = 0; i < fns.length; i++) { fns[i](tx); } if(emulate) { // Done callback(tx); } else { executeQueriesSeq(tx, queries, function(_, err) { callback(tx, err); }); } }; /** * Persists all changes to the database transaction * * @param tx * transaction to use * @param callback * function to be called when done */ persistence.flush = function (tx, callback) { var args = argspec.getArgs(arguments, [ { name: "tx", optional: true, check: persistence.isTransaction }, { name: "callback", optional: true, check: argspec.isCallback(), defaultValue: null } ]); tx = args.tx; callback = args.callback; var session = this; if(!tx) { this.transaction(function(tx) { session.flush(tx, callback); }); return; } var fns = persistence.flushHooks; persistence.asyncForEach(fns, function(fn, callback) { fn(session, tx, callback); }, function() { // After applying the hooks var persistObjArray = []; for (var id in session.trackedObjects) { if (session.trackedObjects.hasOwnProperty(id)) { persistObjArray.push(session.trackedObjects[id]); } } var removeObjArray = []; for (var id in session.objectsToRemove) { if (session.objectsToRemove.hasOwnProperty(id)) { removeObjArray.push(session.objectsToRemove[id]); delete session.trackedObjects[id]; // Stop tracking } } session.objectsToRemove = {}; if(callback) { persistence.asyncParForEach(removeObjArray, function(obj, callback) { remove(obj, tx, callback); }, function(result, err) { if (err) return callback(result, err); persistence.asyncParForEach(persistObjArray, function(obj, callback) { save(obj, tx, callback); }, callback); }); } else { // More efficient for(var i = 0; i < persistObjArray.length; i++) { save(persistObjArray[i], tx); } for(var i = 0; i < removeObjArray.length; i++) { remove(removeObjArray[i], tx); } } }); }; /** * Remove all tables in the database (as defined by the model) */ persistence.reset = function (tx, callback) { var args = argspec.getArgs(arguments, [ { name: "tx", optional: true, check: persistence.isTransaction, defaultValue: null }, { name: "callback", optional: true, check: argspec.isCallback(), defaultValue: function(){} } ]); tx = args.tx; callback = args.callback; var session = this; if(!tx) { session.transaction(function(tx) { session.reset(tx, callback); }); return; } // First emulate syncing the schema (to know which tables were created) this.schemaSync(tx, function() { var tableArray = []; for (var p in persistence.generatedTables) { if (persistence.generatedTables.hasOwnProperty(p)) { tableArray.push(p); } } function dropOneTable () { var tableName = tableArray.pop(); tx.executeSql("DROP TABLE IF EXISTS `" + tableName + "`", null, function () { if (tableArray.length > 0) { dropOneTable(); } else { cb(); } }, cb); } if(tableArray.length > 0) { dropOneTable(); } else { cb(); } function cb(result, err) { session.clean(); persistence.generatedTables = {}; if (callback) callback(result, err); } }, true); }; /** * Converts a database row into an entity object */ function rowToEntity(session, entityName, row, prefix) { prefix = prefix || ''; if (session.trackedObjects[row[prefix + "id"]]) { // Cached version return session.trackedObjects[row[prefix + "id"]]; } var tm = persistence.typeMapper; var rowMeta = persistence.getMeta(entityName); var ent = persistence.define(entityName); // Get entity if(!row[prefix+'id']) { // null value, no entity found return null; } var o = new ent(session, undefined, true); o.id = tm.dbValToEntityVal(row[prefix + 'id'], tm.idType); o._new = false; for ( var p in row) { if (row.hasOwnProperty(p)) { if (p.substring(0, prefix.length) === prefix) { var prop = p.substring(prefix.length); if (prop != 'id') { o._data[prop] = tm.dbValToEntityVal(row[p], rowMeta.fields[prop] || tm.idType); } } } } return o; } /** * Internal function to persist an object to the database * this function is invoked by persistence.flush() */ function save(obj, tx, callback) { var meta = persistence.getMeta(obj._type); var tm = persistence.typeMapper; var properties = []; var values = []; var qs = []; var propertyPairs = []; if(obj._new) { // Mark all properties dirty for (var p in meta.fields) { if(meta.fields.hasOwnProperty(p)) { obj._dirtyProperties[p] = true; } } } for ( var p in obj._dirtyProperties) { if (obj._dirtyProperties.hasOwnProperty(p)) { properties.push("`" + p + "`"); var type = meta.fields[p] || tm.idType; values.push(tm.entityValToDbVal(obj._data[p], type)); qs.push(tm.outVar("?", type)); propertyPairs.push("`" + p + "` = " + tm.outVar("?", type)); } } var additionalQueries = []; if(meta && meta.hasMany) { for(var p in meta.hasMany) { if(meta.hasMany.hasOwnProperty(p)) { additionalQueries = additionalQueries.concat(persistence.get(obj, p).persistQueries()); } } } executeQueriesSeq(tx, additionalQueries, function() { if (!obj._new && properties.length === 0) { // Nothing changed and not new if(callback) callback(); return; } obj._dirtyProperties = {}; if (obj._new) { properties.push('id'); values.push(tm.entityIdToDbId(obj.id)); qs.push(tm.outIdVar('?')); var sql = "INSERT INTO `" + obj._type + "` (" + properties.join(", ") + ") VALUES (" + qs.join(', ') + ")"; obj._new = false; tx.executeSql(sql, values, callback, callback); } else { var sql = "UPDATE `" + obj._type + "` SET " + propertyPairs.join(',') + " WHERE id = " + tm.outId(obj.id); tx.executeSql(sql, values, callback, callback); } }); } persistence.save = save; function remove (obj, tx, callback) { var meta = persistence.getMeta(obj._type); var tm = persistence.typeMapper; var queries = [["DELETE FROM `" + obj._type + "` WHERE id = " + tm.outId(obj.id), null]]; for (var rel in meta.hasMany) { if (meta.hasMany.hasOwnProperty(rel) && meta.hasMany[rel].manyToMany) { var tableName = meta.hasMany[rel].tableName; //var inverseProperty = meta.hasMany[rel].inverseProperty; queries.push(["DELETE FROM `" + tableName + "` WHERE `" + meta.name + '_' + rel + "` = " + tm.outId(obj.id), null]); } } executeQueriesSeq(tx, queries, callback); } /** * Utility function to execute a series of queries in an asynchronous way * @param tx the transaction to execute the queries on * @param queries an array of [query, args] tuples * @param callback the function to call when all queries have been executed */ function executeQueriesSeq (tx, queries, callback) { // queries.reverse(); var callbackArgs = []; for ( var i = 3; i < arguments.length; i++) { callbackArgs.push(arguments[i]); } persistence.asyncForEach(queries, function(queryTuple, callback) { tx.executeSql(queryTuple[0], queryTuple[1], callback, function(_, err) { console.log(err.message); callback(_, err); }); }, function(result, err) { if (err && callback) { callback(result, err); return; } if(callback) callback.apply(null, callbackArgs); }); } persistence.executeQueriesSeq = executeQueriesSeq; /////////////////////////// QueryCollection patches to work in SQL environment /** * Function called when session is flushed, returns list of SQL queries to execute * (as [query, arg] tuples) */ persistence.QueryCollection.prototype.persistQueries = function() { return []; }; var oldQCClone = persistence.QueryCollection.prototype.clone; persistence.QueryCollection.prototype.clone = function (cloneSubscribers) { var c = oldQCClone.call(this, cloneSubscribers); c._additionalJoinSqls = this._additionalJoinSqls.slice(0); c._additionalWhereSqls = this._additionalWhereSqls.slice(0); c._additionalGroupSqls = this._additionalGroupSqls.slice(0); c._manyToManyFetch = this._manyToManyFetch; return c; }; var oldQCInit = persistence.QueryCollection.prototype.init; persistence.QueryCollection.prototype.init = function(session, entityName, constructor) { oldQCInit.call(this, session, entityName, constructor); this._manyToManyFetch = null; this._additionalJoinSqls = []; this._additionalWhereSqls = []; this._additionalGroupSqls = []; }; var oldQCToUniqueString = persistence.QueryCollection.prototype.toUniqueString; persistence.QueryCollection.prototype.toUniqueString = function() { var s = oldQCToUniqueString.call(this); s += '|JoinSQLs:'; for(var i = 0; i < this._additionalJoinSqls.length; i++) { s += this._additionalJoinSqls[i]; } s += '|WhereSQLs:'; for(var i = 0; i < this._additionalWhereSqls.length; i++) { s += this._additionalWhereSqls[i]; } s += '|GroupSQLs:'; for(var i = 0; i < this._additionalGroupSqls.length; i++) { s += this._additionalGroupSqls[i]; } if(this._manyToManyFetch) { s += '|ManyToManyFetch:'; s += JSON.stringify(this._manyToManyFetch); // TODO: Do something more efficient } return s; }; persistence.NullFilter.prototype.sql = function (meta, alias, values) { return "1=1"; }; persistence.AndFilter.prototype.sql = function (meta, alias, values) { return "(" + this.left.sql(meta, alias, values) + " AND " + this.right.sql(meta, alias, values) + ")"; }; persistence.OrFilter.prototype.sql = function (meta, alias, values) { return "(" + this.left.sql(meta, alias, values) + " OR " + this.right.sql(meta, alias, values) + ")"; }; persistence.PropertyFilter.prototype.sql = function (meta, alias, values) { var tm = persistence.typeMapper; var aliasPrefix = alias ? "`" + alias + "`." : ""; var sqlType = meta.fields[this.property] || tm.idType; if (this.operator === '=' && this.value === null) { return aliasPrefix + '`' + this.property + "` IS NULL"; } else if (this.operator === '!=' && this.value === null) { return aliasPrefix + '`' + this.property + "` IS NOT NULL"; } else if (this.operator === 'in') { var vals = this.value; var qs = []; for(var i = 0; i < vals.length; i++) { qs.push('?'); values.push(tm.entityValToDbVal(vals[i], sqlType)); } if(vals.length === 0) { // Optimize this a little return "1 = 0"; } else { return aliasPrefix + '`' + this.property + "` IN (" + qs.join(', ') + ")"; } } else if (this.operator === 'not in') { var vals = this.value; var qs = []; for(var i = 0; i < vals.length; i++) { qs.push('?'); values.push(tm.entityValToDbVal(vals[i], sqlType)); } if(vals.length === 0) { // Optimize this a little return "1 = 1"; } else { return aliasPrefix + '`' + this.property + "` NOT IN (" + qs.join(', ') + ")"; } } else { var value = this.value; if(value === true || value === false) { value = value ? 1 : 0; } values.push(tm.entityValToDbVal(value, sqlType)); return aliasPrefix + '`' + this.property + "` " + this.operator + " " + tm.outVar("?", sqlType); } }; // QueryColleciton's list /** * Asynchronous call to actually fetch the items in the collection * @param tx transaction to use * @param callback function to be called taking an array with * result objects as argument */ persistence.DbQueryCollection.prototype.list = function (tx, callback) { var args = argspec.getArgs(arguments, [ { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, { name: 'callback', optional: false, check: argspec.isCallback() } ]); tx = args.tx; callback = args.callback; var that = this; var session = this._session; if(!tx) { // no transaction supplied session.transaction(function(tx) { that.list(tx, callback); }); return; } var entityName = this._entityName; var meta = persistence.getMeta(entityName); var tm = persistence.typeMapper; // handles mixin case -- this logic is generic and could be in persistence. if (meta.isMixin) { var result = []; persistence.asyncForEach(meta.mixedIns, function(realMeta, next) { var query = that.clone(); query._entityName = realMeta.name; query.list(tx, function(array) { result = result.concat(array); next(); }); }, function() { var query = new persistence.LocalQueryCollection(result); query._orderColumns = that._orderColumns; query._reverse = that._reverse; // TODO: handle skip and limit -- do we really want to do it? query.list(null, callback); }); return; } function selectAll (meta, tableAlias, prefix) { var selectFields = [ tm.inIdVar("`" + tableAlias + "`.id") + " AS " + prefix + "id" ]; for ( var p in meta.fields) { if (meta.fields.hasOwnProperty(p)) { selectFields.push(tm.inVar("`" + tableAlias + "`.`" + p + "`", meta.fields[p]) + " AS `" + prefix + p + "`"); } } for ( var p in meta.hasOne) { if (meta.hasOne.hasOwnProperty(p)) { selectFields.push(tm.inIdVar("`" + tableAlias + "`.`" + p + "`") + " AS `" + prefix + p + "`"); } } return selectFields; } var args = []; var mainPrefix = entityName + "_"; var mainAlias = 'root'; var selectFields = selectAll(meta, mainAlias, mainPrefix); var joinSql = ''; var additionalWhereSqls = this._additionalWhereSqls.slice(0); var mtm = this._manyToManyFetch; if(mtm) { joinSql += "LEFT JOIN `" + mtm.table + "` AS mtm ON mtm.`" + mtm.inverseProp + "` = `root`.`id` "; additionalWhereSqls.push("mtm.`" + mtm.prop + "` = " + tm.outId(mtm.id)); } joinSql += this._additionalJoinSqls.join(' '); for ( var i = 0; i < this._prefetchFields.length; i++) { var prefetchFieldParts = this._prefetchFields[i].split('.'); var prefetchField = prefetchFieldParts[0]; var eName = entityName; if(prefetchFieldParts.length > 1){ prefetchField = prefetchFieldParts[1]; eName = prefetchFieldParts[0]; } var theMeta = persistence.getMeta(eName); var thisMeta = theMeta.hasOne[prefetchField].type.meta; if (thisMeta.isMixin) throw new Error("cannot prefetch a mixin"); var tableAlias = thisMeta.name + '_' + prefetchField + "_tbl"; var PrefetchFrom = mainAlias; if(prefetchFieldParts.length > 1){ PrefetchFrom = eName + '_' + eName + "_tbl";; } selectFields = selectFields.concat(selectAll(thisMeta, tableAlias, prefetchField + "_")); joinSql += "LEFT JOIN `" + thisMeta.name + "` AS `" + tableAlias + "` ON `" + tableAlias + "`.`id` = `" + PrefetchFrom + '`.`' + prefetchField + "` "; } var whereSql = "WHERE " + [ this._filter.sql(meta, mainAlias, args) ].concat(additionalWhereSqls).join(' AND '); var sql = "SELECT " + selectFields.join(", ") + " FROM `" + entityName + "` AS `" + mainAlias + "` " + joinSql + " " + whereSql; if(this._additionalGroupSqls.length > 0) { sql += this._additionalGroupSqls.join(' '); } if(this._orderColumns.length > 0) { sql += " ORDER BY " + this._orderColumns.map( function (c) { return (c[2] ? "`" : "LOWER(`") + mainPrefix + c[0] + (c[2] ? "` " : "`) ") + (c[1] ? "ASC" : "DESC"); }).join(", "); } if(this._limit >= 0) { sql += " LIMIT " + this._limit; } if(this._skip > 0) { sql += " OFFSET " + this._skip; } session.flush(tx, function () { tx.executeSql(sql, args, function (rows) { var results = []; if(that._reverse) { rows.reverse(); } for ( var i = 0; i < rows.length; i++) { var r = rows[i]; var e = rowToEntity(session, entityName, r, mainPrefix); for ( var j = 0; j < that._prefetchFields.length; j++) { var prefetchFieldParts = that._prefetchFields[j].split('.'); var prefetchField = prefetchFieldParts[0]; var eName = entityName; if(prefetchFieldParts.length > 1){ prefetchField = prefetchFieldParts[1]; eName = prefetchFieldParts[0]; } var theMeta = persistence.getMeta(eName); var thisMeta = theMeta.hasOne[prefetchField].type.meta; e._data_obj[prefetchField] = rowToEntity(session, thisMeta.name, r, prefetchField + '_'); session.add(e._data_obj[prefetchField]); } results.push(e); session.add(e); } callback(results); that.triggerEvent('list', that, results); }); }); }; /** * Asynchronous call to remove all the items in the collection. * Note: does not only remove the items from the collection, but * the items themselves. * @param tx transaction to use * @param callback function to be called when clearing has completed */ persistence.DbQueryCollection.prototype.destroyAll = function (tx, callback) { var args = argspec.getArgs(arguments, [ { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, { name: 'callback', optional: true, check: argspec.isCallback(), defaultValue: function(){} } ]); tx = args.tx; callback = args.callback; var that = this; var session = this._session; if(!tx) { // no transaction supplied session.transaction(function(tx) { that.destroyAll(tx, callback); }); return; } var entityName = this._entityName; var meta = persistence.getMeta(entityName); var tm = persistence.typeMapper; // handles mixin case -- this logic is generic and could be in persistence. if (meta.isMixin) { persistence.asyncForEach(meta.mixedIns, function(realMeta, next) { var query = that.clone(); query._entityName = realMeta.name; query.destroyAll(tx, callback); }, callback); return; } var joinSql = ''; var additionalWhereSqls = this._additionalWhereSqls.slice(0); var mtm = this._manyToManyFetch; if(mtm) { joinSql += "LEFT JOIN `" + mtm.table + "` AS mtm ON mtm.`" + mtm.inverseProp + "` = `root`.`id` "; additionalWhereSqls.push("mtm.`" + mtm.prop + "` = " + tm.outId(mtm.id)); } joinSql += this._additionalJoinSqls.join(' '); var args = []; var whereSql = "WHERE " + [ this._filter.sql(meta, null, args) ].concat(additionalWhereSqls).join(' AND '); var selectSql = "SELECT id FROM `" + entityName + "` " + joinSql + ' ' + whereSql; var deleteSql = "DELETE FROM `" + entityName + "` " + joinSql + ' ' + whereSql; var args2 = args.slice(0); session.flush(tx, function () { tx.executeSql(selectSql, args, function(results) { for(var i = 0; i < results.length; i++) { delete session.trackedObjects[results[i].id]; session.objectsRemoved.push({id: results[i].id, entity: entityName}); } that.triggerEvent('change', that); tx.executeSql(deleteSql, args2, callback, callback); }, callback); }); }; /** * Asynchronous call to count the number of items in the collection. * @param tx transaction to use * @param callback function to be called when clearing has completed */ persistence.DbQueryCollection.prototype.count = function (tx, callback) { var args = argspec.getArgs(arguments, [ { name: 'tx', optional: true, check: persistence.isTransaction, defaultValue: null }, { name: 'callback', optional: false, check: argspec.isCallback() } ]); tx = args.tx; callback = args.callback; var that = this; var session = this._session; if(tx && !tx.executeSql) { // provided callback as first argument callback = tx; tx = null; } if(!tx) { // no transaction supplied session.transaction(function(tx) { that.count(tx, callback); }); return; } var entityName = this._entityName; var meta = persistence.getMeta(entityName); var tm = persistence.typeMapper; // handles mixin case -- this logic is generic and could be in persistence. if (meta.isMixin) { var result = 0; persistence.asyncForEach(meta.mixedIns, function(realMeta, next) { var query = that.clone(); query._entityName = realMeta.name; query.count(tx, function(count) { result += count; next(); }); }, function() { callback(result); }); return; } var joinSql = ''; var additionalWhereSqls = this._additionalWhereSqls.slice(0); var mtm = this._manyToManyFetch; if(mtm) { joinSql += "LEFT JOIN `" + mtm.table + "` AS mtm ON mtm.`" + mtm.inverseProp + "` = `root`.`id` "; additionalWhereSqls.push("mtm.`" + mtm.prop + "` = " + tm.outId(mtm.id)); } joinSql += this._additionalJoinSqls.join(' '); var args = []; var whereSql = "WHERE " + [ this._filter.sql(meta, "root", args) ].concat(additionalWhereSqls).join(' AND '); var sql = "SELECT COUNT(*) AS cnt FROM `" + entityName + "` AS `root` " + joinSql + " " + whereSql; session.flush(tx, function () { tx.executeSql(sql, args, function(results) { callback(parseInt(results[0].cnt, 10)); }); }); }; persistence.ManyToManyDbQueryCollection.prototype.persistQueries = function() { var queries = []; var meta = persistence.getMeta(this._obj._type); var inverseMeta = meta.hasMany[this._coll].type.meta; var tm = persistence.typeMapper; var rel = meta.hasMany[this._coll]; var inv = inverseMeta.hasMany[rel.inverseProperty]; var direct = rel.mixin ? rel.mixin.meta.name : meta.name; var inverse = inv.mixin ? inv.mixin.meta.name : inverseMeta.name; // Added for(var i = 0; i < this._localAdded.length; i++) { var columns = [direct + "_" + this._coll, inverse + '_' + rel.inverseProperty]; var vars = [tm.outIdVar("?"), tm.outIdVar("?")]; var args = [tm.entityIdToDbId(this._obj.id), tm.entityIdToDbId(this._localAdded[i].id)]; if (rel.mixin) { columns.push(direct + "_" + this._coll + "_class"); vars.push("?"); args.push(meta.name); } if (inv.mixin) { columns.push(inverse + "_" + rel.inverseProperty + "_class"); vars.push("?"); args.push(inverseMeta.name); } queries.push(["INSERT INTO " + rel.tableName + " (`" + columns.join("`, `") + "`) VALUES (" + vars.join(",") + ")", args]); } this._localAdded = []; // Removed for(var i = 0; i < this._localRemoved.length; i++) { queries.push(["DELETE FROM " + rel.tableName + " WHERE `" + direct + "_" + this._coll + "` = " + tm.outIdVar("?") + " AND `" + inverse + '_' + rel.inverseProperty + "` = " + tm.outIdVar("?"), [tm.entityIdToDbId(this._obj.id), tm.entityIdToDbId(this._localRemoved[i].id)]]); } this._localRemoved = []; return queries; }; }; if (typeof exports !== 'undefined') { exports.defaultTypeMapper = defaultTypeMapper; exports.config = config; } else { window = window || {}; window.persistence = window.persistence || persistence || {}; window.persistence.store = window.persistence.store || {}; window.persistence.store.sql = { defaultTypeMapper: defaultTypeMapper, config: config }; } ================================================ FILE: lib/persistence.store.sqlite.js ================================================ /** * This back-end depends on the node.js asynchronous SQLite driver as found on: * https://github.com/orlandov/node-sqlite * Easy install using npm: * npm install sqlite * @author Eugene Ware */ var sys = require('sys'); var sql = require('./persistence.store.sql'); var sqlite = require('sqlite'); var db, username, password; function log(o) { sys.print(sys.inspect(o) + "\n"); } exports.config = function(persistence, dbPath) { exports.getSession = function(cb) { var that = {}; cb = cb || function() { }; var conn = new sqlite.Database(); conn.open(dbPath, cb); var session = new persistence.Session(that); session.transaction = function (explicitCommit, fn) { if (typeof arguments[0] === "function") { fn = arguments[0]; explicitCommit = false; } var tx = transaction(conn); if (explicitCommit) { tx.executeSql("START TRANSACTION", null, function(){ fn(tx) }); } else fn(tx); }; session.close = function(cb) { cb = cb || function() {}; conn.close(cb); }; return session; }; function transaction(conn){ var that = {}; // TODO: add check for db opened or closed that.executeSql = function(query, args, successFn, errorFn){ function cb(err, result){ if (err) { log(err.message); that.errorHandler && that.errorHandler(err); errorFn && errorFn(null, err); return; } if (successFn) { successFn(result); } } if (persistence.debug) { sys.print(query + "\n"); args && args.length > 0 && sys.print(args.join(",") + "\n") } if (!args) { conn.execute(query, cb); } else { conn.execute(query, args, cb); } } that.commit = function(session, callback){ session.flush(that, function(){ that.executeSql("COMMIT", null, callback); }) } that.rollback = function(session, callback){ that.executeSql("ROLLBACK", null, function() { session.clean(); callback(); }); } return that; } ///////////////////////// SQLite dialect persistence.sqliteDialect = { // columns is an array of arrays, e.g. // [["id", "VARCHAR(32)", "PRIMARY KEY"], ["name", "TEXT"]] createTable: function(tableName, columns) { var tm = persistence.typeMapper; var sql = "CREATE TABLE IF NOT EXISTS `" + tableName + "` ("; var defs = []; for(var i = 0; i < columns.length; i++) { var column = columns[i]; defs.push("`" + column[0] + "` " + tm.columnType(column[1]) + (column[2] ? " " + column[2] : "")); } sql += defs.join(", "); sql += ')'; return sql; }, // columns is array of column names, e.g. // ["id"] createIndex: function(tableName, columns, options) { options = options || {}; return "CREATE "+(options.unique?"UNIQUE ":"")+"INDEX IF NOT EXISTS `" + tableName + "__" + columns.join("_") + "` ON `" + tableName + "` (" + columns.map(function(col) { return "`" + col + "`"; }).join(", ") + ")"; } }; sql.config(persistence, persistence.sqliteDialect); }; ================================================ FILE: lib/persistence.store.sqlite3.js ================================================ /** * This back-end depends on the node.js asynchronous SQLite3 driver as found on: * https://github.com/developmentseed/node-sqlite3 * Easy install using npm: * npm install sqlite3 * @author Eugene Ware * @author Jeff Kunkle * @author Joe Ferner */ var sys = require('sys'); var sql = require('./persistence.store.sql'); var sqlite = require('sqlite3'); var db, username, password; function log(o) { sys.print(sys.inspect(o) + "\n"); } exports.config = function(persistence, dbPath) { exports.getSession = function(cb) { var that = {}; cb = cb || function() { }; var conn = new sqlite.Database(dbPath, cb); var session = new persistence.Session(that); session.transaction = function (explicitCommit, fn) { if (typeof arguments[0] === "function") { fn = arguments[0]; explicitCommit = false; } var tx = transaction(conn); if (explicitCommit) { tx.executeSql("START TRANSACTION", null, function(){ fn(tx) }); } else fn(tx); }; session.close = function(cb) { cb = cb || function() {}; conn.close(cb); }; return session; }; function transaction(conn){ var that = {}; // TODO: add check for db opened or closed that.executeSql = function(query, args, successFn, errorFn){ function cb(err, result){ if (err) { log(err.message); that.errorHandler && that.errorHandler(err); errorFn && errorFn(null, err); return; } if (successFn) { successFn(result); } } if (persistence.debug) { sys.print(query + "\n"); args && args.length > 0 && sys.print(args.join(",") + "\n") } if (!args) { conn.all(query, cb); } else { conn.all(query, args, cb); } } that.commit = function(session, callback){ session.flush(that, function(){ that.executeSql("COMMIT", null, callback); }) } that.rollback = function(session, callback){ that.executeSql("ROLLBACK", null, function() { session.clean(); callback(); }); } return that; } ///////////////////////// SQLite dialect persistence.sqliteDialect = { // columns is an array of arrays, e.g. // [["id", "VARCHAR(32)", "PRIMARY KEY"], ["name", "TEXT"]] createTable: function(tableName, columns) { var tm = persistence.typeMapper; var sql = "CREATE TABLE IF NOT EXISTS `" + tableName + "` ("; var defs = []; for(var i = 0; i < columns.length; i++) { var column = columns[i]; defs.push("`" + column[0] + "` " + tm.columnType(column[1]) + (column[2] ? " " + column[2] : "")); } sql += defs.join(", "); sql += ')'; return sql; }, // columns is array of column names, e.g. // ["id"] createIndex: function(tableName, columns, options) { options = options || {}; return "CREATE "+(options.unique?"UNIQUE ":"")+"INDEX IF NOT EXISTS `" + tableName + "__" + columns.join("_") + "` ON `" + tableName + "` (" + columns.map(function(col) { return "`" + col + "`"; }).join(", ") + ")"; } }; sql.config(persistence, persistence.sqliteDialect); }; ================================================ FILE: lib/persistence.store.titanium.js ================================================ try { if(!window) { window = {}; //exports.console = console; } } catch(e) { window = {}; exports.console = console; } var persistence = (window && window.persistence) ? window.persistence : {}; if(!persistence.store) { persistence.store = {}; } persistence.store.titanium = {}; persistence.store.titanium.config = function(persistence, dbname) { var conn = null; /** * Create a transaction * * @param callback, * the callback function to be invoked when the transaction * starts, taking the transaction object as argument */ persistence.transaction = function (callback) { if(!conn) { throw new Error("No ongoing database connection, please connect first."); } else { conn.transaction(callback); } }; ////////// Low-level database interface, abstracting from HTML5 and Gears databases \\\\ persistence.db = persistence.db || {}; persistence.db.conn = null; persistence.db.titanium = {}; persistence.db.titanium.connect = function (dbname) { var that = {}; var conn = Titanium.Database.open(dbname); that.transaction = function (fn) { fn(persistence.db.titanium.transaction(conn)); }; return that; }; persistence.db.titanium.transaction = function (conn) { var that = {}; that.executeSql = function (query, args, successFn, errorFn) { if(persistence.debug) { console.log(query, args); } try { var executeVarArgs = [query]; if (args) { executeVarArgs = executeVarArgs.concat(args); }; var rs = Function.apply.call(conn.execute, conn, executeVarArgs); if (successFn) { var results = []; if (rs) { while (rs.isValidRow()) { var result = {}; for ( var i = 0; i < rs.fieldCount(); i++) { result[rs.fieldName(i)] = rs.field(i); } results.push(result); rs.next(); } rs.close(); }; successFn(results); } } catch(e) { if (errorFn) { errorFn(null, e); }; } }; return that; }; ///////////////////////// SQLite dialect persistence.store.titanium.sqliteDialect = { // columns is an array of arrays, e.g. // [["id", "VARCHAR(32)", "PRIMARY KEY"], ["name", "TEXT"]] createTable: function(tableName, columns) { var tm = persistence.typeMapper; var sql = "CREATE TABLE IF NOT EXISTS `" + tableName + "` ("; var defs = []; for(var i = 0; i < columns.length; i++) { var column = columns[i]; defs.push("`" + column[0] + "` " + tm.columnType(column[1]) + (column[2] ? " " + column[2] : "")); } sql += defs.join(", "); sql += ')'; return sql; }, // columns is array of column names, e.g. // ["id"] createIndex: function(tableName, columns, options) { options = options || {}; return "CREATE "+(options.unique?"UNIQUE ":"")+"INDEX IF NOT EXISTS `" + tableName + "__" + columns.join("_") + "` ON `" + tableName + "` (" + columns.map(function(col) { return "`" + col + "`"; }).join(", ") + ")"; }, typeMapper: { idType: persistence.store.sql.defaultTypeMapper.idType, classNameType: persistence.store.sql.defaultTypeMapper.classNameType, inVar: persistence.store.sql.defaultTypeMapper.inVar, outVar: persistence.store.sql.defaultTypeMapper.outVar, outId: persistence.store.sql.defaultTypeMapper.outId, inIdVar: persistence.store.sql.defaultTypeMapper.inIdVar, outIdVar: persistence.store.sql.defaultTypeMapper.outIdVar, entityIdToDbId: persistence.store.sql.defaultTypeMapper.entityIdToDbId, zeroPaddingMap: ['0000000000000000', '000000000000000', '00000000000000', '0000000000000', '000000000000', '00000000000', '0000000000', '000000000', '00000000', '0000000', '000000', '00000', '0000', '000', '00', '0'], zeroPadded: function(val) { var result = val.toString(); if (result.length < 16) { return persistence.store.titanium.sqliteDialect.typeMapper.zeroPaddingMap[result.length] + result; } else { return result; }; }, columnType: function(type) { if (type === 'BIGINT') { return 'TEXT'; } else { return persistence.store.sql.defaultTypeMapper.columnType(type); }; }, dbValToEntityVal: function(val, type){ if (val === null || val === undefined) { return val; } else if (type === 'BIGIN') { return parseInt(val); } else { return persistence.store.sql.defaultTypeMapper.dbValToEntityVal(val, type); } }, entityValToDbVal: function(val, type){ if (val === undefined || val === null) { return null; } else if (type === 'BIGINT') { return persistence.store.titanium.sqliteDialect.typeMapper.zeroPadded(val); } else { return persistence.store.sql.defaultTypeMapper.entityValToDbVal(val, type); }; } } }; // Configure persistence for generic sql persistence, using sqliteDialect persistence.store.sql.config(persistence, persistence.store.titanium.sqliteDialect); // Make the connection conn = persistence.db.titanium.connect(dbname); if(!conn) { throw new Error("No supported database found"); } }; try { exports.persistence = persistence; } catch(e) {} ================================================ FILE: lib/persistence.store.websql.js ================================================ try { if(!window) { window = {}; //exports.console = console; } } catch(e) { window = {}; exports.console = console; } var persistence = (window && window.persistence) ? window.persistence : {}; if(!persistence.store) { persistence.store = {}; } persistence.store.websql = {}; persistence.store.websql.config = function(persistence, dbname, description, size) { var conn = null; /** * Create a transaction * * @param callback, * the callback function to be invoked when the transaction * starts, taking the transaction object as argument */ persistence.transaction = function (callback) { if(!conn) { throw new Error("No ongoing database connection, please connect first."); } else { conn.transaction(callback); } }; ////////// Low-level database interface, abstracting from HTML5 and Gears databases \\\\ persistence.db = persistence.db || {}; persistence.db.implementation = "unsupported"; persistence.db.conn = null; // window object does not exist on Qt Declarative UI (http://doc.trolltech.org/4.7-snapshot/declarativeui.html) if (window && window.openDatabase) { persistence.db.implementation = "html5"; } else if (window && window.google && google.gears) { persistence.db.implementation = "gears"; } else { try { if (openDatabaseSync) { // TODO: find a browser that implements openDatabaseSync and check out if // it is attached to the window or some other object persistence.db.implementation = "html5-sync"; } } catch(e) { } } persistence.db.html5 = {}; persistence.db.html5.connect = function (dbname, description, size) { var that = {}; var conn = openDatabase(dbname, '1.0', description, size); that.transaction = function (fn) { return conn.transaction(function (sqlt) { return fn(persistence.db.html5.transaction(sqlt)); }); }; return that; }; persistence.db.html5.transaction = function (t) { var that = {}; that.executeSql = function (query, args, successFn, errorFn) { if(persistence.debug) { console.log(query, args); } t.executeSql(query, args, function (_, result) { if (successFn) { var results = []; for ( var i = 0; i < result.rows.length; i++) { results.push(result.rows.item(i)); } successFn(results); } }, errorFn); }; return that; }; persistence.db.html5Sync = {}; persistence.db.html5Sync.connect = function (dbname, description, size) { var that = {}; var conn = openDatabaseSync(dbname, '1.0', description, size); that.transaction = function (fn) { return conn.transaction(function (sqlt) { return fn(persistence.db.html5Sync.transaction(sqlt)); }); }; return that; }; persistence.db.html5Sync.transaction = function (t) { var that = {}; that.executeSql = function (query, args, successFn, errorFn) { if (args == null) args = []; if(persistence.debug) { console.log(query, args); } var result = t.executeSql(query, args); if (result) { if (successFn) { var results = []; for ( var i = 0; i < result.rows.length; i++) { results.push(result.rows.item(i)); } successFn(results); } } }; return that; }; persistence.db.gears = {}; persistence.db.gears.connect = function (dbname) { var that = {}; var conn = google.gears.factory.create('beta.database'); conn.open(dbname); that.transaction = function (fn) { fn(persistence.db.gears.transaction(conn)); }; return that; }; persistence.db.gears.transaction = function (conn) { var that = {}; that.executeSql = function (query, args, successFn, errorFn) { if(persistence.debug) { console.log(query, args); } var rs = conn.execute(query, args); if (successFn) { var results = []; while (rs.isValidRow()) { var result = {}; for ( var i = 0; i < rs.fieldCount(); i++) { result[rs.fieldName(i)] = rs.field(i); } results.push(result); rs.next(); } successFn(results); } }; return that; }; persistence.db.connect = function (dbname, description, size) { if (persistence.db.implementation == "html5") { return persistence.db.html5.connect(dbname, description, size); } else if (persistence.db.implementation == "html5-sync") { return persistence.db.html5Sync.connect(dbname, description, size); } else if (persistence.db.implementation == "gears") { return persistence.db.gears.connect(dbname); } }; ///////////////////////// SQLite dialect persistence.store.websql.sqliteDialect = { // columns is an array of arrays, e.g. // [["id", "VARCHAR(32)", "PRIMARY KEY"], ["name", "TEXT"]] createTable: function(tableName, columns) { var tm = persistence.typeMapper; var sql = "CREATE TABLE IF NOT EXISTS `" + tableName + "` ("; var defs = []; for(var i = 0; i < columns.length; i++) { var column = columns[i]; defs.push("`" + column[0] + "` " + tm.columnType(column[1]) + (column[2] ? " " + column[2] : "")); } sql += defs.join(", "); sql += ')'; return sql; }, // columns is array of column names, e.g. // ["id"] createIndex: function(tableName, columns, options) { options = options || {}; return "CREATE "+(options.unique?"UNIQUE ":"")+"INDEX IF NOT EXISTS `" + tableName + "__" + columns.join("_") + "` ON `" + tableName + "` (" + columns.map(function(col) { return "`" + col + "`"; }).join(", ") + ")"; } }; // Configure persistence for generic sql persistence, using sqliteDialect persistence.store.sql.config(persistence, persistence.store.websql.sqliteDialect); // Make the connection conn = persistence.db.connect(dbname, description, size); if(!conn) { throw new Error("No supported database found in this browser."); } }; try { exports.persistence = persistence; } catch(e) {} ================================================ FILE: lib/persistence.sync.js ================================================ /** * @license * Copyright (c) 2010 Zef Hemel * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ if(!window.persistence) { // persistence.js not loaded! throw new Error("persistence.js should be loaded before persistence.sync.js"); } persistence.sync = {}; persistence.sync.getJSON = function(uri, callback, errorCallback) { var xmlHttp = new XMLHttpRequest(); xmlHttp.open("GET", uri, true); xmlHttp.send(); xmlHttp.onreadystatechange = function() { if(xmlHttp.readyState==4) { if(xmlHttp.status==200) { callback(JSON.parse(xmlHttp.responseText)); } else if(typeof errorCallback === 'function') { errorCallback(xmlHttp); } } }; }; persistence.sync.postJSON = function(uri, data, callback) { var xmlHttp = new XMLHttpRequest(); xmlHttp.open("POST", uri, true); xmlHttp.setRequestHeader('Content-Type', 'application/json'); xmlHttp.send(data); xmlHttp.onreadystatechange = function() { if(xmlHttp.readyState==4 && xmlHttp.status==200) { callback(JSON.parse(xmlHttp.responseText)); } }; }; (function() { var argspec = persistence.argspec; persistence.sync.Sync = persistence.define('_Sync', { entity: "VARCHAR(255)", localDate: "BIGINT", serverDate: "BIGINT", serverPushDate: "BIGINT" }); persistence.sync.RemovedObject = persistence.define('_SyncRemovedObject', { entity: "VARCHAR(255)", objectId: "VARCHAR(32)" }); function getEpoch(date) { return date.getTime(); } persistence.sync.preferLocalConflictHandler = function(conflicts, updatesToPush, callback) { conflicts.forEach(function(conflict) { var update = {id: conflict.local.id}; conflict.properties.forEach(function(p) { update[p] = conflict.local._data[p]; }); updatesToPush.push(update); }); callback(); }; persistence.sync.preferRemoteConflictHandler = function(conflicts, updatesToPush, callback) { conflicts.forEach(function(conflict) { conflict.properties.forEach(function(p) { conflict.local[p] = conflict.remote[p]; }); }); persistence.flush(callback); }; function encodeUrlObj(obj) { var parts = []; for(var k in obj) { if(obj.hasOwnProperty(k)) { parts.push(encodeURI(k)+"="+encodeURI(obj[k])); } } return "?" + parts.join("&"); } /** * The main caching and updating function, would be nice to refactor this */ function cacheAndFindUpdates(session, Entity, objects, lastLocalSyncTime, lastServerPushTime, conflictCallback, callback) { var ids = []; var lookupTbl = {}; var conflicts = []; var updatesToPush = []; var meta = Entity.meta; var fieldSpec = meta.fields; var objectsToRemove = []; objects.forEach(function(item) { if(item._removed) { // special marker objectsToRemove.push(item.id); } else { ids.push(item.id); lookupTbl[item.id] = item; } }); // Step 1: Look at local versions of remotely updated entities var existingItems = [], groupedIds = []; for (var i=0,l=Math.floor((ids.length/100)+1);i 0) { conflicts.push({local: localItem, remote: remoteItem, properties: conflictingProperties}); } }); // Step 2: Remove all remotely removed objects var groupedObjectsToRemove = []; for (var i=0,l=Math.floor((objectsToRemove.length/100)+1);i", lastLocalSyncTime).list(function(allNewItems) { var newItems = []; for (var i=0,l=allNewItems.length;i 1000000000000) { // it's in milliseconds return new Date(value); } else { return new Date(value * 1000); } break; default: return value; } } else { return value; } } function getEpoch(date) { return date.getTime(); //Math.round(date.getTime()/1000); } function entityValToJson(value, type) { if(type) { switch(type) { case 'DATE': return Math.round(date.getTime() / 1000); break; default: return value; } } else { return value; } } exports.pushUpdates = function(session, tx, Entity, since, callback) { var queryCollection; if(typeof(Entity) == "function"){ queryCollection = Entity.all(session); }else if(typeof(Entity) == "object"){ queryCollection = Entity; } queryCollection.filter("_lastChange", ">", since).list(tx, function(items) { var results = []; var meta = Entity.meta; var fieldSpec = meta.fields; for(var i = 0; i < items.length; i++) { var itemData = items[i]._data; var item = {id: items[i].id}; for(var p in fieldSpec) { if(fieldSpec.hasOwnProperty(p)) { item[p] = entityValToJson(itemData[p], fieldSpec[p]); } } for(var p in meta.hasOne) { if(meta.hasOne.hasOwnProperty(p)) { item[p] = entityValToJson(itemData[p]); } } results.push(item); } if(since>0){ session.sync.RemovedObject.all(session).filter("entity", "=", meta.name).filter("date", ">", since).list(tx, function(items) { for(var i = 0; i < items.length; i++) { results.push({id: items[i].id, _removed: true}); } callback({now: getEpoch(new Date()), updates: results}); }); }else{ callback({now: getEpoch(new Date()), updates: results}); } }); }; exports.receiveUpdates = function(session, tx, Entity, updates, callback) { var allIds = []; var updateLookup = {}; var now = getEpoch(new Date()); var removedIds = []; for(var i = 0; i < updates.length; i++) { if(updates[i]._removed) { // removed removedIds.push(updates[i].id); } else { allIds.push(updates[i].id); updateLookup[updates[i].id] = updates[i]; } } Entity.all(session).filter("id", "in", removedIds).destroyAll(function() { removedIds.forEach(function(id) { session.add(new session.sync.RemovedObject({objectId: id, entity: Entity.meta.name, date: now})); }); Entity.all(session).filter("id", "in", allIds).list(tx, function(existingItems) { var fieldSpec = Entity.meta.fields; for(var i = 0; i < existingItems.length; i++) { var existingItem = existingItems[i]; var updateItem = updateLookup[existingItem.id]; for(var p in updateItem) { if(updateItem.hasOwnProperty(p)) { if(updateItem[p] !== existingItem._data[p]) { existingItem[p] = jsonToEntityVal(updateItem[p], fieldSpec[p]); existingItem._lastChange = now; } } } delete updateLookup[existingItem.id]; } // All new items for(var id in updateLookup) { if(updateLookup.hasOwnProperty(id)) { var update = updateLookup[id]; delete update.id; var newItem = new Entity(session); newItem.id = id; for(var p in update) { if(update.hasOwnProperty(p)) { newItem[p] = jsonToEntityVal(update[p], fieldSpec[p]); } } newItem._lastChange = now; session.add(newItem); } } session.flush(tx, function() { callback({status: 'ok', now: now}); }); }); }); }; exports.config = function(persistence) { persistence.sync = persistence.sync || {}; persistence.sync.RemovedObject = persistence.define('_SyncRemovedObject', { entity: "VARCHAR(255)", objectId: "VARCHAR(32)", date: "BIGINT" }); persistence.entityDecoratorHooks.push(function(Entity) { /** * Declares an entity to be tracked for changes */ Entity.enableSync = function() { Entity.meta.enableSync = true; Entity.meta.fields['_lastChange'] = 'BIGINT'; }; }); /** * Resets _lastChange property if the object has dirty project (i.e. the object has changed) */ persistence.flushHooks.push(function(session, tx, callback) { var queries = []; for (var id in session.getTrackedObjects()) { if (session.getTrackedObjects().hasOwnProperty(id)) { var obj = session.getTrackedObjects()[id]; var meta = persistence.getEntityMeta()[obj._type]; if(meta.enableSync) { var isDirty = obj._new; var lastChangeIsDirty = false; for ( var p in obj._dirtyProperties) { if (obj._dirtyProperties.hasOwnProperty(p)) { isDirty = true; } if(p === '_lastChange') { lastChangeIsDirty = true; } } if(isDirty && !lastChangeIsDirty) { // Only set _lastChange if it has not been set manually (during a sync) obj._lastChange = getEpoch(new Date()); } } } } session.objectsRemoved.forEach(function(rec) { var meta = session.getMeta(rec.entity); if(meta.enableSync) { session.add(new persistence.sync.RemovedObject({entity: rec.entity, objectId: rec.id, date: getEpoch(new Date())})); } }); callback(); }); }; ================================================ FILE: lib/persistence.sync.server.php ================================================ * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * * USAGE: * Before this code can be used to persist data in the database the file persistence.sync.server.php.sql must be run * * This is NOT intended to be used without modification as it implements only the mimimal set of functionality to * get persistence working. It does not include any kind of security model for example. */ /** * Requires that the database schema be setup by running: * * persistence.sync.server.php.sql */ class PersistenceDB { private $db; private $persistence_table; function __construct(PDO $db, $persistence_table) { $this->db = $db; $this->persistence_table = $persistence_table; } public function getObjectChanges($bucket, $since) { $statement = $this->db->prepare("SELECT content FROM {$this->persistence_table} WHERE bucket=:bucket AND lastUpdated > :since"); $statement->execute(array(':bucket' => $bucket, ':since' => $since)); $changes = array(); foreach ($statement->fetchAll(PDO::FETCH_COLUMN) as $content) { $change = json_decode($content); // Don't bother sending removed items to fresh clients if ($since != 0 || !isset($change->_removed)) { $changes[] = $change; } } return $changes; } public function applyObjectChanges($bucket, $now, array $changes) { $statement = $this->db->prepare(" INSERT INTO {$this->persistence_table} (id, bucket, lastUpdated, content) VALUES (:id, :bucket, :lastUpdated, :content) ON DUPLICATE KEY UPDATE lastUpdated=:lastUpdated, content=:content"); foreach ($changes as $change) { $change->_lastChanged = $now; $statement->execute(array(':id' => $change->id, ':bucket' => $bucket, ':lastUpdated' => $now, ':content' => json_encode($change))); } } } $db = new PersistenceDB(new PDO('mysql:host=localhost;dbname=persistencejs', 'root', ''), 'persistencejs_objects'); function http_400() { header($_SERVER['SERVER_PROTOCOL'] . ' 400 Invalid Request'); exit(0); } header('Content-Type: applicatin/json'); switch (strtoupper($_SERVER['REQUEST_METHOD'])) { case 'GET': if (!isset($_GET['bucket']) || !isset($_GET['since'])) http_400(); $bucket = $_GET['bucket']; $since = isset($_GET['since']) ? $_GET['since'] : 0; $changes = $db->getObjectChanges($bucket, $since); echo json_encode(array('now' => round(microtime(true) * 1000), "updates" => $changes)); break; case 'POST': $body = file_get_contents('php://input'); $changes = json_decode($body); $now = floor(microtime(true)*1000); $db->applyObjectChanges($bucket, $now, $changes); echo json_encode(array('now' => $now, "status" => 'ok')); break; default: header($_SERVER['SERVER_PROTOCOL'] . ' 405 Invalid Request'); } ================================================ FILE: lib/persistence.sync.server.php.sql ================================================ -- This table must exist in the database for synchronization with the php version of the server to run. -- This is definitely not an efficient, but it's about as simple as it gets CREATE TABLE `persistencejs_objects` ( `id` char(32) NOT NULL, `bucket` varchar(50) NOT NULL, `lastUpdated` bigint(20) NOT NULL, `content` text NOT NULL, PRIMARY KEY (`id`), KEY `ix_objects_lastUpdated` (`lastUpdated`), KEY `ix_bucket` (`bucket`) ) DEFAULT CHARSET=utf8 ================================================ FILE: package.json ================================================ { "name": "persistencejs", "version": "0.3.0", "engine": "node >=0.2.0", "author": "Zef Hemel", "directories": {"lib": "./lib"} } ================================================ FILE: test/appengine/test.js ================================================ // Run with RingoJS: http://ringojs.org // Set path below to AppEngine Java SDK path var appEngineSdkPath = '/Users/zef/Software/appengine-java-sdk'; addToClasspath(appEngineSdkPath + "/lib/impl/appengine-api-stubs.jar"); addToClasspath(appEngineSdkPath + "/lib/impl/appengine-api.jar"); addToClasspath(appEngineSdkPath + "/lib/impl/appengine-api-labs.jar"); addToClasspath(appEngineSdkPath + "/lib/impl/appengine-api-stubs.jar"); addToClasspath(appEngineSdkPath + "/lib/testing/appengine-testing.jar"); var persistence = require('../../lib/persistence').persistence; var persistenceStore = require('../../lib/persistence.store.appengine'); var assert = require("assert"); var JLocalDatastoreServiceTestConfig = com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; var JLocalServiceTestHelper = com.google.appengine.tools.development.testing.LocalServiceTestHelper; var helper = new JLocalServiceTestHelper(new JLocalDatastoreServiceTestConfig()); persistenceStore.config(persistence); var Project = persistence.define('Project', { name: "TEXT" }); var Task = persistence.define('Task', { name: "TEXT", done: "BOOL", counter: "INT", dateAdded: "DATE", metaData: "JSON" }); var Tag = persistence.define('Tag', { name: "TEXT" }); Task.hasMany('tags', Tag, 'tasks'); Tag.hasMany('tasks', Task, 'tags'); Task.index('dateAdded'); Project.hasMany('tasks', Task, 'project'); function intFilterTests(session, coll, callback) { for(var i = 0; i < 25; i++) { var t = new Task(session, {name: "Task " + i, done: false}); t.counter = i; coll.add(t); } coll.list(function(results) { assert.equal(results.length, 25, "Count items in collection"); coll.filter("counter", ">", 10).list(function(results) { assert.equal(results.length, 14, "> filter test"); coll.filter("counter", "in", [0, 1, 2]).list(function(results) { assert.equal(results.length, 3, "'in' filter test"); coll.filter("counter", "!=", 0).list(function(results) { assert.equal(results.length, 24, "'!=' filter test"); callback(); }); }); }); }); } function textFilterTests(session, coll, callback) { var alphabet = 'abcdefghijklmnopqrstufwxyz'; for(var i = 0; i <= 25; i++) { var t = new Task(session, {name: alphabet[i]}); coll.add(t); } coll.list(function(results) { assert.equal(results.length, 26, "Count items in collection"); coll.filter("name", "=", 'a').list(function(results) { assert.equal(results.length, 1, "= filter test"); coll.filter("name", "!=", 'a').list(function(results) { assert.equal(results.length, 25, "!= filter test"); coll.filter("name", ">", 'm').list(function(results) { assert.equal(results.length, 12, "> filter test"); coll.filter("name", "in", ["a", "b"]).list(function(results) { assert.equal(results.length, 2, "'in' filter test"); callback(); }); }); }); }); }); } function boolFilterTests(session, coll, callback) { for(var i = 0; i < 24; i++) { var t = new Task(session, {name: "Task " + i, done: i % 2 === 0}); coll.add(t); } coll.list(function(results) { assert.equal(results.length, 24, "Count items in collection"); coll.filter("done", "=", true).list(function(results) { assert.equal(results.length, 12, "= filter test"); coll.filter("done", "=", false).list(function(results) { assert.equal(results.length, 12, "= filter test"); coll.filter("done", "!=", true).list(function(results) { assert.equal(results.length, 12, "'!=' filter test"); coll.filter("done", "!=", false).list(function(results) { assert.equal(results.length, 12, "'!=' filter test"); callback(); }); }); }); }); }); } function dateFilterTests(session, coll, callback) { var now = new Date(); function dateInDays(n) { var newDate = new Date(now.getTime()); newDate.setDate(newDate.getDate()+n); return newDate; } for(var i = 0; i < 24; i++) { var t = new Task(session, {name: "Task " + i, dateAdded: dateInDays(i)}); coll.add(t); } coll.list(function(results) { assert.equal(results.length, 24, "Count items in collection"); coll.filter("dateAdded", "=", dateInDays(1)).list(function(results) { assert.equal(results.length, 1, "= filter test"); coll.filter("dateAdded", "!=", dateInDays(1)).list(function(results) { assert.equal(results.length, 23, "!= filter test"); coll.filter("dateAdded", ">", dateInDays(12)).list(function(results) { assert.equal(results.length, 11, "> filter test"); callback(); }); }) }); }); } function intOrderTests(session, coll, callback) { var tasks = []; for(var i = 0; i < 24; i++) { var t = new Task(session, {name: "Task " + i, counter: i}); tasks.push(t); coll.add(t); } coll.order('counter', true).list(function(results) { for(var i = 0; i < 24; i++) { assert.equal(results[i].id, tasks[i].id, "order check, ascending"); } tasks.reverse(); coll.order('counter', false).list(function(results) { for(var i = 0; i < 24; i++) { assert.equal(results[i].id, tasks[i].id, "order check, descending"); } callback(); }); }); } function dateOrderTests(session, coll, callback) { var now = new Date(); function dateInDays(n) { var newDate = new Date(now.getTime()); newDate.setDate(newDate.getDate()+n); return newDate; } var tasks = []; for(var i = 0; i < 24; i++) { var t = new Task(session, {name: "Task " + i, dateAdded: dateInDays(i)}); tasks.push(t); coll.add(t); } coll.order('dateAdded', true).list(function(results) { for(var i = 0; i < 24; i++) { assert.equal(results[i].id, tasks[i].id, "order check, ascending"); } tasks.reverse(); coll.order('dateAdded', false).list(function(results) { for(var i = 0; i < 24; i++) { assert.equal(results[i].id, tasks[i].id, "order check, descending"); } callback(); }); }); } function collectionLimitTests(session, coll, callback) { var tasks = []; for(var i = 0; i < 24; i++) { var t = new Task(session, {name: "Task " + i, counter: i}); tasks.push(t); coll.add(t); } coll.order("counter", true).limit(5).list(function(results) { assert.equal(results.length, 5, "Result length check"); for(var i = 0; i < 5; i++) { assert.equal(results[i].id, tasks[i].id, "limit check"); } callback(); }); } function collectionSkipTests(session, coll, callback) { var tasks = []; for(var i = 0; i < 24; i++) { var t = new Task(session, {name: "Task " + i, counter: i}); tasks.push(t); coll.add(t); } coll.order("counter", true).skip(5).limit(5).list(function(results) { assert.equal(results.length, 5, "Result length check"); for(var i = 5; i < 10; i++) { assert.equal(results[i-5].id, tasks[i].id, "skip check"); } callback(); }); } var tests = { testBasic: function() { helper.setUp(); var session = persistenceStore.getSession(); for(var i = 0; i < 10; i++) { var t = new Task(session, {name: "user " + i, done: i % 2 === 0, dateAdded: new Date()}); session.add(t); } Task.all(session).filter("done", "=", true).order('dateAdded', false).list(function(results) { assert.equal(results.length, 5, "Correct number of completed tasks"); session.close(); helper.tearDown(); }); }, testOneToMany: function() { helper.setUp(); var session = persistenceStore.getSession(); session.schemaSync(function() { var proj = new Project(session); proj.name = "Main"; for(var i = 0; i < 10; i++) { var t = new Task(session, {name: "user " + i, done: i % 2 === 0, dateAdded: new Date()}); proj.tasks.add(t); } for(var i = 0; i < 10; i++) { var t = new Task(session, {name: "non-proj user " + i, done: i % 2 === 0, dateAdded: new Date()}); session.add(t); } proj.tasks.filter("done", "=", true).list(function(results) { assert.equal(results.length, 5, "Correct number of completed tasks"); session.close(); helper.tearDown(); }); }); }, testFetch: function() { helper.setUp(); var session = persistenceStore.getSession(); var proj = new Project(session); proj.name = "Main"; for(var i = 0; i < 10; i++) { var t = new Task(session, {name: "user " + i, done: i % 2 === 0, dateAdded: new Date()}); proj.tasks.add(t); } session.flush(function() { session.clean(); Task.all(session).list(function(results) { results.forEach(function(r) { r.fetch('project', function(p) { assert.equal(p.name, "Main", "Correct number of completed tasks"); }); }); session.close(); helper.tearDown(); }); }); }, testDatabaseIntFilter: function() { helper.setUp(); var session = persistenceStore.getSession(); for(var i = 0; i < 25; i++) { var t = new Task(session, {name: "Root task " + i, done: false}); t.counter = i; session.add(t); } var p = new Project(session, {name: "My project"}); session.add(p); intFilterTests(session, p.tasks, function() { session.close(); helper.tearDown(); }); }, testDatabaseLocalIntFilter: function() { helper.setUp(); var session = persistenceStore.getSession(); var coll = new persistence.LocalQueryCollection(); intFilterTests(session, coll, function() { session.close(); helper.tearDown(); }); }, testDatabaseTextFilter: function() { helper.setUp(); var session = persistenceStore.getSession(); var alphabet = 'abcdefghijklmnopqrstufwxyz'; for(var i = 0; i <= 25; i++) { var t = new Task(session, {name: alphabet[i]}); session.add(t); } var p = new Project(session, {name: "My project"}); session.add(p); textFilterTests(session, p.tasks, function() { session.close(); helper.tearDown(); }); }, testDatabaseLocalTextFilter: function() { helper.setUp(); var session = persistenceStore.getSession(); var coll = new persistence.LocalQueryCollection(); textFilterTests(session, coll, function() { session.close(); helper.tearDown(); }); }, testDatabaseBoolFilter: function() { helper.setUp(); var session = persistenceStore.getSession(); for(var i = 0; i < 25; i++) { var t = new Task(session, {name: "Root task " + i, done: false}); t.counter = i; session.add(t); } var p = new Project(session, {name: "My project"}); session.add(p); boolFilterTests(session, p.tasks, function() { session.close(); helper.tearDown(); }); }, testDatabaseDateFilter: function() { helper.setUp(); var session = persistenceStore.getSession(); var p = new Project(session, {name: "My project"}); session.add(p); dateFilterTests(session, p.tasks, function() { session.close(); helper.tearDown(); }); }, testDatabaseIntOrder: function() { helper.setUp(); var session = persistenceStore.getSession(); var p = new Project(session, {name: "My project"}); session.add(p); intOrderTests(session, p.tasks, function() { session.close(); helper.tearDown(); }); }, testDatabaseDateOrder: function() { helper.setUp(); var session = persistenceStore.getSession(); var p = new Project(session, {name: "My project"}); session.add(p); dateOrderTests(session, p.tasks, function() { session.close(); helper.tearDown(); }); }, testCollectionLimit: function() { helper.setUp(); var session = persistenceStore.getSession(); collectionLimitTests(session, Task.all(session), function() { session.close(); helper.tearDown(); }); }, testCollectionSkip: function() { helper.setUp(); var session = persistenceStore.getSession(); collectionSkipTests(session, Task.all(session), function() { session.close(); helper.tearDown(); }); }, testJSON: function() { helper.setUp(); var session = persistenceStore.getSession(); var p = new Project(session, {name: 'A project'}); for(var i = 0; i < 10; i++) { p.tasks.add(new Task(session, {name: "Some task " + i})); } p.selectJSON(['id', 'name', 'tasks.[id,name]'], function(json) { assert.equal(json.id, p.id, "id"); assert.equal(json.name, p.name, "name"); assert.equal(json.tasks.length, 10, "n tasks"); session.close(); helper.tearDown(); }); } }; require('test').run(tests); java.lang.System.exit(0); ================================================ FILE: test/browser/qunit/jquery.js ================================================ /*! * jQuery JavaScript Library v1.4.2 * http://jquery.com/ * * Copyright 2010, John Resig * Dual licensed under the MIT or GPL Version 2 licenses. * http://jquery.org/license * * Includes Sizzle.js * http://sizzlejs.com/ * Copyright 2010, The Dojo Foundation * Released under the MIT, BSD, and GPL Licenses. * * Date: Sat Feb 13 22:33:48 2010 -0500 */ (function(A,w){function ma(){if(!c.isReady){try{s.documentElement.doScroll("left")}catch(a){setTimeout(ma,1);return}c.ready()}}function Qa(a,b){b.src?c.ajax({url:b.src,async:false,dataType:"script"}):c.globalEval(b.text||b.textContent||b.innerHTML||"");b.parentNode&&b.parentNode.removeChild(b)}function X(a,b,d,f,e,j){var i=a.length;if(typeof b==="object"){for(var o in b)X(a,o,b[o],f,e,d);return a}if(d!==w){f=!j&&f&&c.isFunction(d);for(o=0;o)[^>]*$|^#([\w-]+)$/,Ua=/^.[^:#\[\.,]*$/,Va=/\S/, Wa=/^(\s|\u00A0)+|(\s|\u00A0)+$/g,Xa=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,P=navigator.userAgent,xa=false,Q=[],L,$=Object.prototype.toString,aa=Object.prototype.hasOwnProperty,ba=Array.prototype.push,R=Array.prototype.slice,ya=Array.prototype.indexOf;c.fn=c.prototype={init:function(a,b){var d,f;if(!a)return this;if(a.nodeType){this.context=this[0]=a;this.length=1;return this}if(a==="body"&&!b){this.context=s;this[0]=s.body;this.selector="body";this.length=1;return this}if(typeof a==="string")if((d=Ta.exec(a))&& (d[1]||!b))if(d[1]){f=b?b.ownerDocument||b:s;if(a=Xa.exec(a))if(c.isPlainObject(b)){a=[s.createElement(a[1])];c.fn.attr.call(a,b,true)}else a=[f.createElement(a[1])];else{a=sa([d[1]],[f]);a=(a.cacheable?a.fragment.cloneNode(true):a.fragment).childNodes}return c.merge(this,a)}else{if(b=s.getElementById(d[2])){if(b.id!==d[2])return T.find(a);this.length=1;this[0]=b}this.context=s;this.selector=a;return this}else if(!b&&/^\w+$/.test(a)){this.selector=a;this.context=s;a=s.getElementsByTagName(a);return c.merge(this, a)}else return!b||b.jquery?(b||T).find(a):c(b).find(a);else if(c.isFunction(a))return T.ready(a);if(a.selector!==w){this.selector=a.selector;this.context=a.context}return c.makeArray(a,this)},selector:"",jquery:"1.4.2",length:0,size:function(){return this.length},toArray:function(){return R.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this.slice(a)[0]:this[a]},pushStack:function(a,b,d){var f=c();c.isArray(a)?ba.apply(f,a):c.merge(f,a);f.prevObject=this;f.context=this.context;if(b=== "find")f.selector=this.selector+(this.selector?" ":"")+d;else if(b)f.selector=this.selector+"."+b+"("+d+")";return f},each:function(a,b){return c.each(this,a,b)},ready:function(a){c.bindReady();if(c.isReady)a.call(s,c);else Q&&Q.push(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(R.apply(this,arguments),"slice",R.call(arguments).join(","))},map:function(a){return this.pushStack(c.map(this, function(b,d){return a.call(b,d,b)}))},end:function(){return this.prevObject||c(null)},push:ba,sort:[].sort,splice:[].splice};c.fn.init.prototype=c.fn;c.extend=c.fn.extend=function(){var a=arguments[0]||{},b=1,d=arguments.length,f=false,e,j,i,o;if(typeof a==="boolean"){f=a;a=arguments[1]||{};b=2}if(typeof a!=="object"&&!c.isFunction(a))a={};if(d===b){a=this;--b}for(;b
a"; var e=d.getElementsByTagName("*"),j=d.getElementsByTagName("a")[0];if(!(!e||!e.length||!j)){c.support={leadingWhitespace:d.firstChild.nodeType===3,tbody:!d.getElementsByTagName("tbody").length,htmlSerialize:!!d.getElementsByTagName("link").length,style:/red/.test(j.getAttribute("style")),hrefNormalized:j.getAttribute("href")==="/a",opacity:/^0.55$/.test(j.style.opacity),cssFloat:!!j.style.cssFloat,checkOn:d.getElementsByTagName("input")[0].value==="on",optSelected:s.createElement("select").appendChild(s.createElement("option")).selected, parentNode:d.removeChild(d.appendChild(s.createElement("div"))).parentNode===null,deleteExpando:true,checkClone:false,scriptEval:false,noCloneEvent:true,boxModel:null};b.type="text/javascript";try{b.appendChild(s.createTextNode("window."+f+"=1;"))}catch(i){}a.insertBefore(b,a.firstChild);if(A[f]){c.support.scriptEval=true;delete A[f]}try{delete b.test}catch(o){c.support.deleteExpando=false}a.removeChild(b);if(d.attachEvent&&d.fireEvent){d.attachEvent("onclick",function k(){c.support.noCloneEvent= false;d.detachEvent("onclick",k)});d.cloneNode(true).fireEvent("onclick")}d=s.createElement("div");d.innerHTML="";a=s.createDocumentFragment();a.appendChild(d.firstChild);c.support.checkClone=a.cloneNode(true).cloneNode(true).lastChild.checked;c(function(){var k=s.createElement("div");k.style.width=k.style.paddingLeft="1px";s.body.appendChild(k);c.boxModel=c.support.boxModel=k.offsetWidth===2;s.body.removeChild(k).style.display="none"});a=function(k){var n= s.createElement("div");k="on"+k;var r=k in n;if(!r){n.setAttribute(k,"return;");r=typeof n[k]==="function"}return r};c.support.submitBubbles=a("submit");c.support.changeBubbles=a("change");a=b=d=e=j=null}})();c.props={"for":"htmlFor","class":"className",readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",colspan:"colSpan",tabindex:"tabIndex",usemap:"useMap",frameborder:"frameBorder"};var G="jQuery"+J(),Ya=0,za={};c.extend({cache:{},expando:G,noData:{embed:true,object:true, applet:true},data:function(a,b,d){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==A?za:a;var f=a[G],e=c.cache;if(!f&&typeof b==="string"&&d===w)return null;f||(f=++Ya);if(typeof b==="object"){a[G]=f;e[f]=c.extend(true,{},b)}else if(!e[f]){a[G]=f;e[f]={}}a=e[f];if(d!==w)a[b]=d;return typeof b==="string"?a[b]:a}},removeData:function(a,b){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==A?za:a;var d=a[G],f=c.cache,e=f[d];if(b){if(e){delete e[b];c.isEmptyObject(e)&&c.removeData(a)}}else{if(c.support.deleteExpando)delete a[c.expando]; else a.removeAttribute&&a.removeAttribute(c.expando);delete f[d]}}}});c.fn.extend({data:function(a,b){if(typeof a==="undefined"&&this.length)return c.data(this[0]);else if(typeof a==="object")return this.each(function(){c.data(this,a)});var d=a.split(".");d[1]=d[1]?"."+d[1]:"";if(b===w){var f=this.triggerHandler("getData"+d[1]+"!",[d[0]]);if(f===w&&this.length)f=c.data(this[0],a);return f===w&&d[1]?this.data(d[0]):f}else return this.trigger("setData"+d[1]+"!",[d[0],b]).each(function(){c.data(this, a,b)})},removeData:function(a){return this.each(function(){c.removeData(this,a)})}});c.extend({queue:function(a,b,d){if(a){b=(b||"fx")+"queue";var f=c.data(a,b);if(!d)return f||[];if(!f||c.isArray(d))f=c.data(a,b,c.makeArray(d));else f.push(d);return f}},dequeue:function(a,b){b=b||"fx";var d=c.queue(a,b),f=d.shift();if(f==="inprogress")f=d.shift();if(f){b==="fx"&&d.unshift("inprogress");f.call(a,function(){c.dequeue(a,b)})}}});c.fn.extend({queue:function(a,b){if(typeof a!=="string"){b=a;a="fx"}if(b=== w)return c.queue(this[0],a);return this.each(function(){var d=c.queue(this,a,b);a==="fx"&&d[0]!=="inprogress"&&c.dequeue(this,a)})},dequeue:function(a){return this.each(function(){c.dequeue(this,a)})},delay:function(a,b){a=c.fx?c.fx.speeds[a]||a:a;b=b||"fx";return this.queue(b,function(){var d=this;setTimeout(function(){c.dequeue(d,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])}});var Aa=/[\n\t]/g,ca=/\s+/,Za=/\r/g,$a=/href|src|style/,ab=/(button|input)/i,bb=/(button|input|object|select|textarea)/i, cb=/^(a|area)$/i,Ba=/radio|checkbox/;c.fn.extend({attr:function(a,b){return X(this,a,b,true,c.attr)},removeAttr:function(a){return this.each(function(){c.attr(this,a,"");this.nodeType===1&&this.removeAttribute(a)})},addClass:function(a){if(c.isFunction(a))return this.each(function(n){var r=c(this);r.addClass(a.call(this,n,r.attr("class")))});if(a&&typeof a==="string")for(var b=(a||"").split(ca),d=0,f=this.length;d-1)return true;return false},val:function(a){if(a===w){var b=this[0];if(b){if(c.nodeName(b,"option"))return(b.attributes.value||{}).specified?b.value:b.text;if(c.nodeName(b,"select")){var d=b.selectedIndex,f=[],e=b.options;b=b.type==="select-one";if(d<0)return null;var j=b?d:0;for(d=b?d+1:e.length;j=0;else if(c.nodeName(this,"select")){var u=c.makeArray(r);c("option",this).each(function(){this.selected= c.inArray(c(this).val(),u)>=0});if(!u.length)this.selectedIndex=-1}else this.value=r}})}});c.extend({attrFn:{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true},attr:function(a,b,d,f){if(!a||a.nodeType===3||a.nodeType===8)return w;if(f&&b in c.attrFn)return c(a)[b](d);f=a.nodeType!==1||!c.isXMLDoc(a);var e=d!==w;b=f&&c.props[b]||b;if(a.nodeType===1){var j=$a.test(b);if(b in a&&f&&!j){if(e){b==="type"&&ab.test(a.nodeName)&&a.parentNode&&c.error("type property can't be changed"); a[b]=d}if(c.nodeName(a,"form")&&a.getAttributeNode(b))return a.getAttributeNode(b).nodeValue;if(b==="tabIndex")return(b=a.getAttributeNode("tabIndex"))&&b.specified?b.value:bb.test(a.nodeName)||cb.test(a.nodeName)&&a.href?0:w;return a[b]}if(!c.support.style&&f&&b==="style"){if(e)a.style.cssText=""+d;return a.style.cssText}e&&a.setAttribute(b,""+d);a=!c.support.hrefNormalized&&f&&j?a.getAttribute(b,2):a.getAttribute(b);return a===null?w:a}return c.style(a,b,d)}});var O=/\.(.*)$/,db=function(a){return a.replace(/[^\w\s\.\|`]/g, function(b){return"\\"+b})};c.event={add:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){if(a.setInterval&&a!==A&&!a.frameElement)a=A;var e,j;if(d.handler){e=d;d=e.handler}if(!d.guid)d.guid=c.guid++;if(j=c.data(a)){var i=j.events=j.events||{},o=j.handle;if(!o)j.handle=o=function(){return typeof c!=="undefined"&&!c.event.triggered?c.event.handle.apply(o.elem,arguments):w};o.elem=a;b=b.split(" ");for(var k,n=0,r;k=b[n++];){j=e?c.extend({},e):{handler:d,data:f};if(k.indexOf(".")>-1){r=k.split("."); k=r.shift();j.namespace=r.slice(0).sort().join(".")}else{r=[];j.namespace=""}j.type=k;j.guid=d.guid;var u=i[k],z=c.event.special[k]||{};if(!u){u=i[k]=[];if(!z.setup||z.setup.call(a,f,r,o)===false)if(a.addEventListener)a.addEventListener(k,o,false);else a.attachEvent&&a.attachEvent("on"+k,o)}if(z.add){z.add.call(a,j);if(!j.handler.guid)j.handler.guid=d.guid}u.push(j);c.event.global[k]=true}a=null}}},global:{},remove:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){var e,j=0,i,o,k,n,r,u,z=c.data(a), C=z&&z.events;if(z&&C){if(b&&b.type){d=b.handler;b=b.type}if(!b||typeof b==="string"&&b.charAt(0)==="."){b=b||"";for(e in C)c.event.remove(a,e+b)}else{for(b=b.split(" ");e=b[j++];){n=e;i=e.indexOf(".")<0;o=[];if(!i){o=e.split(".");e=o.shift();k=new RegExp("(^|\\.)"+c.map(o.slice(0).sort(),db).join("\\.(?:.*\\.)?")+"(\\.|$)")}if(r=C[e])if(d){n=c.event.special[e]||{};for(B=f||0;B=0){a.type= e=e.slice(0,-1);a.exclusive=true}if(!d){a.stopPropagation();c.event.global[e]&&c.each(c.cache,function(){this.events&&this.events[e]&&c.event.trigger(a,b,this.handle.elem)})}if(!d||d.nodeType===3||d.nodeType===8)return w;a.result=w;a.target=d;b=c.makeArray(b);b.unshift(a)}a.currentTarget=d;(f=c.data(d,"handle"))&&f.apply(d,b);f=d.parentNode||d.ownerDocument;try{if(!(d&&d.nodeName&&c.noData[d.nodeName.toLowerCase()]))if(d["on"+e]&&d["on"+e].apply(d,b)===false)a.result=false}catch(j){}if(!a.isPropagationStopped()&& f)c.event.trigger(a,b,f,true);else if(!a.isDefaultPrevented()){f=a.target;var i,o=c.nodeName(f,"a")&&e==="click",k=c.event.special[e]||{};if((!k._default||k._default.call(d,a)===false)&&!o&&!(f&&f.nodeName&&c.noData[f.nodeName.toLowerCase()])){try{if(f[e]){if(i=f["on"+e])f["on"+e]=null;c.event.triggered=true;f[e]()}}catch(n){}if(i)f["on"+e]=i;c.event.triggered=false}}},handle:function(a){var b,d,f,e;a=arguments[0]=c.event.fix(a||A.event);a.currentTarget=this;b=a.type.indexOf(".")<0&&!a.exclusive; if(!b){d=a.type.split(".");a.type=d.shift();f=new RegExp("(^|\\.)"+d.slice(0).sort().join("\\.(?:.*\\.)?")+"(\\.|$)")}e=c.data(this,"events");d=e[a.type];if(e&&d){d=d.slice(0);e=0;for(var j=d.length;e-1?c.map(a.options,function(f){return f.selected}).join("-"):"";else if(a.nodeName.toLowerCase()==="select")d=a.selectedIndex;return d},fa=function(a,b){var d=a.target,f,e;if(!(!da.test(d.nodeName)||d.readOnly)){f=c.data(d,"_change_data");e=Fa(d);if(a.type!=="focusout"||d.type!=="radio")c.data(d,"_change_data", e);if(!(f===w||e===f))if(f!=null||e){a.type="change";return c.event.trigger(a,b,d)}}};c.event.special.change={filters:{focusout:fa,click:function(a){var b=a.target,d=b.type;if(d==="radio"||d==="checkbox"||b.nodeName.toLowerCase()==="select")return fa.call(this,a)},keydown:function(a){var b=a.target,d=b.type;if(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(d==="checkbox"||d==="radio")||d==="select-multiple")return fa.call(this,a)},beforeactivate:function(a){a=a.target;c.data(a, "_change_data",Fa(a))}},setup:function(){if(this.type==="file")return false;for(var a in ea)c.event.add(this,a+".specialChange",ea[a]);return da.test(this.nodeName)},teardown:function(){c.event.remove(this,".specialChange");return da.test(this.nodeName)}};ea=c.event.special.change.filters}s.addEventListener&&c.each({focus:"focusin",blur:"focusout"},function(a,b){function d(f){f=c.event.fix(f);f.type=b;return c.event.handle.call(this,f)}c.event.special[b]={setup:function(){this.addEventListener(a, d,true)},teardown:function(){this.removeEventListener(a,d,true)}}});c.each(["bind","one"],function(a,b){c.fn[b]=function(d,f,e){if(typeof d==="object"){for(var j in d)this[b](j,f,d[j],e);return this}if(c.isFunction(f)){e=f;f=w}var i=b==="one"?c.proxy(e,function(k){c(this).unbind(k,i);return e.apply(this,arguments)}):e;if(d==="unload"&&b!=="one")this.one(d,f,e);else{j=0;for(var o=this.length;j0){y=t;break}}t=t[g]}m[q]=y}}}var f=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, e=0,j=Object.prototype.toString,i=false,o=true;[0,0].sort(function(){o=false;return 0});var k=function(g,h,l,m){l=l||[];var q=h=h||s;if(h.nodeType!==1&&h.nodeType!==9)return[];if(!g||typeof g!=="string")return l;for(var p=[],v,t,y,S,H=true,M=x(h),I=g;(f.exec(""),v=f.exec(I))!==null;){I=v[3];p.push(v[1]);if(v[2]){S=v[3];break}}if(p.length>1&&r.exec(g))if(p.length===2&&n.relative[p[0]])t=ga(p[0]+p[1],h);else for(t=n.relative[p[0]]?[h]:k(p.shift(),h);p.length;){g=p.shift();if(n.relative[g])g+=p.shift(); t=ga(g,t)}else{if(!m&&p.length>1&&h.nodeType===9&&!M&&n.match.ID.test(p[0])&&!n.match.ID.test(p[p.length-1])){v=k.find(p.shift(),h,M);h=v.expr?k.filter(v.expr,v.set)[0]:v.set[0]}if(h){v=m?{expr:p.pop(),set:z(m)}:k.find(p.pop(),p.length===1&&(p[0]==="~"||p[0]==="+")&&h.parentNode?h.parentNode:h,M);t=v.expr?k.filter(v.expr,v.set):v.set;if(p.length>0)y=z(t);else H=false;for(;p.length;){var D=p.pop();v=D;if(n.relative[D])v=p.pop();else D="";if(v==null)v=h;n.relative[D](y,v,M)}}else y=[]}y||(y=t);y||k.error(D|| g);if(j.call(y)==="[object Array]")if(H)if(h&&h.nodeType===1)for(g=0;y[g]!=null;g++){if(y[g]&&(y[g]===true||y[g].nodeType===1&&E(h,y[g])))l.push(t[g])}else for(g=0;y[g]!=null;g++)y[g]&&y[g].nodeType===1&&l.push(t[g]);else l.push.apply(l,y);else z(y,l);if(S){k(S,q,l,m);k.uniqueSort(l)}return l};k.uniqueSort=function(g){if(B){i=o;g.sort(B);if(i)for(var h=1;h":function(g,h){var l=typeof h==="string";if(l&&!/\W/.test(h)){h=h.toLowerCase();for(var m=0,q=g.length;m=0))l||m.push(v);else if(l)h[p]=false;return false},ID:function(g){return g[1].replace(/\\/g,"")},TAG:function(g){return g[1].toLowerCase()}, CHILD:function(g){if(g[1]==="nth"){var h=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(g[2]==="even"&&"2n"||g[2]==="odd"&&"2n+1"||!/\D/.test(g[2])&&"0n+"+g[2]||g[2]);g[2]=h[1]+(h[2]||1)-0;g[3]=h[3]-0}g[0]=e++;return g},ATTR:function(g,h,l,m,q,p){h=g[1].replace(/\\/g,"");if(!p&&n.attrMap[h])g[1]=n.attrMap[h];if(g[2]==="~=")g[4]=" "+g[4]+" ";return g},PSEUDO:function(g,h,l,m,q){if(g[1]==="not")if((f.exec(g[3])||"").length>1||/^\w/.test(g[3]))g[3]=k(g[3],null,null,h);else{g=k.filter(g[3],h,l,true^q);l||m.push.apply(m, g);return false}else if(n.match.POS.test(g[0])||n.match.CHILD.test(g[0]))return true;return g},POS:function(g){g.unshift(true);return g}},filters:{enabled:function(g){return g.disabled===false&&g.type!=="hidden"},disabled:function(g){return g.disabled===true},checked:function(g){return g.checked===true},selected:function(g){return g.selected===true},parent:function(g){return!!g.firstChild},empty:function(g){return!g.firstChild},has:function(g,h,l){return!!k(l[3],g).length},header:function(g){return/h\d/i.test(g.nodeName)}, text:function(g){return"text"===g.type},radio:function(g){return"radio"===g.type},checkbox:function(g){return"checkbox"===g.type},file:function(g){return"file"===g.type},password:function(g){return"password"===g.type},submit:function(g){return"submit"===g.type},image:function(g){return"image"===g.type},reset:function(g){return"reset"===g.type},button:function(g){return"button"===g.type||g.nodeName.toLowerCase()==="button"},input:function(g){return/input|select|textarea|button/i.test(g.nodeName)}}, setFilters:{first:function(g,h){return h===0},last:function(g,h,l,m){return h===m.length-1},even:function(g,h){return h%2===0},odd:function(g,h){return h%2===1},lt:function(g,h,l){return hl[3]-0},nth:function(g,h,l){return l[3]-0===h},eq:function(g,h,l){return l[3]-0===h}},filter:{PSEUDO:function(g,h,l,m){var q=h[1],p=n.filters[q];if(p)return p(g,l,h,m);else if(q==="contains")return(g.textContent||g.innerText||a([g])||"").indexOf(h[3])>=0;else if(q==="not"){h= h[3];l=0;for(m=h.length;l=0}},ID:function(g,h){return g.nodeType===1&&g.getAttribute("id")===h},TAG:function(g,h){return h==="*"&&g.nodeType===1||g.nodeName.toLowerCase()===h},CLASS:function(g,h){return(" "+(g.className||g.getAttribute("class"))+" ").indexOf(h)>-1},ATTR:function(g,h){var l=h[1];g=n.attrHandle[l]?n.attrHandle[l](g):g[l]!=null?g[l]:g.getAttribute(l);l=g+"";var m=h[2];h=h[4];return g==null?m==="!=":m=== "="?l===h:m==="*="?l.indexOf(h)>=0:m==="~="?(" "+l+" ").indexOf(h)>=0:!h?l&&g!==false:m==="!="?l!==h:m==="^="?l.indexOf(h)===0:m==="$="?l.substr(l.length-h.length)===h:m==="|="?l===h||l.substr(0,h.length+1)===h+"-":false},POS:function(g,h,l,m){var q=n.setFilters[h[2]];if(q)return q(g,l,h,m)}}},r=n.match.POS;for(var u in n.match){n.match[u]=new RegExp(n.match[u].source+/(?![^\[]*\])(?![^\(]*\))/.source);n.leftMatch[u]=new RegExp(/(^(?:.|\r|\n)*?)/.source+n.match[u].source.replace(/\\(\d+)/g,function(g, h){return"\\"+(h-0+1)}))}var z=function(g,h){g=Array.prototype.slice.call(g,0);if(h){h.push.apply(h,g);return h}return g};try{Array.prototype.slice.call(s.documentElement.childNodes,0)}catch(C){z=function(g,h){h=h||[];if(j.call(g)==="[object Array]")Array.prototype.push.apply(h,g);else if(typeof g.length==="number")for(var l=0,m=g.length;l";var l=s.documentElement;l.insertBefore(g,l.firstChild);if(s.getElementById(h)){n.find.ID=function(m,q,p){if(typeof q.getElementById!=="undefined"&&!p)return(q=q.getElementById(m[1]))?q.id===m[1]||typeof q.getAttributeNode!=="undefined"&& q.getAttributeNode("id").nodeValue===m[1]?[q]:w:[]};n.filter.ID=function(m,q){var p=typeof m.getAttributeNode!=="undefined"&&m.getAttributeNode("id");return m.nodeType===1&&p&&p.nodeValue===q}}l.removeChild(g);l=g=null})();(function(){var g=s.createElement("div");g.appendChild(s.createComment(""));if(g.getElementsByTagName("*").length>0)n.find.TAG=function(h,l){l=l.getElementsByTagName(h[1]);if(h[1]==="*"){h=[];for(var m=0;l[m];m++)l[m].nodeType===1&&h.push(l[m]);l=h}return l};g.innerHTML=""; if(g.firstChild&&typeof g.firstChild.getAttribute!=="undefined"&&g.firstChild.getAttribute("href")!=="#")n.attrHandle.href=function(h){return h.getAttribute("href",2)};g=null})();s.querySelectorAll&&function(){var g=k,h=s.createElement("div");h.innerHTML="

";if(!(h.querySelectorAll&&h.querySelectorAll(".TEST").length===0)){k=function(m,q,p,v){q=q||s;if(!v&&q.nodeType===9&&!x(q))try{return z(q.querySelectorAll(m),p)}catch(t){}return g(m,q,p,v)};for(var l in g)k[l]=g[l];h=null}}(); (function(){var g=s.createElement("div");g.innerHTML="
";if(!(!g.getElementsByClassName||g.getElementsByClassName("e").length===0)){g.lastChild.className="e";if(g.getElementsByClassName("e").length!==1){n.order.splice(1,0,"CLASS");n.find.CLASS=function(h,l,m){if(typeof l.getElementsByClassName!=="undefined"&&!m)return l.getElementsByClassName(h[1])};g=null}}})();var E=s.compareDocumentPosition?function(g,h){return!!(g.compareDocumentPosition(h)&16)}: function(g,h){return g!==h&&(g.contains?g.contains(h):true)},x=function(g){return(g=(g?g.ownerDocument||g:0).documentElement)?g.nodeName!=="HTML":false},ga=function(g,h){var l=[],m="",q;for(h=h.nodeType?[h]:h;q=n.match.PSEUDO.exec(g);){m+=q[0];g=g.replace(n.match.PSEUDO,"")}g=n.relative[g]?g+"*":g;q=0;for(var p=h.length;q=0===d})};c.fn.extend({find:function(a){for(var b=this.pushStack("","find",a),d=0,f=0,e=this.length;f0)for(var j=d;j0},closest:function(a,b){if(c.isArray(a)){var d=[],f=this[0],e,j= {},i;if(f&&a.length){e=0;for(var o=a.length;e-1:c(f).is(e)){d.push({selector:i,elem:f});delete j[i]}}f=f.parentNode}}return d}var k=c.expr.match.POS.test(a)?c(a,b||this.context):null;return this.map(function(n,r){for(;r&&r.ownerDocument&&r!==b;){if(k?k.index(r)>-1:c(r).is(a))return r;r=r.parentNode}return null})},index:function(a){if(!a||typeof a=== "string")return c.inArray(this[0],a?c(a):this.parent().children());return c.inArray(a.jquery?a[0]:a,this)},add:function(a,b){a=typeof a==="string"?c(a,b||this.context):c.makeArray(a);b=c.merge(this.get(),a);return this.pushStack(qa(a[0])||qa(b[0])?b:c.unique(b))},andSelf:function(){return this.add(this.prevObject)}});c.each({parent:function(a){return(a=a.parentNode)&&a.nodeType!==11?a:null},parents:function(a){return c.dir(a,"parentNode")},parentsUntil:function(a,b,d){return c.dir(a,"parentNode", d)},next:function(a){return c.nth(a,2,"nextSibling")},prev:function(a){return c.nth(a,2,"previousSibling")},nextAll:function(a){return c.dir(a,"nextSibling")},prevAll:function(a){return c.dir(a,"previousSibling")},nextUntil:function(a,b,d){return c.dir(a,"nextSibling",d)},prevUntil:function(a,b,d){return c.dir(a,"previousSibling",d)},siblings:function(a){return c.sibling(a.parentNode.firstChild,a)},children:function(a){return c.sibling(a.firstChild)},contents:function(a){return c.nodeName(a,"iframe")? a.contentDocument||a.contentWindow.document:c.makeArray(a.childNodes)}},function(a,b){c.fn[a]=function(d,f){var e=c.map(this,b,d);eb.test(a)||(f=d);if(f&&typeof f==="string")e=c.filter(f,e);e=this.length>1?c.unique(e):e;if((this.length>1||gb.test(f))&&fb.test(a))e=e.reverse();return this.pushStack(e,a,R.call(arguments).join(","))}});c.extend({filter:function(a,b,d){if(d)a=":not("+a+")";return c.find.matches(a,b)},dir:function(a,b,d){var f=[];for(a=a[b];a&&a.nodeType!==9&&(d===w||a.nodeType!==1||!c(a).is(d));){a.nodeType=== 1&&f.push(a);a=a[b]}return f},nth:function(a,b,d){b=b||1;for(var f=0;a;a=a[d])if(a.nodeType===1&&++f===b)break;return a},sibling:function(a,b){for(var d=[];a;a=a.nextSibling)a.nodeType===1&&a!==b&&d.push(a);return d}});var Ja=/ jQuery\d+="(?:\d+|null)"/g,V=/^\s+/,Ka=/(<([\w:]+)[^>]*?)\/>/g,hb=/^(?:area|br|col|embed|hr|img|input|link|meta|param)$/i,La=/<([\w:]+)/,ib=/"},F={option:[1,""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]};F.optgroup=F.option;F.tbody=F.tfoot=F.colgroup=F.caption=F.thead;F.th=F.td;if(!c.support.htmlSerialize)F._default=[1,"div
","
"];c.fn.extend({text:function(a){if(c.isFunction(a))return this.each(function(b){var d= c(this);d.text(a.call(this,b,d.text()))});if(typeof a!=="object"&&a!==w)return this.empty().append((this[0]&&this[0].ownerDocument||s).createTextNode(a));return c.text(this)},wrapAll:function(a){if(c.isFunction(a))return this.each(function(d){c(this).wrapAll(a.call(this,d))});if(this[0]){var b=c(a,this[0].ownerDocument).eq(0).clone(true);this[0].parentNode&&b.insertBefore(this[0]);b.map(function(){for(var d=this;d.firstChild&&d.firstChild.nodeType===1;)d=d.firstChild;return d}).append(this)}return this}, wrapInner:function(a){if(c.isFunction(a))return this.each(function(b){c(this).wrapInner(a.call(this,b))});return this.each(function(){var b=c(this),d=b.contents();d.length?d.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){c(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){c.nodeName(this,"body")||c(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.appendChild(a)})}, prepend:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this)});else if(arguments.length){var a=c(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b, this.nextSibling)});else if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,c(arguments[0]).toArray());return a}},remove:function(a,b){for(var d=0,f;(f=this[d])!=null;d++)if(!a||c.filter(a,[f]).length){if(!b&&f.nodeType===1){c.cleanData(f.getElementsByTagName("*"));c.cleanData([f])}f.parentNode&&f.parentNode.removeChild(f)}return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++)for(b.nodeType===1&&c.cleanData(b.getElementsByTagName("*"));b.firstChild;)b.removeChild(b.firstChild); return this},clone:function(a){var b=this.map(function(){if(!c.support.noCloneEvent&&!c.isXMLDoc(this)){var d=this.outerHTML,f=this.ownerDocument;if(!d){d=f.createElement("div");d.appendChild(this.cloneNode(true));d=d.innerHTML}return c.clean([d.replace(Ja,"").replace(/=([^="'>\s]+\/)>/g,'="$1">').replace(V,"")],f)[0]}else return this.cloneNode(true)});if(a===true){ra(this,b);ra(this.find("*"),b.find("*"))}return b},html:function(a){if(a===w)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(Ja, ""):null;else if(typeof a==="string"&&!ta.test(a)&&(c.support.leadingWhitespace||!V.test(a))&&!F[(La.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Ka,Ma);try{for(var b=0,d=this.length;b0||e.cacheable||this.length>1?k.cloneNode(true):k)}o.length&&c.each(o,Qa)}return this}});c.fragments={};c.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){c.fn[a]=function(d){var f=[];d=c(d);var e=this.length===1&&this[0].parentNode;if(e&&e.nodeType===11&&e.childNodes.length===1&&d.length===1){d[b](this[0]); return this}else{e=0;for(var j=d.length;e0?this.clone(true):this).get();c.fn[b].apply(c(d[e]),i);f=f.concat(i)}return this.pushStack(f,a,d.selector)}}});c.extend({clean:function(a,b,d,f){b=b||s;if(typeof b.createElement==="undefined")b=b.ownerDocument||b[0]&&b[0].ownerDocument||s;for(var e=[],j=0,i;(i=a[j])!=null;j++){if(typeof i==="number")i+="";if(i){if(typeof i==="string"&&!jb.test(i))i=b.createTextNode(i);else if(typeof i==="string"){i=i.replace(Ka,Ma);var o=(La.exec(i)||["", ""])[1].toLowerCase(),k=F[o]||F._default,n=k[0],r=b.createElement("div");for(r.innerHTML=k[1]+i+k[2];n--;)r=r.lastChild;if(!c.support.tbody){n=ib.test(i);o=o==="table"&&!n?r.firstChild&&r.firstChild.childNodes:k[1]===""&&!n?r.childNodes:[];for(k=o.length-1;k>=0;--k)c.nodeName(o[k],"tbody")&&!o[k].childNodes.length&&o[k].parentNode.removeChild(o[k])}!c.support.leadingWhitespace&&V.test(i)&&r.insertBefore(b.createTextNode(V.exec(i)[0]),r.firstChild);i=r.childNodes}if(i.nodeType)e.push(i);else e= c.merge(e,i)}}if(d)for(j=0;e[j];j++)if(f&&c.nodeName(e[j],"script")&&(!e[j].type||e[j].type.toLowerCase()==="text/javascript"))f.push(e[j].parentNode?e[j].parentNode.removeChild(e[j]):e[j]);else{e[j].nodeType===1&&e.splice.apply(e,[j+1,0].concat(c.makeArray(e[j].getElementsByTagName("script"))));d.appendChild(e[j])}return e},cleanData:function(a){for(var b,d,f=c.cache,e=c.event.special,j=c.support.deleteExpando,i=0,o;(o=a[i])!=null;i++)if(d=o[c.expando]){b=f[d];if(b.events)for(var k in b.events)e[k]? c.event.remove(o,k):Ca(o,k,b.handle);if(j)delete o[c.expando];else o.removeAttribute&&o.removeAttribute(c.expando);delete f[d]}}});var kb=/z-?index|font-?weight|opacity|zoom|line-?height/i,Na=/alpha\([^)]*\)/,Oa=/opacity=([^)]*)/,ha=/float/i,ia=/-([a-z])/ig,lb=/([A-Z])/g,mb=/^-?\d+(?:px)?$/i,nb=/^-?\d/,ob={position:"absolute",visibility:"hidden",display:"block"},pb=["Left","Right"],qb=["Top","Bottom"],rb=s.defaultView&&s.defaultView.getComputedStyle,Pa=c.support.cssFloat?"cssFloat":"styleFloat",ja= function(a,b){return b.toUpperCase()};c.fn.css=function(a,b){return X(this,a,b,true,function(d,f,e){if(e===w)return c.curCSS(d,f);if(typeof e==="number"&&!kb.test(f))e+="px";c.style(d,f,e)})};c.extend({style:function(a,b,d){if(!a||a.nodeType===3||a.nodeType===8)return w;if((b==="width"||b==="height")&&parseFloat(d)<0)d=w;var f=a.style||a,e=d!==w;if(!c.support.opacity&&b==="opacity"){if(e){f.zoom=1;b=parseInt(d,10)+""==="NaN"?"":"alpha(opacity="+d*100+")";a=f.filter||c.curCSS(a,"filter")||"";f.filter= Na.test(a)?a.replace(Na,b):b}return f.filter&&f.filter.indexOf("opacity=")>=0?parseFloat(Oa.exec(f.filter)[1])/100+"":""}if(ha.test(b))b=Pa;b=b.replace(ia,ja);if(e)f[b]=d;return f[b]},css:function(a,b,d,f){if(b==="width"||b==="height"){var e,j=b==="width"?pb:qb;function i(){e=b==="width"?a.offsetWidth:a.offsetHeight;f!=="border"&&c.each(j,function(){f||(e-=parseFloat(c.curCSS(a,"padding"+this,true))||0);if(f==="margin")e+=parseFloat(c.curCSS(a,"margin"+this,true))||0;else e-=parseFloat(c.curCSS(a, "border"+this+"Width",true))||0})}a.offsetWidth!==0?i():c.swap(a,ob,i);return Math.max(0,Math.round(e))}return c.curCSS(a,b,d)},curCSS:function(a,b,d){var f,e=a.style;if(!c.support.opacity&&b==="opacity"&&a.currentStyle){f=Oa.test(a.currentStyle.filter||"")?parseFloat(RegExp.$1)/100+"":"";return f===""?"1":f}if(ha.test(b))b=Pa;if(!d&&e&&e[b])f=e[b];else if(rb){if(ha.test(b))b="float";b=b.replace(lb,"-$1").toLowerCase();e=a.ownerDocument.defaultView;if(!e)return null;if(a=e.getComputedStyle(a,null))f= a.getPropertyValue(b);if(b==="opacity"&&f==="")f="1"}else if(a.currentStyle){d=b.replace(ia,ja);f=a.currentStyle[b]||a.currentStyle[d];if(!mb.test(f)&&nb.test(f)){b=e.left;var j=a.runtimeStyle.left;a.runtimeStyle.left=a.currentStyle.left;e.left=d==="fontSize"?"1em":f||0;f=e.pixelLeft+"px";e.left=b;a.runtimeStyle.left=j}}return f},swap:function(a,b,d){var f={};for(var e in b){f[e]=a.style[e];a.style[e]=b[e]}d.call(a);for(e in b)a.style[e]=f[e]}});if(c.expr&&c.expr.filters){c.expr.filters.hidden=function(a){var b= a.offsetWidth,d=a.offsetHeight,f=a.nodeName.toLowerCase()==="tr";return b===0&&d===0&&!f?true:b>0&&d>0&&!f?false:c.curCSS(a,"display")==="none"};c.expr.filters.visible=function(a){return!c.expr.filters.hidden(a)}}var sb=J(),tb=//gi,ub=/select|textarea/i,vb=/color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week/i,N=/=\?(&|$)/,ka=/\?/,wb=/(\?|&)_=.*?(&|$)/,xb=/^(\w+:)?\/\/([^\/?#]+)/,yb=/%20/g,zb=c.fn.load;c.fn.extend({load:function(a,b,d){if(typeof a!== "string")return zb.call(this,a);else if(!this.length)return this;var f=a.indexOf(" ");if(f>=0){var e=a.slice(f,a.length);a=a.slice(0,f)}f="GET";if(b)if(c.isFunction(b)){d=b;b=null}else if(typeof b==="object"){b=c.param(b,c.ajaxSettings.traditional);f="POST"}var j=this;c.ajax({url:a,type:f,dataType:"html",data:b,complete:function(i,o){if(o==="success"||o==="notmodified")j.html(e?c("
").append(i.responseText.replace(tb,"")).find(e):i.responseText);d&&j.each(d,[i.responseText,o,i])}});return this}, serialize:function(){return c.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?c.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||ub.test(this.nodeName)||vb.test(this.type))}).map(function(a,b){a=c(this).val();return a==null?null:c.isArray(a)?c.map(a,function(d){return{name:b.name,value:d}}):{name:b.name,value:a}}).get()}});c.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "), function(a,b){c.fn[b]=function(d){return this.bind(b,d)}});c.extend({get:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b=null}return c.ajax({type:"GET",url:a,data:b,success:d,dataType:f})},getScript:function(a,b){return c.get(a,null,b,"script")},getJSON:function(a,b,d){return c.get(a,b,d,"json")},post:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b={}}return c.ajax({type:"POST",url:a,data:b,success:d,dataType:f})},ajaxSetup:function(a){c.extend(c.ajaxSettings,a)},ajaxSettings:{url:location.href, global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:A.XMLHttpRequest&&(A.location.protocol!=="file:"||!A.ActiveXObject)?function(){return new A.XMLHttpRequest}:function(){try{return new A.ActiveXObject("Microsoft.XMLHTTP")}catch(a){}},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},etag:{},ajax:function(a){function b(){e.success&& e.success.call(k,o,i,x);e.global&&f("ajaxSuccess",[x,e])}function d(){e.complete&&e.complete.call(k,x,i);e.global&&f("ajaxComplete",[x,e]);e.global&&!--c.active&&c.event.trigger("ajaxStop")}function f(q,p){(e.context?c(e.context):c.event).trigger(q,p)}var e=c.extend(true,{},c.ajaxSettings,a),j,i,o,k=a&&a.context||e,n=e.type.toUpperCase();if(e.data&&e.processData&&typeof e.data!=="string")e.data=c.param(e.data,e.traditional);if(e.dataType==="jsonp"){if(n==="GET")N.test(e.url)||(e.url+=(ka.test(e.url)? "&":"?")+(e.jsonp||"callback")+"=?");else if(!e.data||!N.test(e.data))e.data=(e.data?e.data+"&":"")+(e.jsonp||"callback")+"=?";e.dataType="json"}if(e.dataType==="json"&&(e.data&&N.test(e.data)||N.test(e.url))){j=e.jsonpCallback||"jsonp"+sb++;if(e.data)e.data=(e.data+"").replace(N,"="+j+"$1");e.url=e.url.replace(N,"="+j+"$1");e.dataType="script";A[j]=A[j]||function(q){o=q;b();d();A[j]=w;try{delete A[j]}catch(p){}z&&z.removeChild(C)}}if(e.dataType==="script"&&e.cache===null)e.cache=false;if(e.cache=== false&&n==="GET"){var r=J(),u=e.url.replace(wb,"$1_="+r+"$2");e.url=u+(u===e.url?(ka.test(e.url)?"&":"?")+"_="+r:"")}if(e.data&&n==="GET")e.url+=(ka.test(e.url)?"&":"?")+e.data;e.global&&!c.active++&&c.event.trigger("ajaxStart");r=(r=xb.exec(e.url))&&(r[1]&&r[1]!==location.protocol||r[2]!==location.host);if(e.dataType==="script"&&n==="GET"&&r){var z=s.getElementsByTagName("head")[0]||s.documentElement,C=s.createElement("script");C.src=e.url;if(e.scriptCharset)C.charset=e.scriptCharset;if(!j){var B= false;C.onload=C.onreadystatechange=function(){if(!B&&(!this.readyState||this.readyState==="loaded"||this.readyState==="complete")){B=true;b();d();C.onload=C.onreadystatechange=null;z&&C.parentNode&&z.removeChild(C)}}}z.insertBefore(C,z.firstChild);return w}var E=false,x=e.xhr();if(x){e.username?x.open(n,e.url,e.async,e.username,e.password):x.open(n,e.url,e.async);try{if(e.data||a&&a.contentType)x.setRequestHeader("Content-Type",e.contentType);if(e.ifModified){c.lastModified[e.url]&&x.setRequestHeader("If-Modified-Since", c.lastModified[e.url]);c.etag[e.url]&&x.setRequestHeader("If-None-Match",c.etag[e.url])}r||x.setRequestHeader("X-Requested-With","XMLHttpRequest");x.setRequestHeader("Accept",e.dataType&&e.accepts[e.dataType]?e.accepts[e.dataType]+", */*":e.accepts._default)}catch(ga){}if(e.beforeSend&&e.beforeSend.call(k,x,e)===false){e.global&&!--c.active&&c.event.trigger("ajaxStop");x.abort();return false}e.global&&f("ajaxSend",[x,e]);var g=x.onreadystatechange=function(q){if(!x||x.readyState===0||q==="abort"){E|| d();E=true;if(x)x.onreadystatechange=c.noop}else if(!E&&x&&(x.readyState===4||q==="timeout")){E=true;x.onreadystatechange=c.noop;i=q==="timeout"?"timeout":!c.httpSuccess(x)?"error":e.ifModified&&c.httpNotModified(x,e.url)?"notmodified":"success";var p;if(i==="success")try{o=c.httpData(x,e.dataType,e)}catch(v){i="parsererror";p=v}if(i==="success"||i==="notmodified")j||b();else c.handleError(e,x,i,p);d();q==="timeout"&&x.abort();if(e.async)x=null}};try{var h=x.abort;x.abort=function(){x&&h.call(x); g("abort")}}catch(l){}e.async&&e.timeout>0&&setTimeout(function(){x&&!E&&g("timeout")},e.timeout);try{x.send(n==="POST"||n==="PUT"||n==="DELETE"?e.data:null)}catch(m){c.handleError(e,x,null,m);d()}e.async||g();return x}},handleError:function(a,b,d,f){if(a.error)a.error.call(a.context||a,b,d,f);if(a.global)(a.context?c(a.context):c.event).trigger("ajaxError",[b,a,f])},active:0,httpSuccess:function(a){try{return!a.status&&location.protocol==="file:"||a.status>=200&&a.status<300||a.status===304||a.status=== 1223||a.status===0}catch(b){}return false},httpNotModified:function(a,b){var d=a.getResponseHeader("Last-Modified"),f=a.getResponseHeader("Etag");if(d)c.lastModified[b]=d;if(f)c.etag[b]=f;return a.status===304||a.status===0},httpData:function(a,b,d){var f=a.getResponseHeader("content-type")||"",e=b==="xml"||!b&&f.indexOf("xml")>=0;a=e?a.responseXML:a.responseText;e&&a.documentElement.nodeName==="parsererror"&&c.error("parsererror");if(d&&d.dataFilter)a=d.dataFilter(a,b);if(typeof a==="string")if(b=== "json"||!b&&f.indexOf("json")>=0)a=c.parseJSON(a);else if(b==="script"||!b&&f.indexOf("javascript")>=0)c.globalEval(a);return a},param:function(a,b){function d(i,o){if(c.isArray(o))c.each(o,function(k,n){b||/\[\]$/.test(i)?f(i,n):d(i+"["+(typeof n==="object"||c.isArray(n)?k:"")+"]",n)});else!b&&o!=null&&typeof o==="object"?c.each(o,function(k,n){d(i+"["+k+"]",n)}):f(i,o)}function f(i,o){o=c.isFunction(o)?o():o;e[e.length]=encodeURIComponent(i)+"="+encodeURIComponent(o)}var e=[];if(b===w)b=c.ajaxSettings.traditional; if(c.isArray(a)||a.jquery)c.each(a,function(){f(this.name,this.value)});else for(var j in a)d(j,a[j]);return e.join("&").replace(yb,"+")}});var la={},Ab=/toggle|show|hide/,Bb=/^([+-]=)?([\d+-.]+)(.*)$/,W,va=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];c.fn.extend({show:function(a,b){if(a||a===0)return this.animate(K("show",3),a,b);else{a=0;for(b=this.length;a").appendTo("body");f=e.css("display");if(f==="none")f="block";e.remove();la[d]=f}c.data(this[a],"olddisplay",f)}}a=0;for(b=this.length;a=0;f--)if(d[f].elem===this){b&&d[f](true);d.splice(f,1)}});b||this.dequeue();return this}});c.each({slideDown:K("show",1),slideUp:K("hide",1),slideToggle:K("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(a,b){c.fn[a]=function(d,f){return this.animate(b,d,f)}});c.extend({speed:function(a,b,d){var f=a&&typeof a==="object"?a:{complete:d||!d&&b||c.isFunction(a)&&a,duration:a,easing:d&&b||b&&!c.isFunction(b)&&b};f.duration=c.fx.off?0:typeof f.duration=== "number"?f.duration:c.fx.speeds[f.duration]||c.fx.speeds._default;f.old=f.complete;f.complete=function(){f.queue!==false&&c(this).dequeue();c.isFunction(f.old)&&f.old.call(this)};return f},easing:{linear:function(a,b,d,f){return d+f*a},swing:function(a,b,d,f){return(-Math.cos(a*Math.PI)/2+0.5)*f+d}},timers:[],fx:function(a,b,d){this.options=b;this.elem=a;this.prop=d;if(!b.orig)b.orig={}}});c.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this);(c.fx.step[this.prop]|| c.fx.step._default)(this);if((this.prop==="height"||this.prop==="width")&&this.elem.style)this.elem.style.display="block"},cur:function(a){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];return(a=parseFloat(c.css(this.elem,this.prop,a)))&&a>-10000?a:parseFloat(c.curCSS(this.elem,this.prop))||0},custom:function(a,b,d){function f(j){return e.step(j)}this.startTime=J();this.start=a;this.end=b;this.unit=d||this.unit||"px";this.now=this.start; this.pos=this.state=0;var e=this;f.elem=this.elem;if(f()&&c.timers.push(f)&&!W)W=setInterval(c.fx.tick,13)},show:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.show=true;this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur());c(this.elem).show()},hide:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.hide=true;this.custom(this.cur(),0)},step:function(a){var b=J(),d=true;if(a||b>=this.options.duration+this.startTime){this.now= this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;for(var f in this.options.curAnim)if(this.options.curAnim[f]!==true)d=false;if(d){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;a=c.data(this.elem,"olddisplay");this.elem.style.display=a?a:this.options.display;if(c.css(this.elem,"display")==="none")this.elem.style.display="block"}this.options.hide&&c(this.elem).hide();if(this.options.hide||this.options.show)for(var e in this.options.curAnim)c.style(this.elem, e,this.options.orig[e]);this.options.complete.call(this.elem)}return false}else{e=b-this.startTime;this.state=e/this.options.duration;a=this.options.easing||(c.easing.swing?"swing":"linear");this.pos=c.easing[this.options.specialEasing&&this.options.specialEasing[this.prop]||a](this.state,e,0,1,this.options.duration);this.now=this.start+(this.end-this.start)*this.pos;this.update()}return true}};c.extend(c.fx,{tick:function(){for(var a=c.timers,b=0;b
"; a.insertBefore(b,a.firstChild);d=b.firstChild;f=d.firstChild;e=d.nextSibling.firstChild.firstChild;this.doesNotAddBorder=f.offsetTop!==5;this.doesAddBorderForTableAndCells=e.offsetTop===5;f.style.position="fixed";f.style.top="20px";this.supportsFixedPosition=f.offsetTop===20||f.offsetTop===15;f.style.position=f.style.top="";d.style.overflow="hidden";d.style.position="relative";this.subtractsBorderForOverflowNotVisible=f.offsetTop===-5;this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==j;a.removeChild(b); c.offset.initialize=c.noop},bodyOffset:function(a){var b=a.offsetTop,d=a.offsetLeft;c.offset.initialize();if(c.offset.doesNotIncludeMarginInBodyOffset){b+=parseFloat(c.curCSS(a,"marginTop",true))||0;d+=parseFloat(c.curCSS(a,"marginLeft",true))||0}return{top:b,left:d}},setOffset:function(a,b,d){if(/static/.test(c.curCSS(a,"position")))a.style.position="relative";var f=c(a),e=f.offset(),j=parseInt(c.curCSS(a,"top",true),10)||0,i=parseInt(c.curCSS(a,"left",true),10)||0;if(c.isFunction(b))b=b.call(a, d,e);d={top:b.top-e.top+j,left:b.left-e.left+i};"using"in b?b.using.call(a,d):f.css(d)}};c.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),d=this.offset(),f=/^body|html$/i.test(b[0].nodeName)?{top:0,left:0}:b.offset();d.top-=parseFloat(c.curCSS(a,"marginTop",true))||0;d.left-=parseFloat(c.curCSS(a,"marginLeft",true))||0;f.top+=parseFloat(c.curCSS(b[0],"borderTopWidth",true))||0;f.left+=parseFloat(c.curCSS(b[0],"borderLeftWidth",true))||0;return{top:d.top- f.top,left:d.left-f.left}},offsetParent:function(){return this.map(function(){for(var a=this.offsetParent||s.body;a&&!/^body|html$/i.test(a.nodeName)&&c.css(a,"position")==="static";)a=a.offsetParent;return a})}});c.each(["Left","Top"],function(a,b){var d="scroll"+b;c.fn[d]=function(f){var e=this[0],j;if(!e)return null;if(f!==w)return this.each(function(){if(j=wa(this))j.scrollTo(!a?f:c(j).scrollLeft(),a?f:c(j).scrollTop());else this[d]=f});else return(j=wa(e))?"pageXOffset"in j?j[a?"pageYOffset": "pageXOffset"]:c.support.boxModel&&j.document.documentElement[d]||j.document.body[d]:e[d]}});c.each(["Height","Width"],function(a,b){var d=b.toLowerCase();c.fn["inner"+b]=function(){return this[0]?c.css(this[0],d,false,"padding"):null};c.fn["outer"+b]=function(f){return this[0]?c.css(this[0],d,false,f?"margin":"border"):null};c.fn[d]=function(f){var e=this[0];if(!e)return f==null?null:this;if(c.isFunction(f))return this.each(function(j){var i=c(this);i[d](f.call(this,j,i[d]()))});return"scrollTo"in e&&e.document?e.document.compatMode==="CSS1Compat"&&e.document.documentElement["client"+b]||e.document.body["client"+b]:e.nodeType===9?Math.max(e.documentElement["client"+b],e.body["scroll"+b],e.documentElement["scroll"+b],e.body["offset"+b],e.documentElement["offset"+b]):f===w?c.css(e,d):this.css(d,typeof f==="string"?f:f+"px")}});A.jQuery=A.$=c})(window); ================================================ FILE: test/browser/qunit/qunit.css ================================================ ol#qunit-tests { font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; margin:0; padding:0; list-style-position:inside; font-size: smaller; } ol#qunit-tests li{ padding:0.4em 0.5em 0.4em 2.5em; border-bottom:1px solid #fff; font-size:small; list-style-position:inside; } ol#qunit-tests li ol{ box-shadow: inset 0px 2px 13px #999; -moz-box-shadow: inset 0px 2px 13px #999; -webkit-box-shadow: inset 0px 2px 13px #999; margin-top:0.5em; margin-left:0; padding:0.5em; background-color:#fff; border-radius:15px; -moz-border-radius: 15px; -webkit-border-radius: 15px; } ol#qunit-tests li li{ border-bottom:none; margin:0.5em; background-color:#fff; list-style-position: inside; padding:0.4em 0.5em 0.4em 0.5em; } ol#qunit-tests li li.pass{ border-left:26px solid #C6E746; background-color:#fff; color:#5E740B; } ol#qunit-tests li li.fail{ border-left:26px solid #EE5757; background-color:#fff; color:#710909; } ol#qunit-tests li.pass{ background-color:#D2E0E6; color:#528CE0; } ol#qunit-tests li.fail{ background-color:#EE5757; color:#000; } ol#qunit-tests li strong { cursor:pointer; } h1#qunit-header{ background-color:#0d3349; margin:0; padding:0.5em 0 0.5em 1em; color:#fff; font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; border-top-right-radius:15px; border-top-left-radius:15px; -moz-border-radius-topright:15px; -moz-border-radius-topleft:15px; -webkit-border-top-right-radius:15px; -webkit-border-top-left-radius:15px; text-shadow: rgba(0, 0, 0, 0.5) 4px 4px 1px; } h2#qunit-banner{ font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; height:5px; margin:0; padding:0; } h2#qunit-banner.qunit-pass{ background-color:#C6E746; } h2#qunit-banner.qunit-fail, #qunit-testrunner-toolbar { background-color:#EE5757; } #qunit-testrunner-toolbar { font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; padding:0; /*width:80%;*/ padding:0em 0 0.5em 2em; font-size: small; } h2#qunit-userAgent { font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; background-color:#2b81af; margin:0; padding:0; color:#fff; font-size: small; padding:0.5em 0 0.5em 2.5em; text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; } p#qunit-testresult{ font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; margin:0; font-size: small; color:#2b81af; border-bottom-right-radius:15px; border-bottom-left-radius:15px; -moz-border-radius-bottomright:15px; -moz-border-radius-bottomleft:15px; -webkit-border-bottom-right-radius:15px; -webkit-border-bottom-left-radius:15px; background-color:#D2E0E6; padding:0.5em 0.5em 0.5em 2.5em; } strong b.fail{ color:#710909; } strong b.pass{ color:#5E740B; } ================================================ FILE: test/browser/qunit/qunit.js ================================================ /* * QUnit - A JavaScript Unit Testing Framework * * http://docs.jquery.com/QUnit * * Copyright (c) 2009 John Resig, Jörn Zaefferer * Dual licensed under the MIT (MIT-LICENSE.txt) * and GPL (GPL-LICENSE.txt) licenses. */ (function(window) { var QUnit = { // Initialize the configuration options init: function() { config = { stats: { all: 0, bad: 0 }, moduleStats: { all: 0, bad: 0 }, started: +new Date, updateRate: 1000, blocking: false, autorun: false, assertions: [], filters: [], queue: [] }; var tests = id("qunit-tests"), banner = id("qunit-banner"), result = id("qunit-testresult"); if ( tests ) { tests.innerHTML = ""; } if ( banner ) { banner.className = ""; } if ( result ) { result.parentNode.removeChild( result ); } }, // call on start of module test to prepend name to all tests module: function(name, testEnvironment) { config.currentModule = name; synchronize(function() { if ( config.currentModule ) { QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all ); } config.currentModule = name; config.moduleTestEnvironment = testEnvironment; config.moduleStats = { all: 0, bad: 0 }; QUnit.moduleStart( name, testEnvironment ); }); }, asyncTest: function(testName, expected, callback) { if ( arguments.length === 2 ) { callback = expected; expected = 0; } QUnit.test(testName, expected, callback, true); }, test: function(testName, expected, callback, async) { var name = testName, testEnvironment, testEnvironmentArg; if ( arguments.length === 2 ) { callback = expected; expected = null; } // is 2nd argument a testEnvironment? if ( expected && typeof expected === 'object') { testEnvironmentArg = expected; expected = null; } if ( config.currentModule ) { name = config.currentModule + " module: " + name; } if ( !validTest(name) ) { return; } synchronize(function() { QUnit.testStart( testName ); testEnvironment = extend({ setup: function() {}, teardown: function() {} }, config.moduleTestEnvironment); if (testEnvironmentArg) { extend(testEnvironment,testEnvironmentArg); } // allow utility functions to access the current test environment QUnit.current_testEnvironment = testEnvironment; config.assertions = []; config.expected = expected; try { if ( !config.pollution ) { saveGlobal(); } testEnvironment.setup.call(testEnvironment); } catch(e) { QUnit.ok( false, "Setup failed on " + name + ": " + e.message ); } }); synchronize(function() { if ( async ) { QUnit.stop(); } try { callback.call(testEnvironment); } catch(e) { fail("Test " + name + " died, exception and test follows", e, callback); QUnit.ok( false, "Died on test #" + (config.assertions.length + 1) + ": " + e.message ); // else next test will carry the responsibility saveGlobal(); // Restart the tests if they're blocking if ( config.blocking ) { start(); } } }); synchronize(function() { try { checkPollution(); testEnvironment.teardown.call(testEnvironment); } catch(e) { QUnit.ok( false, "Teardown failed on " + name + ": " + e.message ); } }); synchronize(function() { try { QUnit.reset(); } catch(e) { fail("reset() failed, following Test " + name + ", exception and reset fn follows", e, reset); } if ( config.expected && config.expected != config.assertions.length ) { QUnit.ok( false, "Expected " + config.expected + " assertions, but " + config.assertions.length + " were run" ); } var good = 0, bad = 0, tests = id("qunit-tests"); config.stats.all += config.assertions.length; config.moduleStats.all += config.assertions.length; if ( tests ) { var ol = document.createElement("ol"); ol.style.display = "none"; for ( var i = 0; i < config.assertions.length; i++ ) { var assertion = config.assertions[i]; var li = document.createElement("li"); li.className = assertion.result ? "pass" : "fail"; li.appendChild(document.createTextNode(assertion.message || "(no message)")); ol.appendChild( li ); if ( assertion.result ) { good++; } else { bad++; config.stats.bad++; config.moduleStats.bad++; } } var b = document.createElement("strong"); b.innerHTML = name + " (" + bad + ", " + good + ", " + config.assertions.length + ")"; addEvent(b, "click", function() { var next = b.nextSibling, display = next.style.display; next.style.display = display === "none" ? "block" : "none"; }); addEvent(b, "dblclick", function(e) { var target = e && e.target ? e.target : window.event.srcElement; if ( target.nodeName.toLowerCase() === "strong" ) { var text = "", node = target.firstChild; while ( node.nodeType === 3 ) { text += node.nodeValue; node = node.nextSibling; } text = text.replace(/(^\s*|\s*$)/g, ""); if ( window.location ) { window.location.href = window.location.href.match(/^(.+?)(\?.*)?$/)[1] + "?" + encodeURIComponent(text); } } }); var li = document.createElement("li"); li.className = bad ? "fail" : "pass"; li.appendChild( b ); li.appendChild( ol ); tests.appendChild( li ); if ( bad ) { var toolbar = id("qunit-testrunner-toolbar"); if ( toolbar ) { toolbar.style.display = "block"; id("qunit-filter-pass").disabled = null; id("qunit-filter-missing").disabled = null; } } } else { for ( var i = 0; i < config.assertions.length; i++ ) { if ( !config.assertions[i].result ) { bad++; config.stats.bad++; config.moduleStats.bad++; } } } QUnit.testDone( testName, bad, config.assertions.length ); if ( !window.setTimeout && !config.queue.length ) { done(); } }); if ( window.setTimeout && !config.doneTimer ) { config.doneTimer = window.setTimeout(function(){ if ( !config.queue.length ) { done(); } else { synchronize( done ); } }, 13); } }, /** * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. */ expect: function(asserts) { config.expected = asserts; }, /** * Asserts true. * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); */ ok: function(a, msg) { QUnit.log(a, msg); config.assertions.push({ result: !!a, message: msg }); }, /** * Checks that the first two arguments are equal, with an optional message. * Prints out both actual and expected values. * * Prefered to ok( actual == expected, message ) * * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); * * @param Object actual * @param Object expected * @param String message (optional) */ equal: function(actual, expected, message) { push(expected == actual, actual, expected, message); }, notEqual: function(actual, expected, message) { push(expected != actual, actual, expected, message); }, deepEqual: function(a, b, message) { push(QUnit.equiv(a, b), a, b, message); }, notDeepEqual: function(a, b, message) { push(!QUnit.equiv(a, b), a, b, message); }, strictEqual: function(actual, expected, message) { push(expected === actual, actual, expected, message); }, notStrictEqual: function(actual, expected, message) { push(expected !== actual, actual, expected, message); }, start: function() { // A slight delay, to avoid any current callbacks if ( window.setTimeout ) { window.setTimeout(function() { if ( config.timeout ) { clearTimeout(config.timeout); } config.blocking = false; process(); }, 13); } else { config.blocking = false; process(); } }, stop: function(timeout) { config.blocking = true; if ( timeout && window.setTimeout ) { config.timeout = window.setTimeout(function() { QUnit.ok( false, "Test timed out" ); QUnit.start(); }, timeout); } }, /** * Resets the test setup. Useful for tests that modify the DOM. */ reset: function() { if ( window.jQuery ) { jQuery("#main").html( config.fixture ); jQuery.event.global = {}; jQuery.ajaxSettings = extend({}, config.ajaxSettings); } }, /** * Trigger an event on an element. * * @example triggerEvent( document.body, "click" ); * * @param DOMElement elem * @param String type */ triggerEvent: function( elem, type, event ) { if ( document.createEvent ) { event = document.createEvent("MouseEvents"); event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 0, 0, 0, 0, 0, false, false, false, false, 0, null); elem.dispatchEvent( event ); } else if ( elem.fireEvent ) { elem.fireEvent("on"+type); } }, // Safe object type checking is: function( type, obj ) { return Object.prototype.toString.call( obj ) === "[object "+ type +"]"; }, // Logging callbacks done: function(failures, total) {}, log: function(result, message) {}, testStart: function(name) {}, testDone: function(name, failures, total) {}, moduleStart: function(name, testEnvironment) {}, moduleDone: function(name, failures, total) {} }; // Backwards compatibility, deprecated QUnit.equals = QUnit.equal; QUnit.same = QUnit.deepEqual; // Maintain internal state var config = { // The queue of tests to run queue: [], // block until document ready blocking: true }; // Load paramaters (function() { var location = window.location || { search: "", protocol: "file:" }, GETParams = location.search.slice(1).split('&'); for ( var i = 0; i < GETParams.length; i++ ) { GETParams[i] = decodeURIComponent( GETParams[i] ); if ( GETParams[i] === "noglobals" ) { GETParams.splice( i, 1 ); i--; config.noglobals = true; } else if ( GETParams[i].search('=') > -1 ) { GETParams.splice( i, 1 ); i--; } } // restrict modules/tests by get parameters config.filters = GETParams; // Figure out if we're running the tests from a server or not QUnit.isLocal = !!(location.protocol === 'file:'); })(); // Expose the API as global variables, unless an 'exports' // object exists, in that case we assume we're in CommonJS if ( typeof exports === "undefined" || typeof require === "undefined" ) { extend(window, QUnit); window.QUnit = QUnit; } else { extend(exports, QUnit); exports.QUnit = QUnit; } if ( typeof document === "undefined" || document.readyState === "complete" ) { config.autorun = true; } addEvent(window, "load", function() { // Initialize the config, saving the execution queue var oldconfig = extend({}, config); QUnit.init(); extend(config, oldconfig); config.blocking = false; var userAgent = id("qunit-userAgent"); if ( userAgent ) { userAgent.innerHTML = navigator.userAgent; } var toolbar = id("qunit-testrunner-toolbar"); if ( toolbar ) { toolbar.style.display = "none"; var filter = document.createElement("input"); filter.type = "checkbox"; filter.id = "qunit-filter-pass"; filter.disabled = true; addEvent( filter, "click", function() { var li = document.getElementsByTagName("li"); for ( var i = 0; i < li.length; i++ ) { if ( li[i].className.indexOf("pass") > -1 ) { li[i].style.display = filter.checked ? "none" : ""; } } }); toolbar.appendChild( filter ); var label = document.createElement("label"); label.setAttribute("for", "qunit-filter-pass"); label.innerHTML = "Hide passed tests"; toolbar.appendChild( label ); var missing = document.createElement("input"); missing.type = "checkbox"; missing.id = "qunit-filter-missing"; missing.disabled = true; addEvent( missing, "click", function() { var li = document.getElementsByTagName("li"); for ( var i = 0; i < li.length; i++ ) { if ( li[i].className.indexOf("fail") > -1 && li[i].innerHTML.indexOf('missing test - untested code is broken code') > - 1 ) { li[i].parentNode.parentNode.style.display = missing.checked ? "none" : "block"; } } }); toolbar.appendChild( missing ); label = document.createElement("label"); label.setAttribute("for", "qunit-filter-missing"); label.innerHTML = "Hide missing tests (untested code is broken code)"; toolbar.appendChild( label ); } var main = id('main'); if ( main ) { config.fixture = main.innerHTML; } if ( window.jQuery ) { config.ajaxSettings = window.jQuery.ajaxSettings; } QUnit.start(); }); function done() { if ( config.doneTimer && window.clearTimeout ) { window.clearTimeout( config.doneTimer ); config.doneTimer = null; } if ( config.queue.length ) { config.doneTimer = window.setTimeout(function(){ if ( !config.queue.length ) { done(); } else { synchronize( done ); } }, 13); return; } config.autorun = true; // Log the last module results if ( config.currentModule ) { QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all ); } var banner = id("qunit-banner"), tests = id("qunit-tests"), html = ['Tests completed in ', +new Date - config.started, ' milliseconds.
', '', config.stats.all - config.stats.bad, ' tests of ', config.stats.all, ' passed, ', config.stats.bad,' failed.'].join(''); if ( banner ) { banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); } if ( tests ) { var result = id("qunit-testresult"); if ( !result ) { result = document.createElement("p"); result.id = "qunit-testresult"; result.className = "result"; tests.parentNode.insertBefore( result, tests.nextSibling ); } result.innerHTML = html; } QUnit.done( config.stats.bad, config.stats.all ); } function validTest( name ) { var i = config.filters.length, run = false; if ( !i ) { return true; } while ( i-- ) { var filter = config.filters[i], not = filter.charAt(0) == '!'; if ( not ) { filter = filter.slice(1); } if ( name.indexOf(filter) !== -1 ) { return !not; } if ( not ) { run = true; } } return run; } function push(result, actual, expected, message) { message = message || (result ? "okay" : "failed"); QUnit.ok( result, result ? message + ": " + QUnit.jsDump.parse(expected) : message + ", expected: " + QUnit.jsDump.parse(expected) + " result: " + QUnit.jsDump.parse(actual) ); } function synchronize( callback ) { config.queue.push( callback ); if ( config.autorun && !config.blocking ) { process(); } } function process() { var start = (new Date()).getTime(); while ( config.queue.length && !config.blocking ) { if ( config.updateRate <= 0 || (((new Date()).getTime() - start) < config.updateRate) ) { config.queue.shift()(); } else { setTimeout( process, 13 ); break; } } } function saveGlobal() { config.pollution = []; if ( config.noglobals ) { for ( var key in window ) { config.pollution.push( key ); } } } function checkPollution( name ) { var old = config.pollution; saveGlobal(); var newGlobals = diff( old, config.pollution ); if ( newGlobals.length > 0 ) { ok( false, "Introduced global variable(s): " + newGlobals.join(", ") ); config.expected++; } var deletedGlobals = diff( config.pollution, old ); if ( deletedGlobals.length > 0 ) { ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") ); config.expected++; } } // returns a new Array with the elements that are in a but not in b function diff( a, b ) { var result = a.slice(); for ( var i = 0; i < result.length; i++ ) { for ( var j = 0; j < b.length; j++ ) { if ( result[i] === b[j] ) { result.splice(i, 1); i--; break; } } } return result; } function fail(message, exception, callback) { if ( typeof console !== "undefined" && console.error && console.warn ) { console.error(message); console.error(exception); console.warn(callback.toString()); } else if ( window.opera && opera.postError ) { opera.postError(message, exception, callback.toString); } } function extend(a, b) { for ( var prop in b ) { a[prop] = b[prop]; } return a; } function addEvent(elem, type, fn) { if ( elem.addEventListener ) { elem.addEventListener( type, fn, false ); } else if ( elem.attachEvent ) { elem.attachEvent( "on" + type, fn ); } else { fn(); } } function id(name) { return !!(typeof document !== "undefined" && document && document.getElementById) && document.getElementById( name ); } // Test for equality any JavaScript type. // Discussions and reference: http://philrathe.com/articles/equiv // Test suites: http://philrathe.com/tests/equiv // Author: Philippe Rathé QUnit.equiv = function () { var innerEquiv; // the real equiv function var callers = []; // stack to decide between skip/abort functions var parents = []; // stack to avoiding loops from circular referencing // Determine what is o. function hoozit(o) { if (QUnit.is("String", o)) { return "string"; } else if (QUnit.is("Boolean", o)) { return "boolean"; } else if (QUnit.is("Number", o)) { if (isNaN(o)) { return "nan"; } else { return "number"; } } else if (typeof o === "undefined") { return "undefined"; // consider: typeof null === object } else if (o === null) { return "null"; // consider: typeof [] === object } else if (QUnit.is( "Array", o)) { return "array"; // consider: typeof new Date() === object } else if (QUnit.is( "Date", o)) { return "date"; // consider: /./ instanceof Object; // /./ instanceof RegExp; // typeof /./ === "function"; // => false in IE and Opera, // true in FF and Safari } else if (QUnit.is( "RegExp", o)) { return "regexp"; } else if (typeof o === "object") { return "object"; } else if (QUnit.is( "Function", o)) { return "function"; } else { return undefined; } } // Call the o related callback with the given arguments. function bindCallbacks(o, callbacks, args) { var prop = hoozit(o); if (prop) { if (hoozit(callbacks[prop]) === "function") { return callbacks[prop].apply(callbacks, args); } else { return callbacks[prop]; // or undefined } } } var callbacks = function () { // for string, boolean, number and null function useStrictEquality(b, a) { if (b instanceof a.constructor || a instanceof b.constructor) { // to catch short annotaion VS 'new' annotation of a declaration // e.g. var i = 1; // var j = new Number(1); return a == b; } else { return a === b; } } return { "string": useStrictEquality, "boolean": useStrictEquality, "number": useStrictEquality, "null": useStrictEquality, "undefined": useStrictEquality, "nan": function (b) { return isNaN(b); }, "date": function (b, a) { return hoozit(b) === "date" && a.valueOf() === b.valueOf(); }, "regexp": function (b, a) { return hoozit(b) === "regexp" && a.source === b.source && // the regex itself a.global === b.global && // and its modifers (gmi) ... a.ignoreCase === b.ignoreCase && a.multiline === b.multiline; }, // - skip when the property is a method of an instance (OOP) // - abort otherwise, // initial === would have catch identical references anyway "function": function () { var caller = callers[callers.length - 1]; return caller !== Object && typeof caller !== "undefined"; }, "array": function (b, a) { var i, j, loop; var len; // b could be an object literal here if ( ! (hoozit(b) === "array")) { return false; } len = a.length; if (len !== b.length) { // safe and faster return false; } //track reference to avoid circular references parents.push(a); for (i = 0; i < len; i++) { loop = false; for(j=0;j= 0) { type = "array"; } else { type = typeof obj; } return type; }, separator:function() { return this.multiline ? this.HTML ? '
' : '\n' : this.HTML ? ' ' : ' '; }, indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing if ( !this.multiline ) return ''; var chr = this.indentChar; if ( this.HTML ) chr = chr.replace(/\t/g,' ').replace(/ /g,' '); return Array( this._depth_ + (extra||0) ).join(chr); }, up:function( a ) { this._depth_ += a || 1; }, down:function( a ) { this._depth_ -= a || 1; }, setParser:function( name, parser ) { this.parsers[name] = parser; }, // The next 3 are exposed so you can use them quote:quote, literal:literal, join:join, // _depth_: 1, // This is the list of parsers, to modify them, use jsDump.setParser parsers:{ window: '[Window]', document: '[Document]', error:'[ERROR]', //when no parser is found, shouldn't happen unknown: '[Unknown]', 'null':'null', undefined:'undefined', 'function':function( fn ) { var ret = 'function', name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE if ( name ) ret += ' ' + name; ret += '('; ret = [ ret, this.parse( fn, 'functionArgs' ), '){'].join(''); return join( ret, this.parse(fn,'functionCode'), '}' ); }, array: array, nodelist: array, arguments: array, object:function( map ) { var ret = [ ]; this.up(); for ( var key in map ) ret.push( this.parse(key,'key') + ': ' + this.parse(map[key]) ); this.down(); return join( '{', ret, '}' ); }, node:function( node ) { var open = this.HTML ? '<' : '<', close = this.HTML ? '>' : '>'; var tag = node.nodeName.toLowerCase(), ret = open + tag; for ( var a in this.DOMAttrs ) { var val = node[this.DOMAttrs[a]]; if ( val ) ret += ' ' + a + '=' + this.parse( val, 'attribute' ); } return ret + close + open + '/' + tag + close; }, functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function var l = fn.length; if ( !l ) return ''; var args = Array(l); while ( l-- ) args[l] = String.fromCharCode(97+l);//97 is 'a' return ' ' + args.join(', ') + ' '; }, key:quote, //object calls it internally, the key part of an item in a map functionCode:'[code]', //function calls it internally, it's the content of the function attribute:quote, //node calls it internally, it's an html attribute value string:quote, date:quote, regexp:literal, //regex number:literal, 'boolean':literal }, DOMAttrs:{//attributes to dump from nodes, name=>realName id:'id', name:'name', 'class':'className' }, HTML:false,//if true, entities are escaped ( <, >, \t, space and \n ) indentChar:' ',//indentation unit multiline:false //if true, items in a collection, are separated by a \n, else just a space. }; return jsDump; })(); })(this); ================================================ FILE: test/browser/tasks.client.js ================================================ // Data model var Task = persistence.define('Task', { name: "TEXT", done: "BOOL", lastChange: "DATE" }); persistence.connect('taskdemo', 'database', 5 * 1024 * 1024, '1.0'); persistence.schemaSync(); function syncAll() { persistence.sync.synchronize('/recentChanges', Task, function(conflicts, updatesToPush, callback) { console.log(conflicts); callback(); }); } function addTask() { var t = new Task(); t.name = "Some new local task"; t.done = false; t.lastChange = new Date(); persistence.add(t); persistence.flush(); } ================================================ FILE: test/browser/tasks.html ================================================

Tasks

Hello!

================================================ FILE: test/browser/test.jquery-persistence.html ================================================

persistence.js core tests with jquery-plugin

    ================================================ FILE: test/browser/test.jquery-persistence.js ================================================ $(document).ready(function(){ persistence.store.websql.config(persistence, 'persistencetest', 'My db', 5 * 1024 * 1024); //persistence.store.memory.config(persistence); persistence.debug = true; var Project = persistence.define('Project', { name: "TEXT" }); var Task = persistence.define('Task', { name: "TEXT", done: "BOOL", counter: "INT", dateAdded: "DATE", metaData: "JSON" }); var Tag = persistence.define('Tag', { name: "TEXT" }); Task.hasMany('tags', Tag, 'tasks'); Tag.hasMany('tasks', Task, 'tags'); Project.hasMany('tasks', Task, 'project'); window.Project = Project; window.Task = Task window.Project = Project; module("Setup"); asyncTest("setting up database", 1, function() { persistence.schemaSync(function(tx){ ok(true, 'schemaSync called callback function'); start(); }); }); module("Entity manipulation", { setup: function() { stop(); persistence.reset(function() { persistence.schemaSync(start); }); } }); test("Property default values", 5, function() { var t1 = new Task(); QUnit.strictEqual($(t1).data('name'), "", "TEXT properties default to ''"); QUnit.strictEqual($(t1).data('done'), false, "BOOL properties default to false"); QUnit.strictEqual($(t1).data('counter'), 0, "INT properties default to 0"); QUnit.strictEqual($(t1).data('dateAdded'), null, "DATE properties default to null"); QUnit.strictEqual($(t1).data('metaData'), null, "JSON properties default to null"); }); test("Property value assignment", 5, function() { var t1 = new Task(); var now = new Date(); var meta = {starRating: 5}; $(t1).data('name', "Task 1"); $(t1).data('done', false); $(t1).data('counter', 7); $(t1).data('dateAdded', now); $(t1).data('metaData', meta); QUnit.strictEqual($(t1).data('name'), 'Task 1', "Assignment for TEXT properties"); QUnit.strictEqual($(t1).data('done'), false, "Assignment for BOOL properties"); QUnit.strictEqual($(t1).data('counter'), 7, "Assignment for INT properties"); QUnit.strictEqual($(t1).data('dateAdded'), now, "Assignment for DATE properties"); QUnit.strictEqual($(t1).data('metaData'), meta, "Assignment for JSON properties"); }); test("Property contructor property value assignment", 5, function() { var now = new Date(); var meta = {starRating: 5}; var t1 = new Task({ name: "Task 1", done: false, counter: 7, dateAdded: now, metaData: meta }); QUnit.strictEqual($(t1).data('name'), 'Task 1', "Assignment for TEXT properties"); QUnit.strictEqual($(t1).data('done'), false, "Assignment for BOOL properties"); QUnit.strictEqual($(t1).data('counter'), 7, "Assignment for INT properties"); QUnit.strictEqual($(t1).data('dateAdded'), now, "Assignment for DATE properties"); QUnit.strictEqual($(t1).data('metaData'), meta, "Assignment for JSON properties"); }); asyncTest("Empty object persistence", function() { var t1 = new Task(); persistence.add(t1); persistence.flush(function() { Task.all().one(function(t1db) { equals(t1db.id, t1.id, "TEXT properties default to ''"); equals($(t1db).data('name'), "", "TEXT properties default to ''"); equals($(t1db).data('done'), false, "BOOL properties default to false"); equals($(t1db).data('counter'), 0, "INT properties default to 0"); equals($(t1db).data('dateAdded'), null, "DATE properties default to null"); equals($(t1db).data('metaData'), null, "JSON properties default to null"); start(); }); }); }); asyncTest("Object persistence", function() { var now = new Date(); var meta = {starRating: 5}; var t1 = new Task({ name: "Task 1", done: false, counter: 7, dateAdded: now, metaData: meta }); persistence.add(t1); persistence.flush(function() { //persistence.clean(); Task.all().one(function(t1db) { equals($(t1db).data('name'), 'Task 1', "Persistence of TEXT properties"); equals($(t1db).data('done'), false, "Persistence of BOOL properties"); equals($(t1db).data('counter'), 7, "Persistence of INT properties"); equals(Math.round($(t1db).data('dateAdded').getTime()/1000)*1000, Math.round(now.getTime()/1000)*1000, "Persistence of DATE properties"); same($(t1db).data('metaData'), meta, "Persistence of JSON properties"); start(); }); }); }); asyncTest("Multiple objects", function() { var objs = []; var counter = 0; for(var i = 0; i < 25; i++) { var t = new Task({name: "Task " + i}); $(t).data('counter', counter); objs.push(t); persistence.add(t); counter++; } persistence.flush(function() { Task.all().order('counter', true).list(function(results) { for(var i = 0; i < 25; i++) { ok(results[i] === objs[i], 'Cache works OK'); } //persistence.clean(); // Clean out local cache Task.all().order('counter', true).list(function(results) { for(var i = 0; i < 25; i++) { ok(results[i].id === objs[i].id, 'Retrieving from DB ok'); } start(); }); }); }); }); asyncTest("One-to-many", function() { var p = new Project({name: "Some project"}); persistence.add(p); $(p).data('tasks').list(function(tasks) { equals(tasks.length, 0, "Initially, no tasks"); var task1 = new Task({name: "Do dishes"}); var task2 = new Task({name: "Laundry"}); // Adding in two ways $(p).data('tasks').add(task1); $(task2).data('project', p); $(p).data('tasks').order('name', true).list(function(tasks) { equals(tasks.length, 2, "Now two tasks"); equals(tasks[0].id, task1.id, "Right tasks"); equals(tasks[1].id, task2.id, "Right tasks"); start(); }); }); }); asyncTest("Many-to-many", function() { var t = new Task({name: "Some task"}); persistence.add(t); $(t).data('tags').list(function(tags) { equals(tags.length, 0, "Initially, no tags"); var tag1 = new Tag({name: "important"}); var tag2 = new Tag({name: "today"}); $(t).data('tags').add(tag1); $(t).data('tags').add(tag2); $(t).data('tags').list(function(tags) { equals(tags.length, 2, "2 tags added"); var oneTag = tags[0]; $(oneTag).data('tasks').list(function(tagTasks) { equals(tagTasks.length, 1, "Tag has one task"); equals(tagTasks[0].id, t.id, "Correct task"); $(oneTag).data('tasks').remove(tagTasks[0]); $(t).data('tags').list(function(newTags) { equals(newTags.length, 1, "Tag removed task, task has only one tag left"); start(); }); }); }); }); }); module("Query collections", { setup: function() { stop(); persistence.reset(function() { persistence.schemaSync(start); }); } }); function intFilterTests(coll, callback) { for(var i = 0; i < 25; i++) { var t = new Task({name: "Task " + i, done: false}); $(t).data('counter', i); coll.add(t); } coll.list(function(results) { equals(results.length, 25, "Count items in collection"); coll.filter("counter", ">", 10).list(function(results) { equals(results.length, 14, "> filter test"); coll.filter("counter", "in", [0, 1, 2]).list(function(results) { equals(results.length, 3, "'in' filter test"); coll.filter("counter", "not in", [0, 1]).list(function(results) { equals(results.length, 23, "'not in' filter test"); coll.filter("counter", "!=", 0).list(function(results) { equals(results.length, 24, "'!=' filter test"); callback(); }); }); }); }); }); } function textFilterTests(coll, callback) { var alphabet = 'abcdefghijklmnopqrstufwxyz'; for(var i = 0; i <= 25; i++) { var t = new Task({name: alphabet[i]}); coll.add(t); } coll.list(function(results) { equals(results.length, 26, "Count items in collection"); coll.filter("name", "=", 'a').list(function(results) { equals(results.length, 1, "= filter test"); coll.filter("name", "!=", 'a').list(function(results) { equals(results.length, 25, "!= filter test"); coll.filter("name", ">", 'm').list(function(results) { equals(results.length, 12, "> filter test"); coll.filter("name", "in", ["a", "b"]).list(function(results) { equals(results.length, 2, "'in' filter test"); coll.filter("name", "not in", ["q", "x"]).list(function(results) { equals(results.length, 24, "'not in' filter test"); callback(); }); }); }); }); }); }); } function boolFilterTests(coll, callback) { for(var i = 0; i < 24; i++) { var t = new Task({name: "Task " + i, done: i % 2 === 0}); coll.add(t); } coll.list(function(results) { equals(results.length, 24, "Count items in collection"); coll.filter("done", "=", true).list(function(results) { equals(results.length, 12, "= filter test"); coll.filter("done", "=", false).list(function(results) { equals(results.length, 12, "= filter test"); coll.filter("done", "!=", true).list(function(results) { equals(results.length, 12, "'!=' filter test"); coll.filter("done", "!=", false).list(function(results) { equals(results.length, 12, "'!=' filter test"); callback(); }); }); }); }); }); } function dateFilterTests(coll, callback) { var now = new Date(); function dateInDays(n) { var newDate = new Date(now.getTime()); newDate.setDate(newDate.getDate()+n); return newDate; } for(var i = 0; i < 24; i++) { var t = new Task({name: "Task " + i, dateAdded: dateInDays(i)}); coll.add(t); } coll.list(function(results) { equals(results.length, 24, "Count items in collection"); coll.filter("dateAdded", "=", dateInDays(1)).list(function(results) { equals(results.length, 1, "= filter test"); coll.filter("dateAdded", "!=", dateInDays(1)).list(function(results) { equals(results.length, 23, "= filter test"); coll.filter("dateAdded", ">", dateInDays(12)).list(function(results) { equals(results.length, 11, "> filter test"); start(); }); }) }); }); } asyncTest("Database INT filters", function() { for(var i = 0; i < 25; i++) { var t = new Task({name: "Root task " + i, done: false}); $(t).data('counter', i); persistence.add(t); } var p = new Project({name: "My project"}); persistence.add(p); intFilterTests($(p).data('tasks'), start); }); asyncTest("Local INT filters", function() { var coll = new persistence.LocalQueryCollection(); intFilterTests(coll, start); }); asyncTest("Database TEXT filters", function() { var alphabet = 'abcdefghijklmnopqrstufwxyz'; for(var i = 0; i <= 25; i++) { var t = new Task({name: alphabet[i]}); persistence.add(t); } var p = new Project({name: "My project"}); persistence.add(p); textFilterTests($(p).data('tasks'), start); }); asyncTest("Local TEXT filters", function() { var coll = new persistence.LocalQueryCollection(); textFilterTests(coll, start); }); asyncTest("Database BOOL filters", function() { for(var i = 0; i < 25; i++) { var t = new Task({name: "Root task " + i, done: false}); $(t).data('counter', i); persistence.add(t); } var p = new Project({name: "My project"}); persistence.add(p); boolFilterTests($(p).data('tasks'), start); }); asyncTest("Local BOOL filters", function() { var coll = new persistence.LocalQueryCollection(); boolFilterTests(coll, start); }); asyncTest("Database DATE filters", function() { var p = new Project({name: "My project"}); persistence.add(p); dateFilterTests($(p).data('tasks'), start); }); asyncTest("Local DATE filters", function() { var coll = new persistence.LocalQueryCollection(); dateFilterTests(coll, start); }); function intOrderTests(coll, callback) { var tasks = []; for(var i = 0; i < 24; i++) { var t = new Task({name: "Task " + i, counter: i}); tasks.push(t); coll.add(t); } coll.order('counter', true).list(function(results) { for(var i = 0; i < 24; i++) { equals(results[i].id, tasks[i].id, "order check, ascending"); } tasks.reverse(); coll.order('counter', false).list(function(results) { for(var i = 0; i < 24; i++) { equals(results[i].id, tasks[i].id, "order check, descending"); } callback(); }); }); } function dateOrderTests(coll, callback) { var now = new Date(); function dateInDays(n) { var newDate = new Date(now.getTime()); newDate.setDate(newDate.getDate()+n); return newDate; } var tasks = []; for(var i = 0; i < 24; i++) { var t = new Task({name: "Task " + i, dateAdded: dateInDays(i)}); tasks.push(t); coll.add(t); } coll.order('dateAdded', true).list(function(results) { for(var i = 0; i < 24; i++) { equals(results[i].id, tasks[i].id, "order check, ascending"); } tasks.reverse(); coll.order('dateAdded', false).list(function(results) { for(var i = 0; i < 24; i++) { equals(results[i].id, tasks[i].id, "order check, descending"); } callback(); }); }); } asyncTest("Database INT order", function() { var p = new Project({name: "My project"}); persistence.add(p); intOrderTests($(p).data('tasks'), start); }); asyncTest("Local INT order", function() { var coll = new persistence.LocalQueryCollection(); intOrderTests(coll, start); }); asyncTest("Database DATE order", function() { var p = new Project({name: "My project"}); persistence.add(p); dateOrderTests($(p).data('tasks'), start); }); asyncTest("Local DATE order", function() { var coll = new persistence.LocalQueryCollection(); dateOrderTests(coll, start); }); function collectionLimitTests(coll, callback) { var tasks = []; for(var i = 0; i < 24; i++) { var t = new Task({name: "Task " + i, counter: i}); tasks.push(t); coll.add(t); } coll.order("counter", true).limit(5).list(function(results) { equals(results.length, 5, "Result length check"); for(var i = 0; i < 5; i++) { equals(results[i].id, tasks[i].id, "limit check"); } start(); }); } function collectionSkipTests(coll, callback) { var tasks = []; for(var i = 0; i < 24; i++) { var t = new Task({name: "Task " + i, counter: i}); tasks.push(t); coll.add(t); } coll.order("counter", true).skip(5).limit(5).list(function(results) { equals(results.length, 5, "Result length check"); for(var i = 5; i < 10; i++) { equals(results[i-5].id, tasks[i].id, "skip check"); } start(); }); } asyncTest("Database limit", function() { collectionLimitTests(Task.all(), start); }); asyncTest("Local limit", function() { var coll = new persistence.LocalQueryCollection(); collectionLimitTests(coll, start); }); asyncTest("Database skip", function() { collectionSkipTests(Task.all(), start); }); asyncTest("Local skip", function() { var coll = new persistence.LocalQueryCollection(); collectionSkipTests(coll, start); }); }); ================================================ FILE: test/browser/test.migrations.html ================================================

    persistence.js migrations plugin

      ================================================ FILE: test/browser/test.migrations.js ================================================ function createMigrations(starting, amount, actions){ var amount = starting+amount; for (var i = starting; i < amount; i++) { var newActions = { up: actions.up, down: actions.down }; if (actions.createDown) newActions.down = actions.createDown(i); if (actions.createUp) newActions.up = actions.createUp(i); persistence.defineMigration(i, newActions); } } var Migrator = persistence.migrations.Migrator; $(document).ready(function(){ persistence.store.websql.config(persistence, 'migrationstest', 'My db', 5 * 1024 * 1024); persistence.debug = true; persistence.migrations.init(function() { module("Migrator", { setup: function() { }, teardown: function() { stop(); Migrator.reset(start); } }); asyncTest("getting and setting db version", 2, function() { Migrator.version(function(v){ equals(v, 0, 'initial db version'); }); var newVersion = 100; Migrator.setVersion(newVersion, function() { Migrator.version(function(v){ equals(v, newVersion, 'checking if version was set'); start(); }); }); }); asyncTest("migrations scope", 2, function(){ var migration = Migrator.migration(1, { up: function() { same(this, migration, 'up'); }, down: function() { same(this, migration, 'down'); } }); migration.up(function(){ migration.down(function(){ start(); }); }); }); asyncTest("migrating up to some version", 7, function(){ var actionsRan = 0; var totalActions = 5; createMigrations(1, totalActions, { up: function() { actionsRan++; equals(this.version, actionsRan, 'running migration in order'); } }); Migrator.migrate(totalActions, function(){ equals(actionsRan, totalActions, 'actions ran'); Migrator.version(function(v){ equals(v, totalActions, 'version changed to'); start(); }); }); }); asyncTest("migrating down to some version", 7, function(){ var actionsRan = 0; var totalActions = 5; createMigrations(1, totalActions, { createDown: function(i) { var position = Math.abs(actionsRan - i); return function () { actionsRan++; equals(this.version, position, 'running migration in order'); }; } }); Migrator.setVersion(totalActions, function(){ Migrator.migrate(0, function(){ equals(actionsRan, totalActions, 'actions ran'); Migrator.version(function(v){ equals(v, 0, 'version changed to'); start(); }); }); }); }); asyncTest("migrate to latest", 1, function(){ var totalActions = 3; createMigrations(1, totalActions, { up: function() { } }); Migrator.migrate(function() { Migrator.version(function(v){ equals(v, totalActions, 'latest version'); start(); }); }); }); module("Migration", { setup: function() { }, teardown: function() { stop(); // DROPS ALL TABLES var query = "select 'drop table ' || name || ';' AS dropTable from sqlite_master where type = 'table' and name not in ('__WebKitDatabaseInfoTable__', 'schema_version')"; persistence.transaction(function(tx){ tx.executeSql(query, null, function(result){ var dropTablesSql = []; for (var i = 0; i < result.length; i++) dropTablesSql.push([result[i].dropTable, null]); persistence.executeQueriesSeq(tx, dropTablesSql, function(){ Migrator.setVersion(0, function(){Migrator.reset(start);}); }); }); }); } }); asyncTest("API", 12, function(){ var m = Migrator.migration(1, { up: function() { ok(typeof(this.addColumn) == "function", 'addColumn'); ok(typeof(this.removeColumn) == "function", 'removeColumn'); ok(typeof(this.addIndex) == "function", 'addIndex'); ok(typeof(this.removeIndex) == "function", 'removeIndex'); ok(typeof(this.executeSql) == "function", 'execute'); ok(typeof(this.dropTable) == "function", 'dropTable'); ok(typeof(this.createTable) == "function", 'createTable'); this.createTable('posts', function(table){ ok(typeof(table.text) == "function", 'text column'); ok(typeof(table.integer) == "function", 'integer column'); ok(typeof(table.boolean) == "function", 'boolean column'); ok(typeof(table.json) == "function", 'json column'); ok(typeof(table.date) == "function", 'date column'); }); } }); m.up(start); }); asyncTest("execute", 1, function(){ Migrator.migration(1, { up: function() { this.executeSql('CREATE TABLE test (id INTEGER)'); } }); Migrator.migrate(function(){ var sql = 'select name from sqlite_master where type = "table" and name == "test"'; persistence.transaction(function(tx){ tx.executeSql(sql, null, function(result){ ok(result.length == 1, 'sql command ran'); start(); }); }); }); }); asyncTest("createTable", 1, function(){ Migrator.migration(1, { up: function() { this.createTable('testing'); } }); Migrator.migrate(function(){ tableExists('testing', start) }); }); asyncTest("createTable adds id by default", 1, function(){ Migrator.migration(1, { up: function() { this.createTable('testing'); } }); Migrator.migrate(function(){ columnExists('testing', 'id', 'VARCHAR(32) PRIMARY KEY', start); }); }); asyncTest("createTable with text column", 1, function(){ Migrator.migration(1, { up: function() { this.createTable('customer', function(t){ t.text('name'); }); } }); Migrator.migrate(function(){ columnExists('customer', 'name', 'TEXT', start); }); }); asyncTest("createTable with integer column", 1, function(){ Migrator.migration(1, { up: function() { this.createTable('customer', function(t){ t.integer('age'); }); } }); Migrator.migrate(function(){ columnExists('customer', 'age', 'INT', start); }); }); asyncTest("createTable with boolean column", 1, function(){ Migrator.migration(1, { up: function() { this.createTable('customer', function(t){ t.boolean('married'); }); } }); Migrator.migrate(function(){ columnExists('customer', 'married', 'BOOL', start); }); }); asyncTest("createTable with date column", 1, function(){ Migrator.migration(1, { up: function() { this.createTable('customer', function(t){ t.date('birth'); }); } }); Migrator.migrate(function(){ columnExists('customer', 'birth', 'DATE', start); }); }); asyncTest("createTable with json column", 1, function(){ Migrator.migration(1, { up: function() { this.createTable('customer', function(t){ t.json('sample_json'); }); } }); Migrator.migrate(function(){ columnExists('customer', 'sample_json', 'TEXT', start); }); }); asyncTest("addColumn", 1, function(){ Migrator.migration(1, { up: function() { this.createTable('customer'); this.addColumn('customer', 'name', 'TEXT'); } }); Migrator.migrate(function(){ columnExists('customer', 'name', 'TEXT', start); }); }); asyncTest("removeColumn", 2, function(){ Migrator.migration(1, { up: function() { this.createTable('customer', function(t){ t.json('sample_json'); }); this.removeColumn('customer', 'sample_json'); } }); Migrator.migrate(function(){ columnExists('customer', 'id', 'VARCHAR(32) PRIMARY KEY'); columnNotExists('customer', 'sample_json', 'TEXT', start); }); }); asyncTest("dropTable", 1, function(){ Migrator.migration(1, { up: function() { this.createTable('customer'); this.dropTable('customer'); } }); Migrator.migrate(function(){ tableNotExists('customer', start); }); }); asyncTest("addIndex", 1, function(){ Migrator.migration(1, { up: function() { this.createTable('customer', function(t){ t.integer('age'); }); this.addIndex('customer', 'age'); } }); Migrator.migrate(function(){ indexExists('customer', 'age', start); }); }); asyncTest("removeIndex", 1, function(){ Migrator.migration(1, { up: function() { this.createTable('customer', function(t){ t.integer('age'); }); this.addIndex('customer', 'age'); this.removeIndex('customer', 'age'); } }); Migrator.migrate(function(){ indexNotExists('customer', 'age', start); }); }); module("Models", { setup: function() { stop(); this.Task = persistence.define('Task', { name: "TEXT", description: "TEXT", done: "BOOL" }); Migrator.migration(1, { up: function() { this.createTable('Task', function(t){ t.text('name'); t.text('description'); t.boolean('done'); }); }, down: function() { this.dropTable('Task'); } }); Migrator.migrate(function(){ start(); }); }, teardown: function() { stop(); Migrator.migrate(0, function(){ start(); }); } }); asyncTest("Adding and retrieving Entity after migration", 1, function(){ var task = new this.Task({name: 'test'}); var allTasks = this.Task.all(); persistence.add(task).flush(function() { persistence.clean(); delete task; allTasks.list(function(result){ equals(result.length, 1, 'task found'); start(); }); }); }); module("Custom actions", { setup: function() { stop(); this.User = persistence.define('User', { userName: "TEXT", email: "TEXT" }); Migrator.migration(1, { up: function() { this.createTable('User', function(t){ t.text('userName'); }); }, down: function() { this.dropTable('User'); } }); Migrator.migrate(function(){ start(); }); }, teardown: function() { stop(); Migrator.migrate(0, function(){ start(); }); } }); asyncTest("Running custom actions", 1, function(){ var user1 = new this.User({userName: 'user1'}); var user2 = new this.User({userName: 'user2'}); var allUsers = this.User.all(); function addUsers() { persistence.add(user1).add(user2).flush(createAndRunMigration); } function createAndRunMigration() { Migrator.migration(2, { up: function() { this.addColumn('User', 'email', 'TEXT'); this.action(function(tx, nextAction){ ok(true); nextAction(); }); } }); Migrator.migrate(start); } addUsers(); }); }); // end persistence.migrations.init() }); ================================================ FILE: test/browser/test.mixin.html ================================================

      persistence.js mixin tests

        ================================================ FILE: test/browser/test.mixin.js ================================================ $(document).ready(function(){ persistence.store.websql.config(persistence, 'persistencetest', 'My db', 5 * 1024 * 1024); //persistence.store.memory.config(persistence); persistence.debug = true; //persistence.debug = false; var startTime = new Date().getTime(); var Project = persistence.define('Project', { name: "TEXT" }); var Task = persistence.define('Task', { name: "TEXT" }); var Tag = persistence.define('Tag', { name: "TEXT" }); Task.hasMany('tags', Tag, 'tasks'); Tag.hasMany('tasks', Task, 'tags'); Project.hasMany('tasks', Task, 'project'); var Note = persistence.define('Note', { text: "TEXT" }); var Annotatable = persistence.defineMixin('Annotatable', { lastAnnotated: "DATE" }); Annotatable.hasMany('notes', Note, 'annotated'); Task.hasMany('tags', Tag, 'tasks'); Tag.hasMany('tasks', Task, 'tags'); Project.hasMany('tasks', Task, 'project'); Task.is(Annotatable); Project.is(Annotatable); var M1 = persistence.defineMixin('M1', { seq: "INT", m1: "TEXT" }); var M2 = persistence.defineMixin('M2', { seq: "INT", m2: "TEXT" }); M1.hasOne('oneM2', M2); M1.hasMany('manyM2', M2, 'oneM1'); M1.hasMany('manyManyM2', M2, 'manyManyM1'); M2.hasMany('manyManyM1', M1, 'manyManyM2'); var A1 = persistence.define('A1', { a1: 'TEXT' }); var A2 = persistence.define('A2', { a2: 'TEXT' }); var B1 = persistence.define('B1', { b1: 'TEXT' }); var B2 = persistence.define('B2', { b2: 'TEXT' }); A1.is(M1); A2.is(M2); B1.is(M1); B2.is(M2); window.Project = Project; window.Task = Task window.Project = Project; module("Setup"); asyncTest("setting up database", 1, function(){ persistence.schemaSync(function(tx){ ok(true, 'schemaSync called callback function'); start(); }); }); module("Annotatable mixin", { setup: function() { stop(); persistence.reset(function() { persistence.schemaSync(start); }); } }); asyncTest("basic mixin", 7, function(){ var now = new Date(); now.setMilliseconds(0); var p = new Project({ name: "project p" }); persistence.add(p); var n1 = new Note({ text: "note 1" }); var n2 = new Note({ text: "note 2" }); p.notes.add(n1); n2.annotated = p; p.lastAnnotated = now; persistence.flush(function(){ Project.all().list(function(projects){ persistence.clean(); equals(projects.length, 1) var p = projects[0]; p.notes.order('text', true).list(function(notes){ equals(notes.length, 2); equals(notes[0].text, "note 1"); equals(notes[1].text, "note 2"); notes[0].fetch("annotated", function(source){ equals(p.id, source.id); equals(typeof source.lastAnnotated, typeof now); equals(source.lastAnnotated.getTime(), now.getTime()); start(); }); }); }); }); }); asyncTest("many to many with mixins", 17, function(){ var a1 = new A1({ seq: 1, a1: "a1" }); var b1 = new B1({ seq: 2, b1: "b1" }); var a2 = new A2({ seq: 3, a2: "a2" }); var a2x = new A2({ seq: 4, a2: "a2x" }); var a2y = new A2({ seq: 5, a2: "a2y" }); var b2x = new B2({ seq: 6, b2: "b2x" }); var b2y = new B2({ seq: 7, b2: "b2y" }); persistence.add(a1); a1.oneM2 = b2x; a1.manyM2.add(a2x); a1.manyM2.add(b2x); persistence.flush(function(){ persistence.clean(); A1.all().list(function(a1s){ equals(a1s.length, 1, "A1 list ok") var a1 = a1s[0]; a1.fetch("oneM2", function(m2){ ok(m2 != null, "oneM2 not null"); equals(m2.b2, "b2x", "oneM2 ok"); a1.manyM2.order('seq', true).list(function(m2s){ equals(m2s.length, 2, "manyM2 length ok"); equals(m2s[0].a2, "a2x", "manyM2[0] ok"); equals(m2s[1].b2, "b2x", "manyM2[1] ok"); m2s[1].fetch("oneM1", function(m1){ ok(m1 != null, "manyM2[1].oneM1 not null"); ok(m1.a1, "a1", "manyM2[1].oneM1 ok"); a1.manyManyM2.add(a2x); a1.manyManyM2.add(b2x); persistence.add(b2y); b2y.manyManyM1.add(a1); b2y.manyManyM1.add(b1); persistence.flush(function(){ persistence.clean(); A1.all().list(function(a1s){ equals(a1s.length, 1, "A1 list ok") var a1 = a1s[0]; a1.manyManyM2.order('seq', true).list(function(m2s){ equals(m2s.length, 3, "manyManyM2 length ok"); equals(m2s[0].a2, "a2x", "manyManyM2[0] ok"); equals(m2s[1].b2, "b2x", "manyManyM2[1] ok"); equals(m2s[2].b2, "b2y", "manyManyM2[2] ok"); m2s[2].manyManyM1.order('seq', true).list(function(m1s){ equals(m1s.length, 2, "manyManyM1 length ok"); equals(m1s[0].a1, "a1", "manyManyM1[0] ok"); equals(m1s[1].b1, "b1", "manyManyM1[1] ok"); a1.manyManyM2.count(function(count){ equals(count, 3, "count ok on polymorphic list"); //a1.manyManyM2.destroyAll(function(){ start(); //}) }); }) }); }); }) }); }); }); }); }); }); }); ================================================ FILE: test/browser/test.persistence.html ================================================

        persistence.js core tests

          ================================================ FILE: test/browser/test.persistence.js ================================================ $(document).ready(function(){ persistence.store.websql.config(persistence, 'persistencetest', 'My db', 5 * 1024 * 1024); //persistence.store.memory.config(persistence); persistence.debug = true; //persistence.debug = false; var startTime = new Date().getTime(); var Project = persistence.define('Project', { name: "TEXT" }); var Task = persistence.define('Task', { name: "TEXT", done: "BOOL", counter: "INT", dateAdded: "DATE", dateAddedInMillis: "BIGINT", metaData: "JSON" }); var Tag = persistence.define('Tag', { name: "TEXT" }); var UniqueIndexTest = persistence.define('UniqueIndexTest', { id1: "INT", id2: "INT", id3p1: "INT", id3p2: "INT" }); UniqueIndexTest.index('id1'); UniqueIndexTest.index('id2',{unique:true}); UniqueIndexTest.index(['id3p1','id3p2'],{unique:true}); Task.hasMany('tags', Tag, 'tasks'); Tag.hasMany('tasks', Task, 'tags'); Task.index('dateAdded'); Project.hasMany('tasks', Task, 'project'); window.Project = Project; window.Task = Task window.Project = Project; window.UniqueIndexTest = UniqueIndexTest; module("Setup"); asyncTest("setting up database", 1, function() { persistence.schemaSync(function(tx){ ok(true, 'schemaSync called callback function'); start(); }); }); module("Entity manipulation", { setup: function() { stop(); persistence.reset(function() { persistence.schemaSync(start); }); } }); test("Property default values", 6, function() { var t1 = new Task(); QUnit.strictEqual(t1.name, "", "TEXT properties default to ''"); QUnit.strictEqual(t1.done, false, "BOOL properties default to false"); QUnit.strictEqual(t1.counter, 0, "INT properties default to 0"); QUnit.strictEqual(t1.dateAdded, null, "DATE properties default to null"); QUnit.strictEqual(t1.dateAddedInMillis, 0, "BIGINT properties default to 0"); QUnit.strictEqual(t1.metaData, null, "JSON properties default to null"); }); test("Property value assignment", 6, function() { var t1 = new Task(); var now = new Date(); var meta = {starRating: 5}; t1.name = "Task 1"; t1.done = false; t1.counter = 7; t1.dateAdded = now; t1.dateAddedInMillis = now.getTime(); t1.metaData = meta; QUnit.strictEqual(t1.name, 'Task 1', "Assignment for TEXT properties"); QUnit.strictEqual(t1.done, false, "Assignment for BOOL properties"); QUnit.strictEqual(t1.counter, 7, "Assignment for INT properties"); QUnit.strictEqual(t1.dateAdded, now, "Assignment for DATE properties"); QUnit.strictEqual(t1.dateAddedInMillis, now.getTime(), "Assignment for BIGINT properties"); QUnit.strictEqual(t1.metaData, meta, "Assignment for JSON properties"); }); test("Property constructor property value assignment", 6, function() { var now = new Date(); var meta = {starRating: 5}; var t1 = new Task({ name: "Task 1", done: false, counter: 7, dateAdded: now, dateAddedInMillis: now.getTime(), metaData: meta }); QUnit.strictEqual(t1.name, 'Task 1', "Assignment for TEXT properties"); QUnit.strictEqual(t1.done, false, "Assignment for BOOL properties"); QUnit.strictEqual(t1.counter, 7, "Assignment for INT properties"); QUnit.strictEqual(t1.dateAdded, now, "Assignment for DATE properties"); QUnit.strictEqual(t1.dateAddedInMillis, now.getTime(), "Assignment for BIGINT properties"); QUnit.strictEqual(t1.metaData, meta, "Assignment for JSON properties"); }); asyncTest("Empty object persistence", function() { var t1 = new Task(); persistence.add(t1); persistence.flush(function() { //persistence.clean(); Task.all().one(function(t1db) { equals(t1db.id, t1.id, "TEXT properties default to ''"); equals(t1db.name, "", "TEXT properties default to ''"); equals(t1db.done, false, "BOOL properties default to false"); equals(t1db.counter, 0, "INT properties default to 0"); equals(t1db.dateAdded, null, "DATE properties default to null"); equals(t1db.dateAddedInMillis, 0, "BIGINT properties default to 0"); equals(t1db.metaData, null, "JSON properties default to null"); start(); }); }); }); asyncTest("Object persistence", function() { var now = new Date(); var meta = {starRating: 5}; var t1 = new Task({ name: "Task 1", done: false, counter: 7, dateAdded: now, dateAddedInMillis: 1296802544867, metaData: meta }); persistence.add(t1); persistence.flush(function() { persistence.clean(); Task.all().one(function(t1db) { equals(t1db.name, 'Task 1', "Persistence of TEXT properties"); equals(t1db.done, false, "Persistence of BOOL properties"); equals(t1db.counter, 7, "Persistence of INT properties"); equals(Math.round(t1db.dateAdded.getTime()/1000)*1000, Math.round(now.getTime()/1000)*1000, "Persistence of DATE properties"); equals(t1db.dateAddedInMillis, 1296802544867, "Persistence of BIGINT properties"); same(t1db.metaData, meta, "Persistence of JSON properties"); start(); }); }); }); asyncTest("Multiple objects", function() { var objs = []; var counter = 0; for(var i = 0; i < 25; i++) { var t = new Task({name: "Task " + i}); t.counter = counter; objs.push(t); persistence.add(t); counter++; } persistence.flush(function() { Task.all().order('counter', true).list(function(results) { for(var i = 0; i < 25; i++) { ok(results[i] === objs[i], 'Cache works OK'); } //persistence.clean(); // Clean out local cache Task.all().order('counter', true).list(function(results) { for(var i = 0; i < 25; i++) { ok(results[i].id === objs[i].id, 'Retrieving from DB ok'); } start(); }); }); }); }); asyncTest("Removing objects", function() { var originalTasks = []; var counter = 0; for(var i = 0; i < 25; i++) { var t = new Task({name: "Task " + i}); t.counter = counter; originalTasks.push(t); persistence.add(t); counter++; } for(var i = 0; i < 5; i++) { persistence.remove(originalTasks[i]); } Task.all().order('counter', true).list(function(tasks) { equals(tasks.length, 20, 'Correct count after removing local objects'); for(var i = 5; i < 25; i++) { equals(tasks[i - 5].id, originalTasks[i].id, 'Correct objects after removing local objects'); } for(var i = 5; i < 10; i++) { persistence.remove(originalTasks[i]); } Task.all().order('counter', true).list(function(tasks) { equals(tasks.length, 15, 'Correct count after removing persisted objects'); for(var i = 10; i < 25; i++) { equals(tasks[i - 10].id, originalTasks[i].id, 'Correct objects after removing persisted objects'); } start(); }); }); }); asyncTest("One-to-many", function() { var p = new Project({name: "Some project"}); persistence.add(p); p.tasks.list(function(tasks) { equals(tasks.length, 0, "Initially, no tasks"); var task1 = new Task({name: "Do dishes"}); var task2 = new Task({name: "Laundry"}); // Adding in two ways p.tasks.add(task1); task2.project = p; p.tasks.order('name', true).list(function(tasks) { equals(tasks.length, 2, "Now two tasks"); equals(tasks[0].id, task1.id, "Right tasks"); equals(tasks[1].id, task2.id, "Right tasks"); start(); }); }); }); asyncTest("Many-to-many", function() { var t = new Task({name: "Some task"}); persistence.add(t); t.tags.list(function(tags) { equals(tags.length, 0, "Initially, no tags"); var tag1 = new Tag({name: "important"}); var tag2 = new Tag({name: "today"}); t.tags.add(tag1); t.tags.add(tag2); t.tags.list(function(tags) { equals(tags.length, 2, "2 tags added"); var oneTag = tags[0]; oneTag.tasks.list(function(tagTasks) { equals(tagTasks.length, 1, "Tag has one task"); equals(tagTasks[0].id, t.id, "Correct task"); oneTag.tasks.remove(tagTasks[0]); t.tags.count(function(cnt) { equals(cnt, 1, "Tag removed task, task has only one tag left"); start(); }); }); }); }); }); asyncTest("Many-to-many with local changes", function() { var t = new Task({name: "Some task"}); persistence.add(t); t.tags.list(function(tags) { equals(tags.length, 0, "Initially, no tags"); var tag1 = new Tag({name: "important"}); var tag2 = new Tag({name: "today"}); t.tags.add(tag1); t.tags.add(tag2); t.tags.remove(tag1); t.tags.list(function(tags) { equals(tags.length, 1, "2 tags added, 1 removed"); equals(tags[0].id, tag2.id, "Correct tag left"); start(); }); }); }); module("Query collections", { setup: function() { stop(); persistence.reset(function() { persistence.schemaSync(start); }); } }); function intFilterTests(coll, callback) { for(var i = 0; i < 25; i++) { var t = new Task({name: "Task " + i, done: false}); t.counter = i; coll.add(t); } coll.list(function(results) { equals(results.length, 25, "Count items in collection"); coll.filter("counter", ">", 10).list(function(results) { equals(results.length, 14, "> filter test"); coll.filter("counter", "in", [0, 1, 2]).list(function(results) { equals(results.length, 3, "'in' filter test"); coll.filter("counter", "not in", [0, 1]).list(function(results) { equals(results.length, 23, "'not in' filter test"); coll.filter("counter", "!=", 0).list(function(results) { equals(results.length, 24, "'!=' filter test"); callback(); }); }); }); }); }); } function textFilterTests(coll, callback) { var alphabet = 'abcdefghijklmnopqrstufwxyz'; for(var i = 0; i <= 25; i++) { var t = new Task({name: alphabet[i]}); coll.add(t); } coll.list(function(results) { equals(results.length, 26, "Count items in collection"); coll.filter("name", "=", 'a').list(function(results) { equals(results.length, 1, "= filter test"); coll.filter("name", "!=", 'a').list(function(results) { equals(results.length, 25, "!= filter test"); coll.filter("name", ">", 'm').list(function(results) { equals(results.length, 12, "> filter test"); coll.filter("name", "in", ["a", "b"]).list(function(results) { equals(results.length, 2, "'in' filter test"); coll.filter("name", "not in", ["q", "x"]).list(function(results) { equals(results.length, 24, "'not in' filter test"); callback(); }); }); }); }); }); }); } function boolFilterTests(coll, callback) { for(var i = 0; i < 24; i++) { var t = new Task({name: "Task " + i, done: i % 2 === 0}); coll.add(t); } coll.list(function(results) { equals(results.length, 24, "Count items in collection"); coll.filter("done", "=", true).list(function(results) { equals(results.length, 12, "= filter test"); coll.filter("done", "=", false).list(function(results) { equals(results.length, 12, "= filter test"); coll.filter("done", "!=", true).list(function(results) { equals(results.length, 12, "'!=' filter test"); coll.filter("done", "!=", false).list(function(results) { equals(results.length, 12, "'!=' filter test"); callback(); }); }); }); }); }); } function dateFilterTests(coll, callback) { var now = new Date(); function dateInDays(n) { var newDate = new Date(now.getTime()); newDate.setDate(newDate.getDate()+n); return newDate; } for(var i = 0; i < 24; i++) { var t = new Task({name: "Task " + i, dateAdded: dateInDays(i)}); coll.add(t); } coll.list(function(results) { equals(results.length, 24, "Count items in collection"); coll.filter("dateAdded", "=", dateInDays(1)).list(function(results) { equals(results.length, 1, "= filter test"); coll.filter("dateAdded", "!=", dateInDays(1)).list(function(results) { equals(results.length, 23, "!= filter test"); coll.filter("dateAdded", ">", dateInDays(12)).list(function(results) { equals(results.length, 11, "> filter test"); start(); }); }) }); }); } asyncTest("Database INT filters", function() { for(var i = 0; i < 25; i++) { var t = new Task({name: "Root task " + i, done: false}); t.counter = i; persistence.add(t); } var p = new Project({name: "My project"}); persistence.add(p); intFilterTests(p.tasks, start); }); asyncTest("Local INT filters", function() { var coll = new persistence.LocalQueryCollection(); intFilterTests(coll, start); }); asyncTest("Database TEXT filters", function() { var alphabet = 'abcdefghijklmnopqrstufwxyz'; for(var i = 0; i <= 25; i++) { var t = new Task({name: alphabet[i]}); persistence.add(t); } var p = new Project({name: "My project"}); persistence.add(p); textFilterTests(p.tasks, start); }); asyncTest("Local TEXT filters", function() { var coll = new persistence.LocalQueryCollection(); textFilterTests(coll, start); }); asyncTest("Database BOOL filters", function() { for(var i = 0; i < 25; i++) { var t = new Task({name: "Root task " + i, done: false}); t.counter = i; persistence.add(t); } var p = new Project({name: "My project"}); persistence.add(p); boolFilterTests(p.tasks, start); }); asyncTest("Local BOOL filters", function() { var coll = new persistence.LocalQueryCollection(); boolFilterTests(coll, start); }); asyncTest("Database DATE filters", function() { var p = new Project({name: "My project"}); persistence.add(p); dateFilterTests(p.tasks, start); }); asyncTest("Local DATE filters", function() { var coll = new persistence.LocalQueryCollection(); dateFilterTests(coll, start); }); function intOrderTests(coll, callback) { var tasks = []; for(var i = 0; i < 24; i++) { var t = new Task({name: "Task " + i, counter: i}); tasks.push(t); coll.add(t); } coll.order('counter', true).list(function(results) { for(var i = 0; i < 24; i++) { equals(results[i].id, tasks[i].id, "order check, ascending"); } tasks.reverse(); coll.order('counter', false).list(function(results) { for(var i = 0; i < 24; i++) { equals(results[i].id, tasks[i].id, "order check, descending"); } callback(); }); }); } function textOrderTests(coll, callback) { var taskNames = ['Task A', 'task b', 'Task C', 'task d']; var tasks = []; for(var i = 0; i < taskNames.length; i++) { var t = new Task({name: taskNames[i]}); tasks.push(t); coll.add(t); } coll.order('name', true, true).list(function(results) { var expectedIndices = [0, 2, 1, 3]; for(var i = 0; i < tasks.length; i++) { equals(results[i].id, tasks[expectedIndices[i]].id, "order check, ascending, case sensitive"); } coll.order('name', true, false).list(function(results) { for(var i = 0; i < tasks.length; i++) { equals(results[i].id, tasks[i].id, "order check, ascending, case insensitive"); } callback(); }); }); } function dateOrderTests(coll, callback) { var now = new Date(); function dateInDays(n) { var newDate = new Date(now.getTime()); newDate.setDate(newDate.getDate()+n); return newDate; } var tasks = []; for(var i = 0; i < 24; i++) { var t = new Task({name: "Task " + i, dateAdded: dateInDays(i)}); tasks.push(t); coll.add(t); } coll.order('dateAdded', true).list(function(results) { for(var i = 0; i < 24; i++) { equals(results[i].id, tasks[i].id, "order check, ascending"); } tasks.reverse(); coll.order('dateAdded', false).list(function(results) { for(var i = 0; i < 24; i++) { equals(results[i].id, tasks[i].id, "order check, descending"); } callback(); }); }); } asyncTest("Database INT order", function() { var p = new Project({name: "My project"}); persistence.add(p); intOrderTests(p.tasks, start); }); asyncTest("Local INT order", function() { var coll = new persistence.LocalQueryCollection(); intOrderTests(coll, start); }); asyncTest("Database TEXT order", function() { var p = new Project({name: "My project"}); persistence.add(p); textOrderTests(p.tasks, start); }); asyncTest("Local TEXT order", function() { var coll = new persistence.LocalQueryCollection(); textOrderTests(coll, start); }); asyncTest("Database DATE order", function() { var p = new Project({name: "My project"}); persistence.add(p); dateOrderTests(p.tasks, start); }); asyncTest("Local DATE order", function() { var coll = new persistence.LocalQueryCollection(); dateOrderTests(coll, start); }); function collectionLimitTests(coll, callback) { var tasks = []; for(var i = 0; i < 24; i++) { var t = new Task({name: "Task " + i, counter: i}); tasks.push(t); coll.add(t); } coll.order("counter", true).limit(5).list(function(results) { equals(results.length, 5, "Result length check"); for(var i = 0; i < 5; i++) { equals(results[i].id, tasks[i].id, "limit check"); } start(); }); } function collectionSkipTests(coll, callback) { var tasks = []; for(var i = 0; i < 24; i++) { var t = new Task({name: "Task " + i, counter: i}); tasks.push(t); coll.add(t); } coll.order("counter", true).skip(5).limit(5).list(function(results) { equals(results.length, 5, "Result length check"); for(var i = 5; i < 10; i++) { equals(results[i-5].id, tasks[i].id, "skip check"); } start(); }); } asyncTest("Database limit", function() { collectionLimitTests(Task.all(), start); }); asyncTest("Local limit", function() { var coll = new persistence.LocalQueryCollection(); collectionLimitTests(coll, start); }); asyncTest("Database skip", function() { collectionSkipTests(Task.all(), start); }); asyncTest("Local skip", function() { var coll = new persistence.LocalQueryCollection(); collectionSkipTests(coll, start); }); module("Dumping/restoring"); asyncTest("Full dump/restore", function() { persistence.reset(function() { persistence.schemaSync(function() { for(var i = 0; i < 10; i++) { var t = new Task({name: "Task " + i, dateAdded: new Date()}); t.tags.add(new Tag({name: "Some tag: " + i})); t.tags.add(new Tag({name: "Another tag: " + i})); persistence.add(t); } persistence.flush(function() { persistence.dumpToJson(function(dumps) { persistence.reset(function() { persistence.schemaSync(function() { persistence.loadFromJson(dumps, function() { Task.all().list(function(tasks) { equals(tasks.length, 10, "tasks restored successfully"); tasks[0].tags.list(function(tags) { equals(tags.length, 2, "tags restored successfully"); start(); }); }); }); }); }); }); }); }); }); }); asyncTest("Select dump/restore", function() { persistence.reset(function() { persistence.schemaSync(function() { var project = new Project({name: "My project"}); persistence.add(project); var tags = []; for(var i = 0; i < 5; i++) { var tag = new Tag({name: "Tag " + i}); persistence.add(tag); tags.push(tag); } for(var i = 0; i < 10; i++) { var task = new Task({name: "Task " + i}); task.done = true; task.tags = new persistence.LocalQueryCollection(tags); project.tasks.add(task); } Project.all().selectJSON(['id', 'name', 'tasks.[id,name]', 'tasks.tags.[id, name]'], function(result) { persistence.reset(function() { persistence.schemaSync(function() { Project.fromSelectJSON(result[0], function(obj) { persistence.add(obj); Task.all().list(function(tasks) { equals(tasks.length, 10, "number of restored tasks ok"); tasks.forEach(function(task) { equals(task.done, false, "done still default value"); }); start(); console.log(new Date().getTime() - startTime); }); }); }); }); }); }); }); }); asyncTest("AND and OR filters", function() { persistence.reset(function() { persistence.schemaSync(function() { for(var i = 0; i < 10; i++) { var task = new Task({name: "Task " + i}); task.done = i % 2 === 0; persistence.add(task); } Task.all().filter("done", "=", true).or(new persistence.PropertyFilter("done", "=", false)).list(function(results) { equals(results.length, 10, "right number of results"); Task.all().filter("done", "=", true).and(new persistence.PropertyFilter("done", "=", false)).list(function(results) { equals(results.length, 0, "right number of results"); start(); }); }); }); }); }); module("Events"); asyncTest("all collection", function() { persistence.reset(function() { persistence.schemaSync(function() { var allTasks = Task.all(); var changesDetected = 0; allTasks.addEventListener('change', function() { changesDetected++; }); for(var i = 0; i < 10; i++) { var task = new Task({name: "Task " + i}); task.done = i % 2 === 0; Task.all().add(task); } equals(10, changesDetected, "detected all changes"); start(); }); }); }); asyncTest("filter collection", function() { persistence.reset(function() { persistence.schemaSync(function() { var allTasks = Task.all().filter("done", "=", true); var changesDetected = 0; allTasks.addEventListener('change', function() { changesDetected++; }); for(var i = 0; i < 10; i++) { var task = new Task({name: "Task " + i}); task.done = i % 2 === 0; Task.all().add(task); } equals(5, changesDetected, "detected all changes"); changesDetected = 0; Task.all().filter("done", "=", true).list(function(results) { results.forEach(function(r) { r.done = false; }); equals(5, changesDetected, "detected filter changes"); start(); }); }); }); }); module("Indexes"); asyncTest("unique indexes", function() { persistence.reset(function() { persistence.schemaSync(function() { var o1 = new UniqueIndexTest({"id1":101,"id2":102,"id3p1":103,"id3p2":104}); // id1 is not unique var o2 = new UniqueIndexTest({"id1":101,"id2":202,"id3p1":203,"id3p2":204}); //shouldn't work, id2 is unique var o3 = new UniqueIndexTest({"id1":301,"id2":102,"id3p1":303,"id3p2":304}); // id3p1 itself is not unique var o4 = new UniqueIndexTest({"id1":401,"id2":402,"id3p1":103,"id3p2":404}); //shouldn't work, id3p1+id3p2 are unique var o5 = new UniqueIndexTest({"id1":501,"id2":502,"id3p1":103,"id3p2":104}); persistence.add(o1); persistence.add(o2); try { //persistence.add(o3); } catch (e) { console.log("err",e); } persistence.add(o4); try { //persistence.add(o5); } catch (e) { console.log("err",e); } UniqueIndexTest.all().order("id2",true).list(function(results) { equals(3,results.length,"skipped 2 duplicate rows"); if (results.length==3) { equals(102,results[0].id2); equals(202,results[1].id2); equals(402,results[2].id2); } start(); }); }); }); }); }); ================================================ FILE: test/browser/test.search.html ================================================

          persistence.js search tests

            ================================================ FILE: test/browser/test.search.js ================================================ $(document).ready(function(){ persistence.store.websql.config(persistence, 'searchtest', 'My db', 5 * 1024 * 1024); persistence.search.config(persistence, persistence.store.websql.sqliteDialect); persistence.debug = true; var Note = persistence.define('Note', { title: "TEXT", text: "TEXT" }); Note.textIndex('title'); Note.textIndex('text'); module("Setup"); asyncTest("setting up database", 1, function() { persistence.reset(function() { persistence.schemaSync(function(tx){ ok(tx.executeSql, 'schemaSync passed transaction as argument to callback'); start(); }); }); }); module("Search test"); asyncTest("Create some sample data", function() { persistence.add(new Note({title: "My first note", text: "This is my first note. It has a rather high duplication quotient, or whatever."})); persistence.add(new Note({title: "My second note", text: "This is my second note. Isn't it a cool note? Third, fourth."})); persistence.add(new Note({title: "My third note", text: "Nothing here yet"})); persistence.add(new Note({title: "Unrelated", text: "Under contruction."})); persistence.flush(function() { start(); }); }); asyncTest("Searching", function() { Note.search("note").list(function(results) { equals(results.length, 3, "returned correct number of results"); equals(results[0].title, "My second note", "Found most relevant result"); Note.search("title: third").list(function(results) { equals(results.length, 1, "returned correct number of results"); equals(results[0].title, "My third note", "Searched in only title"); Note.search("thi*").list(function(results) { equals(results.length, 3, "wildcard search"); Note.search("thi*").limit(1).list(function(results) { equals(results.length, 1, "limit number of search results"); start(); }); }); }); }); }); }); ================================================ FILE: test/browser/test.sync.html ================================================

            persistence.js sync tests

              ================================================ FILE: test/browser/test.sync.js ================================================ $(document).ready(function(){ persistence.store.websql.config(persistence, 'persistencetest', 'My db', 5 * 1024 * 1024); //persistence.store.memory.config(persistence); persistence.debug = true; var Project = persistence.define('Project', { name: "TEXT" }); var Task = persistence.define('Task', { name: "TEXT", done: "BOOL" }); var Tag = persistence.define('Tag', { name: "TEXT" }); Task.hasMany('tags', Tag, 'tasks'); Tag.hasMany('tasks', Task, 'tags'); Project.hasMany('tasks', Task, 'project'); Task.enableSync('/taskUpdates'); Project.enableSync('/projectUpdates'); Tag.enableSync('/tagUpdates'); module("Setup"); asyncTest("setting up local database", function() { persistence.reset(function() { persistence.schemaSync(function(){ ok(true, 'came back from schemaSync'); start(); }); }); }); asyncTest("setting up remote database", 1, function() { persistence.sync.getJSON('/reset', function(data) { same(data, {status: 'ok'}, "Remote reset"); start(); }); }); module("Sync"); function noConflictsHandler(conflicts, updatesToPush, callback) { ok(false, "Should not go to conflict resolving"); console.log("Conflicts: ", conflicts); callback(); } asyncTest("initial sync of project", function() { Project.syncAll(noConflictsHandler, function() { ok(true, "Came back from sync"); Project.syncAll(noConflictsHandler, function() { ok(true, "Came back from second sync"); Project.all().list(function(projects) { equals(projects.length, 1, "1 project synced"); var p = projects[0]; equals(p.name, "Main project", "project name"); start(); }); }); }); }); asyncTest("initial sync of tasks", function() { Task.syncAll(noConflictsHandler, function() { ok(true, "Came back from sync"); Task.syncAll(noConflictsHandler, function() { ok(true, "Came back from second sync"); Task.all().list(function(tasks) { equals(tasks.length, 25, "25 tasks synced"); tasks.forEach(function(task) { equals(false, task.done, "task not done"); }); start(); }); }); }); }); asyncTest("setting some tasks to done and syncing again", function() { Task.all().list(function(tasks) { for(var i = 0; i < tasks.length; i++) { if(i % 2 === 0) { tasks[i].done = true; } } Task.syncAll(noConflictsHandler, function() { ok(true, "Came back from sync"); start(); }); }); }); function resetResync(callback) { persistence.reset(function() { persistence.schemaSync(function() { ok(true, "Database reset"); Project.syncAll(noConflictsHandler, function() { ok(true, "Came back from project sync"); Task.syncAll(noConflictsHandler, function() { ok(true, "Came back from task sync"); callback(); }); }); }); }); } asyncTest("resetting local db and resyncing", function() { resetResync(function() { Task.all().filter("done", "=", true).count(function(n) { equals(n, 13, "right number of tasks done"); start(); }); }); }); asyncTest("creating some new objects", function() { var p = new Project({name: "Locally created project"}); persistence.add(p); for(var i = 0; i < 10; i++) { var t = new Task({name: "Local task " + i}); p.tasks.add(t); } persistence.flush(function() { ok(true, "project and tasks added locally"); Project.syncAll(noConflictsHandler, function() { ok(true, "returned from project sync"); Task.syncAll(noConflictsHandler, function() { ok(true, "returned from task sync"); p.tasks.list(function(tasks) { equals(tasks.length, 10, 'check collection size'); tasks.forEach(function(task) { task.done = true; }); Task.syncAll(noConflictsHandler, function() { start(); }); }); }); }); }); }); asyncTest("resetting local db and resyncing", function() { resetResync(function() { Task.all().filter("done", "=", true).count(function(n) { equals(n, 23, "right number of tasks done."); start(); }); }); }); asyncTest("marking all tasks done remotely", function() { persistence.sync.getJSON('/markAllDone', function(data) { same(data, {status: 'ok'}, "Remote reset"); Task.syncAll(noConflictsHandler, function() { ok(true, "Came back from sync"); Task.all().filter("done", "=", true).count(function(n) { equals(35, n, "all tasks were marked done and synced correctly"); start(); }); }); }); }); module("Conflicts"); asyncTest("prefer local conflict handler", 8, function() { persistence.sync.getJSON('/markAllUndone', function(data) { same(data, {status: 'ok'}, "Remote marking undone"); Task.all().list(function(tasks) { for(var i = 0; i < tasks.length; i++) { if(i % 2 === 0) { // Force a dirty flag tasks[i].done = true; tasks[i].done = false; tasks[i].done = true; } } persistence.flush(function() { Task.syncAll(function(conflicts, updatesToPush, callback) { ok(true, "Conflict resolver called"); equals(conflicts.length, 18, "Number of conflicts"); console.log("Conflicts: ", conflicts); persistence.sync.preferLocalConflictHandler(conflicts, updatesToPush, callback); }, function() { ok(true, "Came back from sync"); resetResync(function() { Task.all().filter("done", "=", true).list(function(tasks) { equals(tasks.length, 18, "Conflicts were properly resolved towards the server"); start(); }); }); }); }); }); }); }); asyncTest("prefer remote conflict handler", 5, function() { persistence.sync.getJSON('/markAllUndone', function(data) { same(data, {status: 'ok'}, "Remote marking undone"); Task.all().list(function(tasks) { for(var i = 0; i < tasks.length; i++) { if(i % 2 === 0) { // Force a dirty flag tasks[i].done = true; tasks[i].done = false; tasks[i].done = true; } } persistence.flush(function() { Task.syncAll(function(conflicts, updatesToPush, callback) { ok(true, "Conflict resolver called"); equals(conflicts.length, 18, "Number of conflicts"); console.log("Conflicts: ", conflicts); persistence.sync.preferRemoteConflictHandler(conflicts, updatesToPush, callback); }, function() { ok(true, "Came back from sync"); Task.all().filter("done", "=", true).list(function(tasks) { equals(tasks.length, 0, "Conflicts were properly resolved"); start(); }); }); }); }); }); }); asyncTest("Object removal", function() { Task.all().list(function(tasks) { for(var i = 0; i < tasks.length; i++) { if(i % 2 === 0) { persistence.remove(tasks[i]); } } persistence.flush(function() { console.log("Now going to sync"); Task.syncAll(noConflictsHandler, function() { //Task.syncAll(noConflictsHandler, function() { start(); //}); }); }); }); }); }); ================================================ FILE: test/browser/test.uki-persistence.html ================================================

              persistence.js core tests with uki MVC framework

                ================================================ FILE: test/browser/test.uki-persistence.js ================================================ $(document).ready(function(){ persistence.store.websql.config(persistence, 'persistencetest', 'My db', 5 * 1024 * 1024); //persistence.store.memory.config(persistence); persistence.debug = true; var Project = persistence.define('Project', { name: "TEXT" }); var Task = persistence.define('Task', { name: "TEXT", done: "BOOL", counter: "INT", dateAdded: "DATE", metaData: "JSON" }); var Tag = persistence.define('Tag', { name: "TEXT" }); Task.hasMany('tags', Tag, 'tasks'); Tag.hasMany('tasks', Task, 'tags'); Project.hasMany('tasks', Task, 'project'); window.Project = Project; window.Task = Task window.Project = Project; module("Setup"); asyncTest("setting up database", 1, function() { persistence.schemaSync(function(tx){ ok(true, 'schemaSync called callback function'); start(); }); }); module("Entity manipulation", { setup: function() { stop(); persistence.reset(function() { persistence.schemaSync(start); }); } }); test("Property default values", 5, function() { var t1 = new Task(); QUnit.strictEqual(t1.name(), "", "TEXT properties default to ''"); QUnit.strictEqual(t1.done(), false, "BOOL properties default to false"); QUnit.strictEqual(t1.counter(), 0, "INT properties default to 0"); QUnit.strictEqual(t1.dateAdded(), null, "DATE properties default to null"); QUnit.strictEqual(t1.metaData(), null, "JSON properties default to null"); }); test("Property value assignment", 5, function() { var t1 = new Task(); var now = new Date(); var meta = {starRating: 5}; t1.name("Task 1"); t1.done(false); t1.counter(7); t1.dateAdded(now); t1.metaData(meta); QUnit.strictEqual(t1.name(), 'Task 1', "Assignment for TEXT properties"); QUnit.strictEqual(t1.done(), false, "Assignment for BOOL properties"); QUnit.strictEqual(t1.counter(), 7, "Assignment for INT properties"); QUnit.strictEqual(t1.dateAdded(), now, "Assignment for DATE properties"); QUnit.strictEqual(t1.metaData(), meta, "Assignment for JSON properties"); }); test("Property contructor property value assignment", 5, function() { var now = new Date(); var meta = {starRating: 5}; var t1 = new Task({ name: "Task 1", done: false, counter: 7, dateAdded: now, metaData: meta }); QUnit.strictEqual(t1.name(), 'Task 1', "Assignment for TEXT properties"); QUnit.strictEqual(t1.done(), false, "Assignment for BOOL properties"); QUnit.strictEqual(t1.counter(), 7, "Assignment for INT properties"); QUnit.strictEqual(t1.dateAdded(), now, "Assignment for DATE properties"); QUnit.strictEqual(t1.metaData(), meta, "Assignment for JSON properties"); }); asyncTest("Empty object persistence", function() { var t1 = new Task(); persistence.add(t1); persistence.flush(function() { Task.all().one(function(t1db) { equals(t1db.id, t1.id, "TEXT properties default to ''"); equals(t1db.name(), "", "TEXT properties default to ''"); equals(t1db.done(), false, "BOOL properties default to false"); equals(t1db.counter(), 0, "INT properties default to 0"); equals(t1db.dateAdded(), null, "DATE properties default to null"); equals(t1db.metaData(), null, "JSON properties default to null"); start(); }); }); }); asyncTest("Object persistence", function() { var now = new Date(); var meta = {starRating: 5}; var t1 = new Task({ name: "Task 1", done: false, counter: 7, dateAdded: now, metaData: meta }); persistence.add(t1); persistence.flush(function() { //persistence.clean(); Task.all().one(function(t1db) { equals(t1db.name(), 'Task 1', "Persistence of TEXT properties"); equals(t1db.done(), false, "Persistence of BOOL properties"); equals(t1db.counter(), 7, "Persistence of INT properties"); equals(Math.round(t1db.dateAdded().getTime()/1000)*1000, Math.round(now.getTime()/1000)*1000, "Persistence of DATE properties"); same(t1db.metaData(), meta, "Persistence of JSON properties"); start(); }); }); }); asyncTest("Multiple objects", function() { var objs = []; var counter = 0; for(var i = 0; i < 25; i++) { var t = new Task({name: "Task " + i}); t.counter(counter); objs.push(t); persistence.add(t); counter++; } persistence.flush(function() { Task.all().order('counter', true).list(function(results) { for(var i = 0; i < 25; i++) { ok(results[i] === objs[i], 'Cache works OK'); } //persistence.clean(); // Clean out local cache Task.all().order('counter', true).list(function(results) { for(var i = 0; i < 25; i++) { ok(results[i].id === objs[i].id, 'Retrieving from DB ok'); } start(); }); }); }); }); asyncTest("One-to-many", function() { var p = new Project({name: "Some project"}); persistence.add(p); p.tasks().list(function(tasks) { equals(tasks.length, 0, "Initially, no tasks"); var task1 = new Task({name: "Do dishes"}); var task2 = new Task({name: "Laundry"}); // Adding in two ways p.tasks().add(task1); task2.project(p); p.tasks().order('name', true).list(function(tasks) { equals(tasks.length, 2, "Now two tasks"); equals(tasks[0].id, task1.id, "Right tasks"); equals(tasks[1].id, task2.id, "Right tasks"); start(); }); }); }); asyncTest("Many-to-many", function() { var t = new Task({name: "Some task"}); persistence.add(t); t.tags().list(function(tags) { equals(tags.length, 0, "Initially, no tags"); var tag1 = new Tag({name: "important"}); var tag2 = new Tag({name: "today"}); t.tags().add(tag1); t.tags().add(tag2); t.tags().list(function(tags) { equals(tags.length, 2, "2 tags added"); var oneTag = tags[0]; oneTag.tasks().list(function(tagTasks) { equals(tagTasks.length, 1, "Tag has one task"); equals(tagTasks[0].id, t.id, "Correct task"); oneTag.tasks().remove(tagTasks[0]); t.tags().list(function(newTags) { equals(newTags.length, 1, "Tag removed task, task has only one tag left"); start(); }); }); }); }); }); module("Query collections", { setup: function() { stop(); persistence.reset(function() { persistence.schemaSync(start); }); } }); function intFilterTests(coll, callback) { for(var i = 0; i < 25; i++) { var t = new Task({name: "Task " + i, done: false}); t.counter(i); coll.add(t); } coll.list(function(results) { equals(results.length, 25, "Count items in collection"); coll.filter("counter", ">", 10).list(function(results) { equals(results.length, 14, "> filter test"); coll.filter("counter", "in", [0, 1, 2]).list(function(results) { equals(results.length, 3, "'in' filter test"); coll.filter("counter", "not in", [0, 1]).list(function(results) { equals(results.length, 23, "'not in' filter test"); coll.filter("counter", "!=", 0).list(function(results) { equals(results.length, 24, "'!=' filter test"); callback(); }); }); }); }); }); } function textFilterTests(coll, callback) { var alphabet = 'abcdefghijklmnopqrstufwxyz'; for(var i = 0; i <= 25; i++) { var t = new Task({name: alphabet[i]}); coll.add(t); } coll.list(function(results) { equals(results.length, 26, "Count items in collection"); coll.filter("name", "=", 'a').list(function(results) { equals(results.length, 1, "= filter test"); coll.filter("name", "!=", 'a').list(function(results) { equals(results.length, 25, "!= filter test"); coll.filter("name", ">", 'm').list(function(results) { equals(results.length, 12, "> filter test"); coll.filter("name", "in", ["a", "b"]).list(function(results) { equals(results.length, 2, "'in' filter test"); coll.filter("name", "not in", ["q", "x"]).list(function(results) { equals(results.length, 24, "'not in' filter test"); callback(); }); }); }); }); }); }); } function boolFilterTests(coll, callback) { for(var i = 0; i < 24; i++) { var t = new Task({name: "Task " + i, done: i % 2 === 0}); coll.add(t); } coll.list(function(results) { equals(results.length, 24, "Count items in collection"); coll.filter("done", "=", true).list(function(results) { equals(results.length, 12, "= filter test"); coll.filter("done", "=", false).list(function(results) { equals(results.length, 12, "= filter test"); coll.filter("done", "!=", true).list(function(results) { equals(results.length, 12, "'!=' filter test"); coll.filter("done", "!=", false).list(function(results) { equals(results.length, 12, "'!=' filter test"); callback(); }); }); }); }); }); } function dateFilterTests(coll, callback) { var now = new Date(); function dateInDays(n) { var newDate = new Date(now.getTime()); newDate.setDate(newDate.getDate()+n); return newDate; } for(var i = 0; i < 24; i++) { var t = new Task({name: "Task " + i, dateAdded: dateInDays(i)}); coll.add(t); } coll.list(function(results) { equals(results.length, 24, "Count items in collection"); coll.filter("dateAdded", "=", dateInDays(1)).list(function(results) { equals(results.length, 1, "= filter test"); coll.filter("dateAdded", "!=", dateInDays(1)).list(function(results) { equals(results.length, 23, "= filter test"); coll.filter("dateAdded", ">", dateInDays(12)).list(function(results) { equals(results.length, 11, "> filter test"); start(); }); }) }); }); } asyncTest("Database INT filters", function() { for(var i = 0; i < 25; i++) { var t = new Task({name: "Root task " + i, done: false}); t.counter(i); persistence.add(t); } var p = new Project({name: "My project"}); persistence.add(p); intFilterTests(p.tasks(), start); }); asyncTest("Local INT filters", function() { var coll = new persistence.LocalQueryCollection(); intFilterTests(coll, start); }); asyncTest("Database TEXT filters", function() { var alphabet = 'abcdefghijklmnopqrstufwxyz'; for(var i = 0; i <= 25; i++) { var t = new Task({name: alphabet[i]}); persistence.add(t); } var p = new Project({name: "My project"}); persistence.add(p); textFilterTests(p.tasks(), start); }); asyncTest("Local TEXT filters", function() { var coll = new persistence.LocalQueryCollection(); textFilterTests(coll, start); }); asyncTest("Database BOOL filters", function() { for(var i = 0; i < 25; i++) { var t = new Task({name: "Root task " + i, done: false}); t.counter(i); persistence.add(t); } var p = new Project({name: "My project"}); persistence.add(p); boolFilterTests(p.tasks(), start); }); asyncTest("Local BOOL filters", function() { var coll = new persistence.LocalQueryCollection(); boolFilterTests(coll, start); }); asyncTest("Database DATE filters", function() { var p = new Project({name: "My project"}); persistence.add(p); dateFilterTests(p.tasks(), start); }); asyncTest("Local DATE filters", function() { var coll = new persistence.LocalQueryCollection(); dateFilterTests(coll, start); }); function intOrderTests(coll, callback) { var tasks = []; for(var i = 0; i < 24; i++) { var t = new Task({name: "Task " + i, counter: i}); tasks.push(t); coll.add(t); } coll.order('counter', true).list(function(results) { for(var i = 0; i < 24; i++) { equals(results[i].id, tasks[i].id, "order check, ascending"); } tasks.reverse(); coll.order('counter', false).list(function(results) { for(var i = 0; i < 24; i++) { equals(results[i].id, tasks[i].id, "order check, descending"); } callback(); }); }); } function dateOrderTests(coll, callback) { var now = new Date(); function dateInDays(n) { var newDate = new Date(now.getTime()); newDate.setDate(newDate.getDate()+n); return newDate; } var tasks = []; for(var i = 0; i < 24; i++) { var t = new Task({name: "Task " + i, dateAdded: dateInDays(i)}); tasks.push(t); coll.add(t); } coll.order('dateAdded', true).list(function(results) { for(var i = 0; i < 24; i++) { equals(results[i].id, tasks[i].id, "order check, ascending"); } tasks.reverse(); coll.order('dateAdded', false).list(function(results) { for(var i = 0; i < 24; i++) { equals(results[i].id, tasks[i].id, "order check, descending"); } callback(); }); }); } asyncTest("Database INT order", function() { var p = new Project({name: "My project"}); persistence.add(p); intOrderTests(p.tasks(), start); }); asyncTest("Local INT order", function() { var coll = new persistence.LocalQueryCollection(); intOrderTests(coll, start); }); asyncTest("Database DATE order", function() { var p = new Project({name: "My project"}); persistence.add(p); dateOrderTests(p.tasks(), start); }); asyncTest("Local DATE order", function() { var coll = new persistence.LocalQueryCollection(); dateOrderTests(coll, start); }); function collectionLimitTests(coll, callback) { var tasks = []; for(var i = 0; i < 24; i++) { var t = new Task({name: "Task " + i, counter: i}); tasks.push(t); coll.add(t); } coll.order("counter", true).limit(5).list(function(results) { equals(results.length, 5, "Result length check"); for(var i = 0; i < 5; i++) { equals(results[i].id, tasks[i].id, "limit check"); } start(); }); } function collectionSkipTests(coll, callback) { var tasks = []; for(var i = 0; i < 24; i++) { var t = new Task({name: "Task " + i, counter: i}); tasks.push(t); coll.add(t); } coll.order("counter", true).skip(5).limit(5).list(function(results) { equals(results.length, 5, "Result length check"); for(var i = 5; i < 10; i++) { equals(results[i-5].id, tasks[i].id, "skip check"); } start(); }); } asyncTest("Database limit", function() { collectionLimitTests(Task.all(), start); }); asyncTest("Local limit", function() { var coll = new persistence.LocalQueryCollection(); collectionLimitTests(coll, start); }); asyncTest("Database skip", function() { collectionSkipTests(Task.all(), start); }); asyncTest("Local skip", function() { var coll = new persistence.LocalQueryCollection(); collectionSkipTests(coll, start); }); }); ================================================ FILE: test/browser/uki/uki-persistence.js ================================================ // Original file at: http://github.com/rsaccon/uki/tree/master/src/uki-persistence/persistence.js /** * persistencejs integration (http://www.persistencejs.org) * **/ // Example // ======= // // persistence engine // include('path/to/persistence.js'); // include('path/to/persistence.store.sql.js'); // include('path/to/persistence.store.websql.js'); // include('path/to/persistence.store.memory.js'); // include('path/to/persistence.sync.js'); // optional // include('path/to/persistence.search.js'); // optional // include('path/to/persistence.migrations.js'); // optional // include('path/to/uki-data/uki-persistence.js'); // // if (window.openDatabase) { // persistence.store.websql.config(persistence, 'myDbName', 'database', 5 * 1024 * 1024); // } else { // persistence.store.memory.config(persistence); // } // // var User = uki.persistence.define('User', { // firstname: "TEXT", // lastname: "TEXT" // }); // // var aUser = new User({firstname: "Joe", lastname: "Doo"}); // // aUser.firstname("Mike") ; // // console.log(aUser.firstname()); // => Mike // // persistence.add(aUser); // // persistence.flush(); /** * uki implementation for entity-property */ persistence.defineProp = function(scope, field, setterCallback, getterCallback) { scope[field] = function(value) { if (value === undefined) { return getterCallback(); } else { setterCallback(value); return scope; } }; }; /** * uki implementation for entity-property setter */ persistence.set = function(scope, fieldName, value) { if (persistence.isImmutable(fieldName)) throw "immutable field: "+fieldName; scope[fieldName](value); return scope; }; /** * uki implementation for entity-property getter */ persistence.get = function(arg1, arg2) { var val = (arguments.length == 1) ? arg1 : arg1[arg2]; return (typeof val === "function") ? val() : val; }; /** * uki ajax implementation */ if (persistence.sync) { uki.extend(persistence.sync, { getJSON: function(url, callback) { uki.getJSON(url, null, callback); }, postJSON: function(url, data, callback) { uki.ajax({ url: url, type: 'POST', data: data, dataType: 'json', success: function(response) { callback(JSON.parse(response)); } }); } }); } ================================================ FILE: test/browser/util.js ================================================ function tableExists(name, callback){ var sql = 'select name from sqlite_master where type = "table" and name == "'+name+'"'; persistence.transaction(function(tx){ tx.executeSql(sql, null, function(result){ ok(result.length == 1, name + ' table exists'); if (callback) callback(); }); }); } function checkNoTables(callback) { var sql = 'select name from sqlite_master where type = "table"'; persistence.transaction(function(tx){ tx.executeSql(sql, null, function(results){ var foundLegitimate = false; results.forEach(function(result) { if(result.name[0] !== '_') { foundLegitimate = true; } }); ok(!foundLegitimate, 'all tables are gone'); if (callback) callback(); }); }); } function tableNotExists(name, callback){ var sql = 'select name from sqlite_master where type = "table" and name == "'+name+'"'; persistence.transaction(function(tx){ tx.executeSql(sql, null, function(result){ ok(result.length == 0, name + ' table not exists'); if (callback) callback(); }); }); } function columnExists(table, column, type, callback) { var sql = 'select sql from sqlite_master where type = "table" and name == "'+table+'"'; type = type.replace('(', '\\(').replace(')', '\\)'); var regex = "CREATE TABLE .+`?" + column + "`?\\s+" + type + ".+"; persistence.transaction(function(tx){ tx.executeSql(sql, null, function(result){ console.log('Table def: ------> ', result[0].sql); ok(result[0].sql.match(regex), column + ' colum exists'); if (callback) callback(); }); }); } function columnNotExists(table, column, type, callback) { var sql = 'select sql from sqlite_master where type = "table" and name == "'+table+'"'; type = type.replace('(', '\\(').replace(')', '\\)'); var regex = "CREATE TABLE \\w+ \\((\\w|[\\(\\), ])*" + column + " " + type + "(\\w|[\\(\\), ])*\\)"; persistence.transaction(function(tx){ tx.executeSql(sql, null, function(result){ ok(!result[0].sql.match(regex), column + ' colum not exists'); if (callback) callback(); }); }); } function indexExists(table, column, callback) { var sql = 'select sql from sqlite_master where type = "index" and name == "'+table+'_'+column+'"'; persistence.transaction(function(tx){ tx.executeSql(sql, null, function(result){ ok(result.length == 1, 'index ' + table + '_' + column + ' exists'); if (callback) callback(); }); }); } function indexNotExists(table, column, callback) { var sql = 'select sql from sqlite_master where type = "index" and name == "'+table+'_'+column+'"'; persistence.transaction(function(tx){ tx.executeSql(sql, null, function(result){ ok(result.length == 0, 'index ' + table + '_' + column + ' not exists'); if (callback) callback(); }); }); } ================================================ FILE: test/node/node-blog.js ================================================ /** * Copyright (c) 2010 Zef Hemel * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * * USAGE: * On first run, be sure to initialize the database first: http://localhost:8888/init * otherwise the application will hang (because the select query fails). After that, * just visit http://localhost:8888/ */ var sys = require('sys'); var parseUrl = require('url').parse; var persistence = require('persistencejs/persistence').persistence; var persistenceStore = require('persistencejs/persistence.store.mysql'); // Database configuration persistenceStore.config(persistence, 'localhost', 3306, 'nodejs_mysql', 'test', 'test'); // Switch off query logging: //persistence.db.log = false; function log(o) { sys.print(sys.inspect(o) + "\n"); } // Data model var Post = persistence.define('Post', { title: "TEXT", text: "TEXT", date: "DATE" }); var Comment = persistence.define('Comment', { author: "TEXT", text: "TEXT", date: "DATE" }); Post.hasMany('comments', Comment, 'post'); // HTML utilities function htmlHeader(res, title) { res.write("" + title + ""); } function htmlFooter(res) { res.write('
                Home'); res.write(""); } // Actions function initDatabase(session, tx, req, res, callback) { htmlHeader(res, "Initializing database."); session.schemaSync(tx, function() { res.write("Done."); htmlFooter(res); callback(); }); } function resetDatabase(session, tx, req, res, callback) { htmlHeader(res, "Dropping all tables"); session.reset(tx, function() { res.write('All tables dropped, Click here to create fresh ones'); htmlFooter(res); callback(); }); } function showItems(session, tx, req, res, callback) { htmlHeader(res, "Blog"); res.write('

                Latest Posts

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

                ' + post.title + '

                '); res.write(post.text); res.write('
                '); res.write('Posted ' + post.date); } res.write('

                Create new post

                '); res.write('
                '); res.write('

                Title:

                '); res.write('

                '); res.write('

                '); res.write('
                '); htmlFooter(res); callback(); }); } function showItem(session, tx, req, res, callback) { htmlHeader(res, "Blog"); var query = parseUrl(req.url, true).query; Post.load(session, tx, query.id, function(post) { res.write('

                ' + post.title + '

                '); res.write(post.text); res.write('
                '); res.write('Posted ' + post.date); res.write('

                Comments

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

                By ' + comment.author + '

                '); res.write(comment.text); res.write('
                '); res.write('Posted ' + post.date); } res.write('

                Add a comment

                '); res.write('
                '); res.write(''); res.write('

                Your name:

                '); res.write('

                '); res.write('

                '); res.write('
                '); htmlFooter(res); callback(); }); }); } function post(session, tx, req, res, callback) { htmlHeader(res, "Created new post"); var query = parseUrl(req.url, true).query; var post = new Post(session, {title: query.title, text: query.text, date: new Date()}); session.add(post); session.flush(tx, function() { res.write('

                Post added.

                '); res.write('Go back'); htmlFooter(res); callback(); }); } function postComment(session, tx, req, res, callback) { htmlHeader(res, "Created new comment"); var query = parseUrl(req.url, true).query; var comment = new Comment(session, {text: query.text, author: query.author, date: new Date()}); Post.load(session, tx, query.post, function(post) { post.comments.add(comment); session.flush(tx, function() { res.write('

                Comment added.

                '); res.write('Go back'); htmlFooter(res); callback(); }); }); } var urlMap = { '/init': initDatabase, '/reset': resetDatabase, '/post': post, '/postComment': postComment, '/show': showItem, '/': showItems }; var http = require('http'); http.createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/html'}); var parsed = parseUrl(req.url, true); var fn = urlMap[parsed.pathname]; if(fn) { var session = persistenceStore.getSession(); session.transaction(function(tx) { fn(session, tx, req, res, function() { session.close(); res.end(); }); }); } else { res.end("Not found: " + req.url); } }).listen(8888, "127.0.0.1"); console.log('Server running at http://127.0.0.1:8888/'); ================================================ FILE: test/node/partial.sync.schema.sql ================================================ -- -- Table structure for table `_syncremovedobject` -- DROP TABLE IF EXISTS `_syncremovedobject`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `_syncremovedobject` ( `entity` varchar(255) DEFAULT NULL, `objectId` varchar(32) DEFAULT NULL, `date` bigint(20) DEFAULT NULL, `id` varchar(32) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `_syncremovedobject` -- LOCK TABLES `_syncremovedobject` WRITE; /*!40000 ALTER TABLE `_syncremovedobject` DISABLE KEYS */; /*!40000 ALTER TABLE `_syncremovedobject` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `class` -- DROP TABLE IF EXISTS `class`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `class` ( `class_id` int(11) NOT NULL AUTO_INCREMENT, `teacher_id` int(11) NOT NULL, `class_name` varchar(120) NOT NULL, `id` varchar(32) DEFAULT NULL, `_lastChange` bigint(20) DEFAULT NULL, PRIMARY KEY (`class_id`), KEY `fk_teacher_id` (`teacher_id`), CONSTRAINT `fk_teacher_id` FOREIGN KEY (`teacher_id`) REFERENCES `teacher` (`teacher_id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `class` -- LOCK TABLES `class` WRITE; /*!40000 ALTER TABLE `class` DISABLE KEYS */; INSERT INTO `class` VALUES (1,1,'Computer Systems','d2ffcf2a33c911e08f5eb58579ce9ead',1297199638),(2,2,'Linux/Dark Arts','d2ffd2e533c911e08f5eb58579ce9ead',1297199638); /*!40000 ALTER TABLE `class` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `student` -- DROP TABLE IF EXISTS `student`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `student` ( `student_id` int(11) NOT NULL AUTO_INCREMENT, `first_name` varchar(60) NOT NULL, `last_name` varchar(60) NOT NULL, `id` varchar(32) DEFAULT NULL, `_lastChange` bigint(20) DEFAULT NULL, PRIMARY KEY (`student_id`) ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `student` -- LOCK TABLES `student` WRITE; /*!40000 ALTER TABLE `student` DISABLE KEYS */; 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); /*!40000 ALTER TABLE `student` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `student_class` -- DROP TABLE IF EXISTS `student_class`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `student_class` ( `student_id` int(11) NOT NULL, `class_id` int(11) NOT NULL, `id` varchar(32) DEFAULT NULL, `_lastChange` bigint(20) DEFAULT NULL, KEY `fk_student_id` (`student_id`), KEY `fk_class_id` (`class_id`), CONSTRAINT `fk_class_id` FOREIGN KEY (`class_id`) REFERENCES `class` (`class_id`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT `fk_student_id` FOREIGN KEY (`student_id`) REFERENCES `student` (`student_id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `student_class` -- LOCK TABLES `student_class` WRITE; /*!40000 ALTER TABLE `student_class` DISABLE KEYS */; 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); /*!40000 ALTER TABLE `student_class` ENABLE KEYS */; UNLOCK TABLES; -- -- Table structure for table `teacher` -- DROP TABLE IF EXISTS `teacher`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `teacher` ( `teacher_id` int(11) NOT NULL AUTO_INCREMENT, `first_name` varchar(60) NOT NULL, `last_name` varchar(60) NOT NULL, `id` varchar(32) DEFAULT NULL, `_lastChange` bigint(20) DEFAULT NULL, PRIMARY KEY (`teacher_id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping data for table `teacher` -- LOCK TABLES `teacher` WRITE; /*!40000 ALTER TABLE `teacher` DISABLE KEYS */; INSERT INTO `teacher` VALUES (1,'Mark','Price','d1521f2933c911e08f5eb58579ce9ead',1297199641),(2,'Tony','Basil','d152235d33c911e08f5eb58579ce9ead',1297199641); /*!40000 ALTER TABLE `teacher` ENABLE KEYS */; UNLOCK TABLES; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; ================================================ FILE: test/node/test.error.handling.js ================================================ // $ expresso -s test/test.error.handling.js var assert = require('assert'); var persistence = require('../lib/persistence').persistence; var persistenceStore = require('../lib/persistence.store.mysql'); persistenceStore.config(persistence, 'localhost', 3306, 'nodejs_mysql', 'test', 'test'); var InexistentTable = persistence.define('inexistent_table', { name: "TEXT" }); var session = persistenceStore.getSession(); var create = function(data, cb) { var inexistent_table = new InexistentTable(data); session.add(inexistent_table); session.flush(function(result, err) { cb && cb(err, inexistent_table); }); }; var remove = function(inexistent_table, cb) { session.remove(inexistent_table); session.flush(function(result, err) { cb && cb(err, result); }); }; var temp; module.exports = { 'beforeAll': function(done) { session.transaction(function(tx) { tx.executeSql('FLUSH TABLES WITH READ LOCK;', function() { done(); }); }); }, 'schemaSync fail': function(done) { session.schemaSync(function(tx, err) { assert.isDefined(err); done(); }); }, 'create fail': function(done) { create({ name: 'test' }, function(err, result) { assert.isDefined(err); temp = result; done(); }); }, 'remove fail': function(done) { remove(temp, function(err, result) { assert.isDefined(err); done(); }); }, 'destroyAll fail': function(done) { InexistentTable.all(session).destroyAll(function(result, err) { assert.isDefined(err); done(); }); }, 'reset fail': function(done) { session.reset(function(result, err) { assert.isDefined(err); done(); }); }, afterAll: function(done) { session.transaction(function(tx) { tx.executeSql('UNLOCK TABLES;', function() { session.close(); done(); }); }); } }; ================================================ FILE: test/node/test.memory.store.js ================================================ // $ expresso -s test.memory.store.js var assert = require('assert'); var persistence = require('../../lib/persistence').persistence; var persistenceStore = require('../../lib/persistence.store.memory'); persistenceStore.config(persistence); var Task = persistence.define('Task', { username: 'TEXT' }); var data = { username: 'test' }; var task, session; module.exports = { init: function(done) { persistence.schemaSync(); session = persistenceStore.getSession(); done(); }, add: function(done) { task = new Task(data); session.add(task); session.flush(function(result, err) { assert.ifError(err); done(); }); }, get: function(done) { Task.findBy(session, 'id', task.id, function(task) { assert.equal(task.username, data.username); done(); }); }, update: function(done) { task.username = 'test2'; Task.findBy(session, 'id', task.id, function(task) { assert.equal(task.username, 'test2'); done(); }); }, remove: function(done) { session.remove(task); session.flush(function(result, err) { assert.ifError(err); Task.findBy(session, 'id', task.id, function(task) { assert.equal(task, null); done(); }); }); }, afterAll: function(done) { session.close(); done(); } }; ================================================ FILE: test/node/test.sqlite.store.js ================================================ // $ expresso -s test.sqlite.store.js var assert = require('assert'); var persistence = require('../../lib/persistence').persistence; var persistenceStore = require('../../lib/persistence.store.sqlite'); var dbPath = __dirname + '/test.db'; persistenceStore.config(persistence, dbPath); var Task = persistence.define('Task', { username: 'TEXT' }); var data = { username: 'test' }; var task, session; // remove test database function removeDb() { try { require('fs').unlinkSync(dbPath); } catch (err) { } } module.exports = { init: function(done) { removeDb(); session = persistenceStore.getSession(function () { session.schemaSync(done); }); }, add: function(done) { task = new Task(session, data); session.add(task); session.flush(function(result, err) { assert.ifError(err); done(); }); }, get: function(done) { Task.findBy(session, 'id', task.id, function(task) { assert.equal(task.username, data.username); done(); }); }, update: function(done) { task.username = 'test2'; Task.findBy(session, 'id', task.id, function(task) { assert.equal(task.username, 'test2'); done(); }); }, remove: function(done) { session.remove(task); session.flush(function(result, err) { assert.ifError(err); Task.findBy(session, 'id', task.id, function(task) { assert.equal(task, null); done(); }); }); }, afterAll: function(done) { session.close(function() { removeDb(); done(); }); } }; ================================================ FILE: test/node/test.sqlite3.store.js ================================================ // $ expresso -s test.sqlite3.store.js var assert = require('assert'); var persistence = require('../../lib/persistence').persistence; var persistenceStore = require('../../lib/persistence.store.sqlite3'); var dbPath = __dirname + '/test-sqlite3.db'; persistenceStore.config(persistence, dbPath); var Task = persistence.define('Task', { username: 'TEXT' }); var data = { username: 'test' }; var data2 = { username: 'test2' }; var task, task2, session; // remove test database function removeDb() { try { require('fs').unlinkSync(dbPath); } catch (err) { } } module.exports = { init: function(done) { removeDb(); session = persistenceStore.getSession(function () { session.schemaSync(done); }); }, add: function(done) { task = new Task(session, data); session.add(task); session.flush(function(result, err) { assert.ifError(err); done(); }); }, get: function(done) { Task.findBy(session, 'id', task.id, function(task) { assert.equal(task.username, data.username); done(); }); }, update: function(done) { task.username = 'test2'; Task.findBy(session, 'id', task.id, function(task) { assert.equal(task.username, 'test2'); done(); }); }, remove: function(done) { session.remove(task); session.flush(function(result, err) { assert.ifError(err); Task.findBy(session, 'id', task.id, function(task) { assert.equal(task, null); done(); }); }); }, addMultiple: function(done) { task = new Task(session, data); session.add(task); task2 = new Task(session, data2); session.add(task2); session.flush(function(result, err) { assert.ifError(err); var count = 0; Task.all(session).order('username', true).each(function(row) { count++; if (count == 1) { assert.equal(row.username, data.username); } if (count == 2) { assert.equal(row.username, data2.username); done(); } }); }); }, afterAll: function(done) { session.close(function() { removeDb(); done(); }); } }; ================================================ FILE: test/node/test.store.config.js ================================================ // $ expresso test.store.config.js var assert = require('assert'); var persistence = require('../../lib/persistence').persistence; var config = { adaptor: '', database: 'test', host: 'localhost', port: 3306, user: 'root', password: '' }; module.exports = { memory: function() { config.adaptor = 'memory'; var persistenceStore = require('../../lib/persistence.store.config').init(persistence, config); var session = persistenceStore.getSession(); session.close(); }, mysql: function() { config.adaptor = 'mysql'; var persistenceStore = require('../../lib/persistence.store.config').init(persistence, config); var session = persistenceStore.getSession(); session.close(); }, default: function() { var persistenceStore = require('../../lib/persistence.store.config').init(persistence, config); var session = persistenceStore.getSession(); session.close(); } }; ================================================ FILE: test/node/test.sync.server.js ================================================ /** * Copyright (c) 2010 Zef Hemel * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * * Requirements: * node.js * npm install connect * npm install express */ var sys = require('sys'); var connect = require('connect'); var express = require('express'); var persistence = require('../../lib/persistence').persistence; var persistenceStore = require('../../lib/persistence.store.mysql'); var persistenceSync = require('../../lib/persistence.sync.server'); // Database configuration persistenceStore.config(persistence, 'localhost', 3306, 'synctest', 'test', 'test'); // Switch off query logging: //persistence.db.log = false; function log(o) { sys.print(sys.inspect(o) + "\n"); } persistenceSync.config(persistence); // Data model var Project = persistence.define('Project', { name: "TEXT" }); var Task = persistence.define('Task', { name: "TEXT", done: "BOOL" }); var Tag = persistence.define('Tag', { name: "TEXT" }); Task.hasMany('tags', Tag, 'tasks'); Tag.hasMany('tasks', Task, 'tags'); Project.hasMany('tasks', Task, 'project'); Project.enableSync(); Task.enableSync(); Tag.enableSync(); var app = express.createServer( //connect.logger(), connect.bodyDecoder(), connect.staticProvider('../browser'), function(req, res, next) { var end = res.end; req.conn = persistenceStore.getSession(); res.end = function() { req.conn.close(); end.apply(res, arguments); }; req.conn.transaction(function(tx) { req.tx = tx; next(); }); } ); function generateDummyData(session) { var p = new Project(session, {name: "Main project"}); session.add(p); for(var i = 0; i < 25; i++) { var t = new Task(session, {name: "Task " + i, done: false}); p.tasks.add(t); } } // Actions app.get('/reset', function(req, res) { req.conn.reset(req.tx, function() { req.conn.schemaSync(req.tx, function() { generateDummyData(req.conn); req.conn.flush(req.tx, function() { res.send({status: "ok"}); }); }); }); }); app.get('/projectUpdates', function(req, res) { persistenceSync.pushUpdates(req.conn, req.tx, Project, req.query.since, function(updates) { res.send(updates); }); }); app.post('/projectUpdates', function(req, res) { persistenceSync.receiveUpdates(req.conn, req.tx, Project, req.body, function(result) { res.send(result); }); }); app.get('/taskUpdates', function(req, res) { persistenceSync.pushUpdates(req.conn, req.tx, Task, req.query.since, function(updates) { res.send(updates); }); }); app.post('/taskUpdates', function(req, res) { persistenceSync.receiveUpdates(req.conn, req.tx, Task, req.body, function(result) { res.send(result); }); }); app.get('/tagUpdates', function(req, res) { persistenceSync.pushUpdates(req.conn, req.tx, Tag, req.query.since, function(updates) { res.send(updates); }); }); app.post('/tagUpdates', function(req, res) { persistenceSync.receiveUpdates(req.conn, req.tx, Tag, req.body, function(result) { res.send(result); }); }); app.get('/markAllDone', function(req, res) { Task.all(req.conn).list(req.tx, function(tasks) { tasks.forEach(function(task) { task.done = true; }); req.conn.flush(req.tx, function() { res.send({status: 'ok'}); }); }); }); app.get('/markAllUndone', function(req, res) { Task.all(req.conn).list(req.tx, function(tasks) { tasks.forEach(function(task) { task.done = false; }); req.conn.flush(req.tx, function() { res.send({status: 'ok'}); }); }); }); app.listen(8888); console.log('Server running at http://127.0.0.1:8888/'); ================================================ FILE: test/titanium/.gitignore ================================================ tmp ================================================ FILE: test/titanium/Resources/app.js ================================================ var win = Titanium.UI.createWindow({ url:'runner.js', title: 'Unit Test' }); win.open(); ================================================ FILE: test/titanium/Resources/qunit/qunit.js ================================================ /* * QUnit - A JavaScript Unit Testing Framework * * http://docs.jquery.com/QUnit * * Copyright (c) 2009 John Resig, Jörn Zaefferer * Dual licensed under the MIT (MIT-LICENSE.txt) * and GPL (GPL-LICENSE.txt) licenses. */ (function(window) { var QUnit = { // call on start of module test to prepend name to all tests module: function(name, testEnvironment) { config.currentModule = name; synchronize(function() { if ( config.currentModule ) { QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all ); } config.currentModule = name; config.moduleTestEnvironment = testEnvironment; config.moduleStats = { all: 0, bad: 0 }; QUnit.moduleStart( name, testEnvironment ); }); }, asyncTest: function(testName, expected, callback) { if ( arguments.length === 2 ) { callback = expected; expected = 0; } QUnit.test(testName, expected, callback, true); }, test: function(testName, expected, callback, async) { var name = '' + testName + '', testEnvironment, testEnvironmentArg; if ( arguments.length === 2 ) { callback = expected; expected = null; } // is 2nd argument a testEnvironment? if ( expected && typeof expected === 'object') { testEnvironmentArg = expected; expected = null; } if ( config.currentModule ) { name = '' + config.currentModule + ": " + name; } if ( !validTest(config.currentModule + ": " + testName) ) { return; } synchronize(function() { testEnvironment = extend({ setup: function() {}, teardown: function() {} }, config.moduleTestEnvironment); if (testEnvironmentArg) { extend(testEnvironment,testEnvironmentArg); } QUnit.testStart( testName, testEnvironment ); // allow utility functions to access the current test environment QUnit.current_testEnvironment = testEnvironment; config.assertions = []; config.expected = expected; var tests = id("qunit-tests"); if (tests) { var b = document.createElement("strong"); b.innerHTML = "Running " + name; var li = document.createElement("li"); li.appendChild( b ); li.id = "current-test-output"; tests.appendChild( li ) } try { if ( !config.pollution ) { saveGlobal(); } testEnvironment.setup.call(testEnvironment); } catch(e) { QUnit.ok( false, "Setup failed on " + name + ": " + e.message ); } }); synchronize(function() { if ( async ) { QUnit.stop(); } try { callback.call(testEnvironment); } catch(e) { fail("Test " + name + " died, exception and test follows", e, callback); var message; if (e.message) { message = e.message; } else { message = JSON.stringify(e); } QUnit.ok( false, "Died on test #" + (config.assertions.length + 1) + ": " + message ); // else next test will carry the responsibility saveGlobal(); // Restart the tests if they're blocking if ( config.blocking ) { start(); } } }); synchronize(function() { try { checkPollution(); testEnvironment.teardown.call(testEnvironment); } catch(e) { QUnit.ok( false, "Teardown failed on " + name + ": " + e.message ); } }); synchronize(function() { try { QUnit.reset(); } catch(e) { fail("reset() failed, following Test " + name + ", exception and reset fn follows", e, reset); } if ( config.expected && config.expected != config.assertions.length ) { QUnit.ok( false, "Expected " + config.expected + " assertions, but " + config.assertions.length + " were run" ); } var good = 0, bad = 0, tests = id("qunit-tests"); config.stats.all += config.assertions.length; config.moduleStats.all += config.assertions.length; if ( tests ) { var ol = document.createElement("ol"); for ( var i = 0; i < config.assertions.length; i++ ) { var assertion = config.assertions[i]; var li = document.createElement("li"); li.className = assertion.result ? "pass" : "fail"; li.innerHTML = assertion.message || "(no message)"; ol.appendChild( li ); if ( assertion.result ) { good++; } else { bad++; config.stats.bad++; config.moduleStats.bad++; } } if (bad == 0) { ol.style.display = "none"; } var b = document.createElement("strong"); b.innerHTML = name + " (" + bad + ", " + good + ", " + config.assertions.length + ")"; addEvent(b, "click", function() { var next = b.nextSibling, display = next.style.display; next.style.display = display === "none" ? "block" : "none"; }); addEvent(b, "dblclick", function(e) { var target = e && e.target ? e.target : window.event.srcElement; if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) { target = target.parentNode; } if ( window.location && target.nodeName.toLowerCase() === "strong" ) { window.location.search = "?" + encodeURIComponent(getText([target]).replace(/\(.+\)$/, "").replace(/(^\s*|\s*$)/g, "")); } }); var li = id("current-test-output"); li.id = ""; li.className = bad ? "fail" : "pass"; li.removeChild( li.firstChild ); li.appendChild( b ); li.appendChild( ol ); if ( bad ) { var toolbar = id("qunit-testrunner-toolbar"); if ( toolbar ) { toolbar.style.display = "block"; id("qunit-filter-pass").disabled = null; id("qunit-filter-missing").disabled = null; } } } else { for ( var i = 0; i < config.assertions.length; i++ ) { if ( !config.assertions[i].result ) { bad++; config.stats.bad++; config.moduleStats.bad++; } } } QUnit.testDone( testName, bad, config.assertions.length ); if ( !window.setTimeout && !config.queue.length ) { done(); } }); if ( window.setTimeout && !config.doneTimer ) { config.doneTimer = window.setTimeout(function(){ if ( !config.queue.length ) { done(); } else { synchronize( done ); } }, 13); } }, /** * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. */ expect: function(asserts) { config.expected = asserts; }, /** * Asserts true. * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); */ ok: function(a, msg) { msg = escapeHtml(msg); QUnit.log(a, msg); config.assertions.push({ result: !!a, message: msg }); }, /** * Checks that the first two arguments are equal, with an optional message. * Prints out both actual and expected values. * * Prefered to ok( actual == expected, message ) * * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); * * @param Object actual * @param Object expected * @param String message (optional) */ equal: function(actual, expected, message) { push(expected == actual, actual, expected, message); }, notEqual: function(actual, expected, message) { push(expected != actual, actual, expected, message); }, deepEqual: function(actual, expected, message) { push(QUnit.equiv(actual, expected), actual, expected, message); }, notDeepEqual: function(actual, expected, message) { push(!QUnit.equiv(actual, expected), actual, expected, message); }, strictEqual: function(actual, expected, message) { push(expected === actual, actual, expected, message); }, notStrictEqual: function(actual, expected, message) { push(expected !== actual, actual, expected, message); }, raises: function(fn, message) { try { fn(); ok( false, message ); } catch (e) { ok( true, message ); } }, start: function(run_sync) { // A slight delay, to avoid any current callbacks if ( window.setTimeout && !run_sync) { window.setTimeout(function() { if ( config.timeout ) { clearTimeout(config.timeout); } config.blocking = false; process(); }, 13); } else { config.blocking = false; process(); } }, stop: function(timeout) { config.blocking = true; if ( timeout && window.setTimeout ) { config.timeout = window.setTimeout(function() { QUnit.ok( false, "Test timed out" ); QUnit.start(); }, timeout); } } }; // Backwards compatibility, deprecated QUnit.equals = QUnit.equal; QUnit.same = QUnit.deepEqual; // Maintain internal state var config = { // The queue of tests to run queue: [], // block until document ready blocking: true }; // Load paramaters (function() { var location = window.location || { search: "", protocol: "file:" }, GETParams = location.search.slice(1).split('&'); for ( var i = 0; i < GETParams.length; i++ ) { GETParams[i] = decodeURIComponent( GETParams[i] ); if ( GETParams[i] === "noglobals" ) { GETParams.splice( i, 1 ); i--; config.noglobals = true; } else if ( GETParams[i].search('=') > -1 ) { GETParams.splice( i, 1 ); i--; } } // restrict modules/tests by get parameters config.filters = GETParams; // Figure out if we're running the tests from a server or not QUnit.isLocal = !!(location.protocol === 'file:'); })(); // Expose the API as global variables, unless an 'exports' // object exists, in that case we assume we're in CommonJS if ( typeof exports === "undefined" || typeof require === "undefined" ) { extend(window, QUnit); window.QUnit = QUnit; } else { extend(exports, QUnit); exports.QUnit = QUnit; } // define these after exposing globals to keep them in these QUnit namespace only extend(QUnit, { config: config, // Initialize the configuration options init: function() { extend(config, { stats: { all: 0, bad: 0 }, moduleStats: { all: 0, bad: 0 }, started: +new Date, updateRate: 1000, blocking: false, autostart: true, autorun: false, assertions: [], filters: [], queue: [] }); var tests = id("qunit-tests"), banner = id("qunit-banner"), result = id("qunit-testresult"); if ( tests ) { tests.innerHTML = ""; } if ( banner ) { banner.className = ""; } if ( result ) { result.parentNode.removeChild( result ); } }, /** * Resets the test setup. Useful for tests that modify the DOM. */ reset: function() { if ( window.jQuery ) { jQuery("#main, #qunit-fixture").html( config.fixture ); } }, /** * Trigger an event on an element. * * @example triggerEvent( document.body, "click" ); * * @param DOMElement elem * @param String type */ triggerEvent: function( elem, type, event ) { if ( document.createEvent ) { event = document.createEvent("MouseEvents"); event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 0, 0, 0, 0, 0, false, false, false, false, 0, null); elem.dispatchEvent( event ); } else if ( elem.fireEvent ) { elem.fireEvent("on"+type); } }, // Safe object type checking is: function( type, obj ) { return QUnit.objectType( obj ) == type; }, objectType: function( obj ) { if (typeof obj === "undefined") { return "undefined"; // consider: typeof null === object } if (obj === null) { return "null"; } var type = Object.prototype.toString.call( obj ) .match(/^\[object\s(.*)\]$/)[1] || ''; switch (type) { case 'Number': if (isNaN(obj)) { return "nan"; } else { return "number"; } case 'String': case 'Boolean': case 'Array': case 'Date': case 'RegExp': case 'Function': return type.toLowerCase(); } if (typeof obj === "object") { return "object"; } return undefined; }, // Logging callbacks begin: function() {}, done: function(failures, total) {}, log: function(result, message) {}, testStart: function(name, testEnvironment) {}, testDone: function(name, failures, total) {}, moduleStart: function(name, testEnvironment) {}, moduleDone: function(name, failures, total) {} }); if ( typeof document === "undefined" || document.readyState === "complete" ) { config.autorun = true; } addEvent(window, "load", function() { QUnit.begin(); // Initialize the config, saving the execution queue var oldconfig = extend({}, config); QUnit.init(); extend(config, oldconfig); config.blocking = false; var userAgent = id("qunit-userAgent"); if ( userAgent ) { userAgent.innerHTML = navigator.userAgent; } var toolbar = id("qunit-testrunner-toolbar"); if ( toolbar ) { toolbar.style.display = "none"; var filter = document.createElement("input"); filter.type = "checkbox"; filter.id = "qunit-filter-pass"; filter.disabled = true; addEvent( filter, "click", function() { var li = document.getElementsByTagName("li"); for ( var i = 0; i < li.length; i++ ) { if ( li[i].className.indexOf("pass") > -1 ) { li[i].style.display = filter.checked ? "none" : ""; } } }); toolbar.appendChild( filter ); var label = document.createElement("label"); label.setAttribute("for", "qunit-filter-pass"); label.innerHTML = "Hide passed tests"; toolbar.appendChild( label ); var missing = document.createElement("input"); missing.type = "checkbox"; missing.id = "qunit-filter-missing"; missing.disabled = true; addEvent( missing, "click", function() { var li = document.getElementsByTagName("li"); for ( var i = 0; i < li.length; i++ ) { if ( li[i].className.indexOf("fail") > -1 && li[i].innerHTML.indexOf('missing test - untested code is broken code') > - 1 ) { li[i].parentNode.parentNode.style.display = missing.checked ? "none" : "block"; } } }); toolbar.appendChild( missing ); label = document.createElement("label"); label.setAttribute("for", "qunit-filter-missing"); label.innerHTML = "Hide missing tests (untested code is broken code)"; toolbar.appendChild( label ); } var main = id('main') || id('qunit-fixture'); if ( main ) { config.fixture = main.innerHTML; } if (config.autostart) { QUnit.start(true); } }); function done() { if ( config.doneTimer && window.clearTimeout ) { window.clearTimeout( config.doneTimer ); config.doneTimer = null; } if ( config.queue.length ) { config.doneTimer = window.setTimeout(function(){ if ( !config.queue.length ) { done(); } else { synchronize( done ); } }, 13); return; } config.autorun = true; // Log the last module results if ( config.currentModule ) { QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all ); } var banner = id("qunit-banner"), tests = id("qunit-tests"), html = ['Tests completed in ', +new Date - config.started, ' milliseconds.
                ', '', config.stats.all - config.stats.bad, ' tests of ', config.stats.all, ' passed, ', config.stats.bad,' failed.'].join(''); if ( banner ) { banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); } if ( tests ) { var result = id("qunit-testresult"); if ( !result ) { result = document.createElement("p"); result.id = "qunit-testresult"; result.className = "result"; tests.parentNode.insertBefore( result, tests.nextSibling ); } result.innerHTML = html; } QUnit.done( config.stats.bad, config.stats.all ); } function validTest( name ) { var i = config.filters.length, run = false; if ( !i ) { return true; } while ( i-- ) { var filter = config.filters[i], not = filter.charAt(0) == '!'; if ( not ) { filter = filter.slice(1); } if ( name.indexOf(filter) !== -1 ) { return !not; } if ( not ) { run = true; } } return run; } function escapeHtml(s) { s = s === null ? "" : s + ""; return s.replace(/[\&"<>\\]/g, function(s) { switch(s) { case "&": return "&"; case "\\": return "\\\\"; case '"': return '\"'; case "<": return "<"; case ">": return ">"; default: return s; } }); } function push(result, actual, expected, message) { message = escapeHtml(message) || (result ? "okay" : "failed"); message = '' + message + ""; expected = escapeHtml(QUnit.jsDump.parse(expected)); actual = escapeHtml(QUnit.jsDump.parse(actual)); var output = message + ', expected: ' + expected + ''; if (actual != expected) { output += ' result: ' + actual + ', diff: ' + QUnit.diff(expected, actual); } // can't use ok, as that would double-escape messages QUnit.log(result, output); config.assertions.push({ result: !!result, message: output }); } function synchronize( callback ) { config.queue.push( callback ); if ( config.autorun && !config.blocking ) { process(); } } function process() { var start = (new Date()).getTime(); while ( config.queue.length && !config.blocking ) { if ( config.updateRate <= 0 || (((new Date()).getTime() - start) < config.updateRate) ) { config.queue.shift()(); } else { setTimeout( process, 13 ); break; } } } function saveGlobal() { config.pollution = []; if ( config.noglobals ) { for ( var key in window ) { config.pollution.push( key ); } } } function checkPollution( name ) { var old = config.pollution; saveGlobal(); var newGlobals = diff( old, config.pollution ); if ( newGlobals.length > 0 ) { ok( false, "Introduced global variable(s): " + newGlobals.join(", ") ); config.expected++; } var deletedGlobals = diff( config.pollution, old ); if ( deletedGlobals.length > 0 ) { ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") ); config.expected++; } } // returns a new Array with the elements that are in a but not in b function diff( a, b ) { var result = a.slice(); for ( var i = 0; i < result.length; i++ ) { for ( var j = 0; j < b.length; j++ ) { if ( result[i] === b[j] ) { result.splice(i, 1); i--; break; } } } return result; } function fail(message, exception, callback) { if ( typeof console !== "undefined" && console.error && console.warn ) { console.error(message); console.error(exception); console.warn(callback.toString()); } else if ( window.opera && opera.postError ) { opera.postError(message, exception, callback.toString); } } function extend(a, b) { for ( var prop in b ) { a[prop] = b[prop]; } return a; } function addEvent(elem, type, fn) { if ( elem.addEventListener ) { elem.addEventListener( type, fn, false ); } else if ( elem.attachEvent ) { elem.attachEvent( "on" + type, fn ); } else { fn(); } } function id(name) { return !!(typeof document !== "undefined" && document && document.getElementById) && document.getElementById( name ); } // Test for equality any JavaScript type. // Discussions and reference: http://philrathe.com/articles/equiv // Test suites: http://philrathe.com/tests/equiv // Author: Philippe Rathé QUnit.equiv = function () { var innerEquiv; // the real equiv function var callers = []; // stack to decide between skip/abort functions var parents = []; // stack to avoiding loops from circular referencing // Call the o related callback with the given arguments. function bindCallbacks(o, callbacks, args) { var prop = QUnit.objectType(o); if (prop) { if (QUnit.objectType(callbacks[prop]) === "function") { return callbacks[prop].apply(callbacks, args); } else { return callbacks[prop]; // or undefined } } } var callbacks = function () { // for string, boolean, number and null function useStrictEquality(b, a) { if (b instanceof a.constructor || a instanceof b.constructor) { // to catch short annotaion VS 'new' annotation of a declaration // e.g. var i = 1; // var j = new Number(1); return a == b; } else { return a === b; } } return { "string": useStrictEquality, "boolean": useStrictEquality, "number": useStrictEquality, "null": useStrictEquality, "undefined": useStrictEquality, "nan": function (b) { return isNaN(b); }, "date": function (b, a) { return QUnit.objectType(b) === "date" && a.valueOf() === b.valueOf(); }, "regexp": function (b, a) { return QUnit.objectType(b) === "regexp" && a.source === b.source && // the regex itself a.global === b.global && // and its modifers (gmi) ... a.ignoreCase === b.ignoreCase && a.multiline === b.multiline; }, // - skip when the property is a method of an instance (OOP) // - abort otherwise, // initial === would have catch identical references anyway "function": function () { var caller = callers[callers.length - 1]; return caller !== Object && typeof caller !== "undefined"; }, "array": function (b, a) { var i, j, loop; var len; // b could be an object literal here if ( ! (QUnit.objectType(b) === "array")) { return false; } len = a.length; if (len !== b.length) { // safe and faster return false; } //track reference to avoid circular references parents.push(a); for (i = 0; i < len; i++) { loop = false; for(j=0;j= 0) { type = "array"; } else { type = typeof obj; } return type; }, separator:function() { return this.multiline ? this.HTML ? '
                ' : '\n' : this.HTML ? ' ' : ' '; }, indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing if ( !this.multiline ) return ''; var chr = this.indentChar; if ( this.HTML ) chr = chr.replace(/\t/g,' ').replace(/ /g,' '); return Array( this._depth_ + (extra||0) ).join(chr); }, up:function( a ) { this._depth_ += a || 1; }, down:function( a ) { this._depth_ -= a || 1; }, setParser:function( name, parser ) { this.parsers[name] = parser; }, // The next 3 are exposed so you can use them quote:quote, literal:literal, join:join, // _depth_: 1, // This is the list of parsers, to modify them, use jsDump.setParser parsers:{ window: '[Window]', document: '[Document]', error:'[ERROR]', //when no parser is found, shouldn't happen unknown: '[Unknown]', 'null':'null', undefined:'undefined', 'function':function( fn ) { var ret = 'function', name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE if ( name ) ret += ' ' + name; ret += '('; ret = [ ret, this.parse( fn, 'functionArgs' ), '){'].join(''); return join( ret, this.parse(fn,'functionCode'), '}' ); }, array: array, nodelist: array, arguments: array, object:function( map ) { var ret = [ ]; this.up(); for ( var key in map ) ret.push( this.parse(key,'key') + ': ' + this.parse(map[key]) ); this.down(); return join( '{', ret, '}' ); }, node:function( node ) { var open = this.HTML ? '<' : '<', close = this.HTML ? '>' : '>'; var tag = node.nodeName.toLowerCase(), ret = open + tag; for ( var a in this.DOMAttrs ) { var val = node[this.DOMAttrs[a]]; if ( val ) ret += ' ' + a + '=' + this.parse( val, 'attribute' ); } return ret + close + open + '/' + tag + close; }, functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function var l = fn.length; if ( !l ) return ''; var args = Array(l); while ( l-- ) args[l] = String.fromCharCode(97+l);//97 is 'a' return ' ' + args.join(', ') + ' '; }, key:quote, //object calls it internally, the key part of an item in a map functionCode:'[code]', //function calls it internally, it's the content of the function attribute:quote, //node calls it internally, it's an html attribute value string:quote, date:quote, regexp:literal, //regex number:literal, 'boolean':literal }, DOMAttrs:{//attributes to dump from nodes, name=>realName id:'id', name:'name', 'class':'className' }, HTML:false,//if true, entities are escaped ( <, >, \t, space and \n ) indentChar:' ',//indentation unit multiline:false //if true, items in a collection, are separated by a \n, else just a space. }; return jsDump; })(); // from Sizzle.js function getText( elems ) { var ret = "", elem; for ( var i = 0; elems[i]; i++ ) { elem = elems[i]; // Get the text from text nodes and CDATA nodes if ( elem.nodeType === 3 || elem.nodeType === 4 ) { ret += elem.nodeValue; // Traverse everything else, except comment nodes } else if ( elem.nodeType !== 8 ) { ret += getText( elem.childNodes ); } } return ret; }; /* * Javascript Diff Algorithm * By John Resig (http://ejohn.org/) * Modified by Chu Alan "sprite" * * Released under the MIT license. * * More Info: * http://ejohn.org/projects/javascript-diff-algorithm/ * * Usage: QUnit.diff(expected, actual) * * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the quick brown fox jumped jumps over" */ QUnit.diff = (function() { function diff(o, n){ var ns = new Object(); var os = new Object(); for (var i = 0; i < n.length; i++) { if (ns[n[i]] == null) ns[n[i]] = { rows: new Array(), o: null }; ns[n[i]].rows.push(i); } for (var i = 0; i < o.length; i++) { if (os[o[i]] == null) os[o[i]] = { rows: new Array(), n: null }; os[o[i]].rows.push(i); } for (var i in ns) { if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) { n[ns[i].rows[0]] = { text: n[ns[i].rows[0]], row: os[i].rows[0] }; o[os[i].rows[0]] = { text: o[os[i].rows[0]], row: ns[i].rows[0] }; } } for (var i = 0; i < n.length - 1; i++) { if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null && n[i + 1] == o[n[i].row + 1]) { n[i + 1] = { text: n[i + 1], row: n[i].row + 1 }; o[n[i].row + 1] = { text: o[n[i].row + 1], row: i + 1 }; } } for (var i = n.length - 1; i > 0; i--) { if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null && n[i - 1] == o[n[i].row - 1]) { n[i - 1] = { text: n[i - 1], row: n[i].row - 1 }; o[n[i].row - 1] = { text: o[n[i].row - 1], row: i - 1 }; } } return { o: o, n: n }; } return function(o, n){ o = o.replace(/\s+$/, ''); n = n.replace(/\s+$/, ''); var out = diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/)); var str = ""; var oSpace = o.match(/\s+/g); if (oSpace == null) { oSpace = [" "]; } else { oSpace.push(" "); } var nSpace = n.match(/\s+/g); if (nSpace == null) { nSpace = [" "]; } else { nSpace.push(" "); } if (out.n.length == 0) { for (var i = 0; i < out.o.length; i++) { str += '' + out.o[i] + oSpace[i] + ""; } } else { if (out.n[0].text == null) { for (n = 0; n < out.o.length && out.o[n].text == null; n++) { str += '' + out.o[n] + oSpace[n] + ""; } } for (var i = 0; i < out.n.length; i++) { if (out.n[i].text == null) { str += '' + out.n[i] + nSpace[i] + ""; } else { var pre = ""; for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) { pre += '' + out.o[n] + oSpace[n] + ""; } str += " " + out.n[i].text + nSpace[i] + pre; } } } return str; } })(); })(this); ================================================ FILE: test/titanium/Resources/qunit/titanium_adaptor.js ================================================ Titanium.include('qunit/qunit.js'); // ============================================================================= // Uncomment the following lines in order to get jsMockito support for mocking // (after following jsMockito install instructions) // ============================================================================= // Titanium.include('qunit/jshamcrest.js'); // Titanium.include('qunit/jsmockito-1.0.2.js'); // JsHamcrest.Integration.QUnit(); // JsMockito.Integration.importTo(this); var logger = function(failures, message) { if (failures) { Titanium.API.error(message); } else { Titanium.API.info(message); } }; // QUnit.testStart(name) is called whenever a new test batch of assertions starts running. name is the string name of the test batch. QUnit.testStart = function(name) { logger(false, '>> >> >>TEST START: '+name); }; // 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. QUnit.testDone = function(name, failures, total) { logger(failures, '<< << <> >>MODULE START: '+name); }; // 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. QUnit.moduleDone = function(name, failures, total) { logger(failures, '<< < persistencejs.titanium.test titanium 1.0 staugaard https://github.com/zefhemel/persistencejs No description provided 2011 by staugaard default_app_logo.png false false default false false false true add78b91-427a-456d-9d6f-e21a272adf95 Ti.UI.PORTRAIT Ti.UI.PORTRAIT Ti.UI.UPSIDE_PORTRAIT Ti.UI.LANDSCAPE_LEFT Ti.UI.LANDSCAPE_RIGHT