Showing preview only (586K chars total). Download the full file or copy to clipboard to get everything.
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 <zef@zef.me>
Fabio Rehm <fgrehm@gmail.com>
Lukas Berns
Roberto Saccon <rsaccon@gmail.com>
Wilker Lúcio <wilkerlucio@gmail.com>
Bruno Jouhier <bjouhier@gmail.com>
Robin Wenglewski <robin@wenglewski.de>
Matthias Hochgatterer <matthias.hochgatterer@gmail.com>
Chris Chua <chris.sirhc@gmail.com>
Mike Smullin <mike@smullindesign.com>
Masahiro Hayashi <hayashi.masahiro@gmail.com>
Mick Staugaard <mick@staugaard.com>
Shane Tomlinson <set117@gmail.com>
Eugene Ware <eugene.ware@nextharbour.com>
================================================
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:
<script src="persistence.js" type="application/javascript"></script>
<script src="persistence.store.sql.js" type="application/javascript"></script>
<script src="persistence.store.websql.js" type="application/javascript"></script>
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 `<script>` to your `index.html`:
`lib/persistence.js` needs to be added, as well as any data stores you want to use. Note that the `mysql` and
`websql` stores both depend on the `sql` store. A typical setup requires you to add at least
`lib/persistence.js`, `lib/persistence.store.sql.js` and `lib/persistence.store.websql.js` as follows:
<script src="/bower_components/persistencejs/lib/persistence.js"></script>
<script src="/bower_components/persistencejs/lib/persistence.store.sql.js"></script>
<script src="/bower_components/persistencejs/lib/persistence.store.websql.js"></script>
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:
<a href="http://flattr.com/thing/2510/persistence-js" target="_blank">
<img src="http://api.flattr.com/button/button-static-50x60.png" title="Flattr this" border="0" /></a>
================================================
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 <zef@zef.me>",
"Fabio Rehm <fgrehm@gmail.com>",
"Lukas Berns",
"Roberto Saccon <rsaccon@gmail.com>",
"Wilker Lúcio <wilkerlucio@gmail.com>",
"Bruno Jouhier <bjouhier@gmail.com>",
"Robin Wenglewski <robin@wenglewski.de>",
"Matthias Hochgatterer <matthias.hochgatterer@gmail.com>",
"Chris Chua <chris.sirhc@gmail.com>",
"Mike Smullin <mike@smullindesign.com>",
"Masahiro Hayashi <hayashi.masahiro@gmail.com>",
"Mick Staugaard <mick@staugaard.com>",
"Shane Tomlinson <set117@gmail.com>",
"Eugene Ware <eugene.ware@nextharbour.com>"
],
"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
================================================
<!DOCTYPE html>
<html>
<head>
<title>Text only</title>
</head>
<body>
<div data-role="page">
<div data-role="header">
<h1>Text only</h1>
</div><!-- /header -->
<div data-role="content">
<p>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.</p>
<p>The critical difference with our approach is the <a href="platforms.html">wide variety of mobile platforms we’re targeting</a> 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.</p>
<p>To make this broad support possible, all pages in jQuery Mobile are built on a foundation of <strong>clean, semantic HTML</strong> to ensure compatibility with pretty much any web-enabled device. In devices that interpret CSS and JavaScript, jQuery Mobile applies <strong>progressive enhancement techniques</strong> to unobtrusively transform the semantic page into a rich, interactive experience that leverages the power of jQuery and CSS. <strong>Accessibility features</strong> such as WAI-ARIA are tightly integrated throughout the framework to provide support for screen readers and other assistive technologies.</p>
</div><!-- /content -->
</div><!-- /page -->
</body>
</html>
================================================
FILE: demo/jquerymobile/docs/text_and_images.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>Text and image</title>
</head>
<body>
<div data-role="page">
<div data-role="header">
<h1>Text and image</h1>
</div><!-- /header -->
<div data-role="content">
<p>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.</p>
<p>The critical difference with our approach is the <a href="platforms.html">wide variety of mobile platforms we’re targeting</a> 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.</p>
<p>To make this broad support possible, all pages in jQuery Mobile are built on a foundation of <strong>clean, semantic HTML</strong> to ensure compatibility with pretty much any web-enabled device. In devices that interpret CSS and JavaScript, jQuery Mobile applies <strong>progressive enhancement techniques</strong> to unobtrusively transform the semantic page into a rich, interactive experience that leverages the power of jQuery and CSS. <strong>Accessibility features</strong> such as WAI-ARIA are tightly integrated throughout the framework to provide support for screen readers and other assistive technologies.</p>
<img src="../assets/ipad-palm.png" alt="Smartphone and tablet designs" style="max-width:100%; margin-top:20px;">
</div><!-- /content -->
</div><!-- /page -->
</body>
</html>
================================================
FILE: demo/jquerymobile/index.html
================================================
<!DOCTYPE html>
<html lang='en' >
<head>
<title>jQuery mobile / persistencejs integration</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" href="http://code.jquery.com/mobile/1.0a2/jquery.mobile-1.0a2.min.css" />
<style>
.ui-mobile #jqm-home { background: #e5e5e5 url(assets/jqm-sitebg.png) top center repeat-x; }
.ui-mobile #jqm-homeheader { padding: 55px 25px 0; text-align: center }
.ui-mobile #jqm-homeheader h1 { margin: 0 0 10px; }
.ui-mobile #jqm-homeheader p { margin: 0; }
.ui-mobile #jqm-version { text-indent: -99999px; background: url(assets/version.png) top right no-repeat; width: 119px; height: 122px; overflow: hidden; position: absolute; top: 0; right: 0; }
.ui-mobile .jqm-themeswitcher { clear: both; margin: 20px 0 0; }
h2 { margin-top:1.5em; }
p code { font-size:1.2em; font-weight:bold; }
dt { font-weight: bold; margin: 2em 0 .5em; }
dt code, dd code { font-size:1.3em; line-height:150%; }
</style>
<script src="http://code.jquery.com/jquery-1.4.4.min.js"></script>
<script src="http://code.jquery.com/mobile/1.0a2/jquery.mobile-1.0a2.min.js"></script>
<script src="http://code.google.com/apis/gears/gears_init.js"></script>
<script src="../../lib/persistence.js"></script>
<script src="../../lib/persistence.store.sql.js"></script>
<script src="../../lib/persistence.store.websql.js"></script>
<script src="../../lib/persistence.store.memory.js"></script>
<script src="../../lib/persistence.jquery.js"></script>
<script src="../../lib/persistence.jquery.mobile.js"></script>
<script type="text/javascript">
if (location.protocol == "file:") {
alert("Didn't you read the README ? You need to load this page from a server.");
}
if (window.openDatabase) {
persistence.store.websql.config(persistence, "jquerymobile", 'database', 5 * 1024 * 1024);
} else {
persistence.store.memory.config(persistence);
}
persistence.define('Page', {
path: "TEXT",
data: "TEXT",
});
persistence.define('Image', {
path: "TEXT",
data: "TEXT",
});
persistence.define('Order', {
shipping: "TEXT"
});
persistence.schemaSync();
$('#reset').click(function() {
persistence.reset();
persistence.schemaSync();
return false;
});
</script>
</head>
<body>
<div data-role="page" data-theme="b" id="jqm-home">
<div id="jqm-homeheader">
<h1 id="jqm-logo"><img src="assets/jquery-logo.png" alt="jQuery Mobile Framework" width="235" height="61" /></h1>
<p>Touch-Optimized Web Framework for Smartphones & Tablets - now with PersistenceJS integration</p>
<p id="jqm-version">Alpha Release</p>
</div>
<div data-role="content">
<ul data-role="listview" data-inset="true" data-theme="c" data-dividertheme="b">
<li data-role="list-divider">Local persistence demos</li>
<li><a href="docs/text.html">Text only</a></li>
<li><a href="docs/text_and_images.html">Text and images</a></li>
<li><a href="#form_submission">Form submission</a></li>
</ul>
</div>
<a id="reset" href="#" data-role="button" data-theme="c" class="ui-btn-right">Reset DB</a>
</div>
<div data-role="page" id="form_submission">
<div data-role="header">
<h1>Form submission</h1>
</div><!-- /header -->
<div data-role="content">
<form action="order/form-fake-response.html" method="post">
<fieldset>
<div data-role="fieldcontain">
<label for="shipping" class="select">Formsubmission (GET):</label>
<select name="shipping" id="shipping">
<option value="Standard shipping">Standard: 7 day</option>
<option value="Rush shipping">Rush: 3 days</option>
<option value="Express shipping">Express: next day</option>
<option value="Overnight shipping">Overnight</option>
</select>
</div>
<button type="submit" data-theme="a">Submit</button>
</fieldset>
</form>
</div>
</div>
</body>
</html>
================================================
FILE: demo/jquerymobile/order/form-fake-response.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>Form submission</title>
</head>
<body>
<div data-role="page">
<div data-role="header">
<h1>Sample form response</h1>
</div><!-- /header -->
<div data-role="content">
<h1>Fake response</h1>
<h2>You choose: (your value here if it would be no fake)</h2>
</div><!-- /content -->
</div><!-- /page -->
</body>
</html>
================================================
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=<UNIX MS TIMESTAMP>` 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 <rsaccon@gmail.com>
*
* 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 <rsaccon@gmail.com>
*
* 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 <stevenlevithan.com>
// 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 = /(<img[^>]+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<entities.length; j++) {
persistence.add(entities[j]);
}
persistence.flush();
}
});
$(img).error(function() {
crawlImages = false;
});
});
}
var obj = {};
obj[$pjqm.pathField] = settings.url;
obj[$pjqm.dataField] = data;
entities.push(new Page(obj));
if (!crawlImages) {
persistence.add(entities[0]);
persistence.flush();
}
}
},
error: settings.error
});
}
});
} else {
originalAjaxMethod(settings);
}
}
};
}
})(jQuery);
================================================
FILE: lib/persistence.js
================================================
/**
* Copyright (c) 2010 Zef Hemel <zef@zef.me>
*
* 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 <fgrehm@gmail.com>
*
* 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 <zef@zef.me>
*
* 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);
},
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
SYMBOL INDEX (181 symbols across 32 files)
FILE: lib/persistence.jquery.mobile.js
function expand (line 43) | function expand(docPath, srcPath) {
function base64Image (line 66) | function base64Image(img, type) {
function parseUri (line 95) | function parseUri (str) {
function getImageType (line 111) | function getImageType(parsedUri) {
FILE: lib/persistence.js
function initPersistence (line 55) | function initPersistence(persistence) {
function f (line 2235) | function f(n) {
function quote (line 2272) | function quote(string) {
function str (line 2284) | function str(key, holder) {
function walk (line 2384) | function walk(holder, key) {
FILE: lib/persistence.migrations.js
function migrateOne (line 85) | function migrateOne() {
function migrateOne (line 114) | function migrateOne() {
function nextAction (line 169) | function nextAction() {
FILE: lib/persistence.pool.js
function log (line 4) | function log(o) {
function ConnectionPool (line 8) | function ConnectionPool(getSession, initialPoolSize) {
FILE: lib/persistence.search.js
function normalizeWord (line 45) | function normalizeWord(word, filterShortWords) {
function searchTokenizer (line 58) | function searchTokenizer(text) {
function searchPhraseParser (line 80) | function searchPhraseParser(query, indexTbl, prefixByDefault) {
function SearchFilter (line 114) | function SearchFilter(query, entityName) {
function SearchQueryCollection (line 160) | function SearchQueryCollection(session, entityName, query, prefixByDefau...
FILE: lib/persistence.store.appengine.js
function dbValToEntityVal (line 28) | function dbValToEntityVal(val, type) {
function entityValToDbVal (line 67) | function entityValToDbVal(val, type) {
function aeEntityToEntity (line 87) | function aeEntityToEntity(session, aeEnt, Entity) {
function entityToAEEntity (line 103) | function entityToAEEntity(meta, o) {
function prepareDbEntity (line 198) | function prepareDbEntity(obj) {
function prepareQuery (line 261) | function prepareQuery(coll, callback) {
FILE: lib/persistence.store.memory.js
function makeLocalClone (line 132) | function makeLocalClone(otherColl) {
FILE: lib/persistence.store.mysql.js
function log (line 13) | function log(o) {
function handleDisconnect (line 29) | function handleDisconnect() {
function transaction (line 76) | function transaction(conn){
FILE: lib/persistence.store.react-native.js
function log (line 16) | function log(o) {
function transaction (line 50) | function transaction(conn){
FILE: lib/persistence.store.sql.js
function config (line 125) | function config(persistence, dialect) {
FILE: lib/persistence.store.sqlite.js
function log (line 14) | function log(o) {
function transaction (line 49) | function transaction(conn){
FILE: lib/persistence.store.sqlite3.js
function log (line 16) | function log(o) {
function transaction (line 50) | function transaction(conn){
FILE: lib/persistence.sync.js
function getEpoch (line 78) | function getEpoch(date) {
function encodeUrlObj (line 102) | function encodeUrlObj(obj) {
function cacheAndFindUpdates (line 115) | function cacheAndFindUpdates(session, Entity, objects, lastLocalSyncTime...
function synchronize (line 278) | function synchronize(session, uri, Entity, conflictCallback, callback, e...
FILE: lib/persistence.sync.server.js
function log (line 33) | function log(o) {
function jsonToEntityVal (line 37) | function jsonToEntityVal(value, type) {
function getEpoch (line 56) | function getEpoch(date) {
function entityValToJson (line 60) | function entityValToJson(value, type) {
FILE: lib/persistence.sync.server.php
class PersistenceDB (line 40) | class PersistenceDB {
method __construct (line 44) | function __construct(PDO $db, $persistence_table) {
method getObjectChanges (line 49) | public function getObjectChanges($bucket, $since) {
method applyObjectChanges (line 64) | public function applyObjectChanges($bucket, $now, array $changes) {
function http_400 (line 79) | function http_400() {
FILE: lib/persistence.sync.server.php.sql
type `persistencejs_objects` (line 4) | CREATE TABLE `persistencejs_objects` (
FILE: test/appengine/test.js
function intFilterTests (line 45) | function intFilterTests(session, coll, callback) {
function textFilterTests (line 66) | function textFilterTests(session, coll, callback) {
function boolFilterTests (line 90) | function boolFilterTests(session, coll, callback) {
function dateFilterTests (line 113) | function dateFilterTests(session, coll, callback) {
function intOrderTests (line 141) | function intOrderTests(session, coll, callback) {
function dateOrderTests (line 162) | function dateOrderTests(session, coll, callback) {
function collectionLimitTests (line 191) | function collectionLimitTests(session, coll, callback) {
function collectionSkipTests (line 207) | function collectionSkipTests(session, coll, callback) {
FILE: test/browser/qunit/jquery.js
function ma (line 16) | function ma(){if(!c.isReady){try{s.documentElement.doScroll("left")}catc...
function Qa (line 16) | function Qa(a,b){b.src?c.ajax({url:b.src,async:false,dataType:"script"})...
function X (line 16) | function X(a,b,d,f,e,j){var i=a.length;if(typeof b==="object"){for(var o...
function J (line 17) | function J(){return(new Date).getTime()}
function Y (line 17) | function Y(){return false}
function Z (line 17) | function Z(){return true}
function na (line 17) | function na(a,b,d){d[0].type=a;return c.event.handle.apply(b,d)}
function oa (line 17) | function oa(a){var b,d=[],f=[],e=arguments,j,i,o,k,n,r;i=c.data(this,"ev...
function pa (line 18) | function pa(a,b){return"live."+(a&&a!=="*"?a+".":"")+b.replace(/\./g,"`"...
function qa (line 19) | function qa(a){return!a||!a.parentNode||a.parentNode.nodeType===11}
function ra (line 19) | function ra(a,b){var d=0;b.each(function(){if(this.nodeName===(a[d]&&a[d...
function sa (line 19) | function sa(a,b,d){var f,e,j;b=b&&b[0]?b[0].ownerDocument||b[0]:s;if(a.l...
function K (line 20) | function K(a,b){var d={};c.each(va.concat.apply([],va.slice(0,b)),functi...
function wa (line 20) | function wa(a){return"scrollTo"in a&&a.document?a:a.nodeType===9?a.defau...
function d (line 64) | function d(f){f=c.event.fix(f);f.type=b;return c.event.handle.call(this,f)}
function a (line 69) | function a(g){for(var h="",l,m=0;g[m];m++){l=g[m];if(l.nodeType===3||l.n...
function b (line 69) | function b(g,h,l,m,q,p){q=0;for(var v=m.length;q<v;q++){var t=m[q];
function d (line 70) | function d(g,h,l,m,q,p){q=0;for(var v=m.length;q<v;q++){var t=m[q];if(t)...
function f (line 108) | function f(u){return c.nodeName(u,"table")?u.getElementsByTagName("tbody...
function i (line 116) | function i(){e=b==="width"?a.offsetWidth:a.offsetHeight;f!=="border"&&c....
function b (line 123) | function b(){e.success&&
function d (line 124) | function d(){e.complete&&e.complete.call(k,x,i);e.global&&f("ajaxComplet...
function f (line 124) | function f(q,p){(e.context?c(e.context):c.event).trigger(q,p)}
function d (line 132) | function d(i,o){if(c.isArray(o))c.each(o,function(k,n){b||/\[\]$/.test(i...
function f (line 132) | function f(i,o){o=c.isFunction(o)?o():o;e[e.length]=encodeURIComponent(i...
function f (line 140) | function f(j){return e.step(j)}
FILE: test/browser/qunit/qunit.js
function done (line 505) | function done() {
function validTest (line 556) | function validTest( name ) {
function push (line 584) | function push(result, actual, expected, message) {
function synchronize (line 589) | function synchronize( callback ) {
function process (line 597) | function process() {
function saveGlobal (line 611) | function saveGlobal() {
function checkPollution (line 621) | function checkPollution( name ) {
function diff (line 639) | function diff( a, b ) {
function fail (line 653) | function fail(message, exception, callback) {
function extend (line 664) | function extend(a, b) {
function addEvent (line 672) | function addEvent(elem, type, fn) {
function id (line 682) | function id(name) {
function hoozit (line 699) | function hoozit(o) {
function bindCallbacks (line 747) | function bindCallbacks(o, callbacks, args) {
function useStrictEquality (line 761) | function useStrictEquality(b, a) {
function quote (line 912) | function quote( str ) {
function literal (line 915) | function literal( o ) {
function join (line 918) | function join( pre, arr, post ) {
function array (line 928) | function array( arr ) {
FILE: test/browser/tasks.client.js
function syncAll (line 12) | function syncAll() {
function addTask (line 20) | function addTask() {
FILE: test/browser/test.jquery-persistence.js
function intFilterTests (line 212) | function intFilterTests(coll, callback) {
function textFilterTests (line 236) | function textFilterTests(coll, callback) {
function boolFilterTests (line 263) | function boolFilterTests(coll, callback) {
function dateFilterTests (line 286) | function dateFilterTests(coll, callback) {
function intOrderTests (line 377) | function intOrderTests(coll, callback) {
function dateOrderTests (line 398) | function dateOrderTests(coll, callback) {
function collectionLimitTests (line 449) | function collectionLimitTests(coll, callback) {
function collectionSkipTests (line 465) | function collectionSkipTests(coll, callback) {
FILE: test/browser/test.migrations.js
function createMigrations (line 1) | function createMigrations(starting, amount, actions){
function addUsers (line 449) | function addUsers() {
function createAndRunMigration (line 453) | function createAndRunMigration() {
FILE: test/browser/test.persistence.js
function intFilterTests (line 291) | function intFilterTests(coll, callback) {
function textFilterTests (line 315) | function textFilterTests(coll, callback) {
function boolFilterTests (line 342) | function boolFilterTests(coll, callback) {
function dateFilterTests (line 365) | function dateFilterTests(coll, callback) {
function intOrderTests (line 456) | function intOrderTests(coll, callback) {
function textOrderTests (line 477) | function textOrderTests(coll, callback) {
function dateOrderTests (line 499) | function dateOrderTests(coll, callback) {
function collectionLimitTests (line 561) | function collectionLimitTests(coll, callback) {
function collectionSkipTests (line 577) | function collectionSkipTests(coll, callback) {
FILE: test/browser/test.sync.js
function noConflictsHandler (line 48) | function noConflictsHandler(conflicts, updatesToPush, callback) {
function resetResync (line 99) | function resetResync(callback) {
FILE: test/browser/test.uki-persistence.js
function intFilterTests (line 212) | function intFilterTests(coll, callback) {
function textFilterTests (line 236) | function textFilterTests(coll, callback) {
function boolFilterTests (line 263) | function boolFilterTests(coll, callback) {
function dateFilterTests (line 286) | function dateFilterTests(coll, callback) {
function intOrderTests (line 377) | function intOrderTests(coll, callback) {
function dateOrderTests (line 398) | function dateOrderTests(coll, callback) {
function collectionLimitTests (line 449) | function collectionLimitTests(coll, callback) {
function collectionSkipTests (line 465) | function collectionSkipTests(coll, callback) {
FILE: test/browser/util.js
function tableExists (line 1) | function tableExists(name, callback){
function checkNoTables (line 11) | function checkNoTables(callback) {
function tableNotExists (line 27) | function tableNotExists(name, callback){
function columnExists (line 37) | function columnExists(table, column, type, callback) {
function columnNotExists (line 50) | function columnNotExists(table, column, type, callback) {
function indexExists (line 62) | function indexExists(table, column, callback) {
function indexNotExists (line 72) | function indexNotExists(table, column, callback) {
FILE: test/node/node-blog.js
function log (line 43) | function log(o) {
function htmlHeader (line 63) | function htmlHeader(res, title) {
function htmlFooter (line 66) | function htmlFooter(res) {
function initDatabase (line 73) | function initDatabase(session, tx, req, res, callback) {
function resetDatabase (line 82) | function resetDatabase(session, tx, req, res, callback) {
function showItems (line 91) | function showItems(session, tx, req, res, callback) {
function showItem (line 113) | function showItem(session, tx, req, res, callback) {
function post (line 143) | function post(session, tx, req, res, callback) {
function postComment (line 156) | function postComment(session, tx, req, res, callback) {
FILE: test/node/partial.sync.schema.sql
type `_syncremovedobject` (line 8) | CREATE TABLE `_syncremovedobject` (
type `class` (line 33) | CREATE TABLE `class` (
type `student` (line 62) | CREATE TABLE `student` (
type `student_class` (line 89) | CREATE TABLE `student_class` (
type `teacher` (line 118) | CREATE TABLE `teacher` (
FILE: test/node/test.sqlite.store.js
function removeDb (line 21) | function removeDb() {
FILE: test/node/test.sqlite3.store.js
function removeDb (line 25) | function removeDb() {
FILE: test/node/test.sync.server.js
function log (line 45) | function log(o) {
function generateDummyData (line 93) | function generateDummyData(session) {
FILE: test/titanium/Resources/qunit/qunit.js
function done (line 568) | function done() {
function validTest (line 619) | function validTest( name ) {
function escapeHtml (line 647) | function escapeHtml(s) {
function push (line 661) | function push(result, actual, expected, message) {
function synchronize (line 679) | function synchronize( callback ) {
function process (line 687) | function process() {
function saveGlobal (line 701) | function saveGlobal() {
function checkPollution (line 711) | function checkPollution( name ) {
function diff (line 729) | function diff( a, b ) {
function fail (line 743) | function fail(message, exception, callback) {
function extend (line 754) | function extend(a, b) {
function addEvent (line 762) | function addEvent(elem, type, fn) {
function id (line 772) | function id(name) {
function bindCallbacks (line 788) | function bindCallbacks(o, callbacks, args) {
function useStrictEquality (line 802) | function useStrictEquality(b, a) {
function quote (line 953) | function quote( str ) {
function literal (line 956) | function literal( o ) {
function join (line 959) | function join( pre, arr, post ) {
function array (line 969) | function array( arr ) {
function getText (line 1115) | function getText( elems ) {
function diff (line 1149) | function diff(o, n){
Condensed preview — 78 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (603K chars).
[
{
"path": ".gitignore",
"chars": 20,
"preview": "test/titanium/build\n"
},
{
"path": "AUTHORS",
"chars": 541,
"preview": "# Authors ordered by first contribution.\n\nZef Hemel <zef@zef.me>\nFabio Rehm <fgrehm@gmail.com>\nLukas Berns\nRoberto Sacco"
},
{
"path": "CHANGES",
"chars": 1012,
"preview": "Changes\n=======\n\n* Moved all the SQL stuff into persistence.store.sql.js, and WebSQL to\n\tpersistence.store.websql.js. So"
},
{
"path": "README.md",
"chars": 28955,
"preview": "persistence.js\n==============\n`persistence.js` is a asynchronous Javascript object-relational\nmapper library. It can be "
},
{
"path": "bower.json",
"chars": 1102,
"preview": "{\n \"name\": \"persistence\",\n \"main\": \"./lib/persistence.js\",\n \"version\": \"0.3.0\",\n \"_release\": \"0.3.0\",\n \"_target\": \""
},
{
"path": "demo/jquerymobile/README.md",
"chars": 299,
"preview": "To try this demo, you need to run it through a web server.\n`index.html` uses relative links to persistence.js (in\n`../.."
},
{
"path": "demo/jquerymobile/docs/text.html",
"chars": 1512,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>Text only</title>\n</head>\n<body>\n\n<div data-role=\"page\">\n\n <div data-role=\"hea"
},
{
"path": "demo/jquerymobile/docs/text_and_images.html",
"chars": 1651,
"preview": "<!DOCTYPE html> \n<html> \n <head> \n <title>Text and image</title> \n</head> \n<body> \n\n<div data-role=\"page\">\n\n <div dat"
},
{
"path": "demo/jquerymobile/index.html",
"chars": 4341,
"preview": "<!DOCTYPE html>\n<html lang='en' >\n <head>\n <title>jQuery mobile / persistencejs integration</title>\n <meta http-e"
},
{
"path": "demo/jquerymobile/order/form-fake-response.html",
"chars": 387,
"preview": "<!DOCTYPE html> \n<html> \n <head> \n <title>Form submission</title> \n</head> \n<body> \n\n<div data-role=\"page\">\n\n <div da"
},
{
"path": "docs/DEVELOPMENT.md",
"chars": 1930,
"preview": "Documentation for developers\n============================\n\nConstructor functions\n---------------------\n\n var Task = p"
},
{
"path": "docs/jquery.md",
"chars": 572,
"preview": "# persistence.jquery.js\n\n`persistence.jquery.js` is a jquery plugin for `persistence.js` that\nallows the usage of jquery"
},
{
"path": "docs/jquery.mobile.md",
"chars": 2056,
"preview": "# persistence.jquery.mobile.js\n\n`persistence.jquery.mobile.js` is a plugin for `persistence.js` and [jQuery mobile](http"
},
{
"path": "docs/migrations.md",
"chars": 3461,
"preview": "# persistence.migrations.js\n\n`persistence.migrations.js` is a plugin for `persistence.js` that provides\na simple API for"
},
{
"path": "docs/search.md",
"chars": 1416,
"preview": "persistence.search.js\n==============\n`persistence.search.js` is a light-weight extension of the\n`persistence.js` library"
},
{
"path": "docs/sync.md",
"chars": 4972,
"preview": "persistence.sync.js\n===================\n\n`persystence.sync.js` is a `persistence.js` plug-in that adds data\nsynchronizat"
},
{
"path": "index.js",
"chars": 36,
"preview": "module.exports = require('./lib/');\n"
},
{
"path": "lib/index.js",
"chars": 123,
"preview": "module.exports = require('./persistence').persistence;\nmodule.exports.StoreConfig = require('./persistence.store.config'"
},
{
"path": "lib/persistence.jquery.js",
"chars": 3180,
"preview": "/**\n * Copyright (c) 2010 Roberto Saccon <rsaccon@gmail.com>\n *\n * Permission is hereby granted, free of charge, to any "
},
{
"path": "lib/persistence.jquery.mobile.js",
"chars": 12299,
"preview": "/**\n * Copyright (c) 2010 Roberto Saccon <rsaccon@gmail.com>\n *\n * Permission is hereby granted, free of charge, to any "
},
{
"path": "lib/persistence.js",
"chars": 82076,
"preview": "/**\n * Copyright (c) 2010 Zef Hemel <zef@zef.me>\n *\n * Permission is hereby granted, free of charge, to any person\n * ob"
},
{
"path": "lib/persistence.migrations.js",
"chars": 10424,
"preview": "/**\n * @license\n * Copyright (c) 2010 Fábio Rehm <fgrehm@gmail.com>\n * \n * Permission is hereby granted, free of charge,"
},
{
"path": "lib/persistence.pool.js",
"chars": 1080,
"preview": "var sys = require('sys');\nvar mysql = require('mysql');\n\nfunction log(o) {\n sys.print(sys.inspect(o) + \"\\n\");\n}\n\nfuncti"
},
{
"path": "lib/persistence.search.js",
"chars": 10429,
"preview": "/**\n * @license\n * Copyright (c) 2010 Zef Hemel <zef@zef.me>\n * \n * Permission is hereby granted, free of charge, to any"
},
{
"path": "lib/persistence.store.appengine.js",
"chars": 13076,
"preview": "var jdatastore = Packages.com.google.appengine.api.datastore,\n JDatastoreServiceFactory = jdatastore.DatastoreService"
},
{
"path": "lib/persistence.store.config.js",
"chars": 970,
"preview": "exports.init = function(persistence, config) {\n var persistenceStore;\n switch (config.adaptor) {\n case 'memory':\n "
},
{
"path": "lib/persistence.store.cordovasql.js",
"chars": 6414,
"preview": "try {\n if (!window) {\n window = {};\n //exports.console = console;\n }\n} catch (e) {\n window = {};\n exports.cons"
},
{
"path": "lib/persistence.store.memory.js",
"chars": 7586,
"preview": "try {\n if(!window) {\n window = {};\n //exports.console = console;\n }\n} catch(e) {\n window = {};\n exports.consol"
},
{
"path": "lib/persistence.store.mysql.js",
"chars": 4113,
"preview": "/**\n * This back-end depends on the node.js asynchronous MySQL driver as found on:\n * http://github.com/felixge/node-mys"
},
{
"path": "lib/persistence.store.react-native.js",
"chars": 3580,
"preview": "/**\n * This module depends on the react-native asynchronous SQLite3 driver as found on:\n * https://github.com/almost/rea"
},
{
"path": "lib/persistence.store.sql.js",
"chars": 31711,
"preview": "/**\n * Default type mapper. Override to support more types or type options.\n */\nvar defaultTypeMapper = {\n /**\n * SQL"
},
{
"path": "lib/persistence.store.sqlite.js",
"chars": 3287,
"preview": "/**\n * This back-end depends on the node.js asynchronous SQLite driver as found on:\n * https://github.com/orlandov/node-"
},
{
"path": "lib/persistence.store.sqlite3.js",
"chars": 3308,
"preview": "/**\n * This back-end depends on the node.js asynchronous SQLite3 driver as found on:\n * https://github.com/developmentse"
},
{
"path": "lib/persistence.store.titanium.js",
"chars": 5911,
"preview": "try {\n if(!window) {\n window = {};\n //exports.console = console;\n }\n} catch(e) {\n window = {};\n exports.consol"
},
{
"path": "lib/persistence.store.websql.js",
"chars": 6302,
"preview": "try {\n if(!window) {\n window = {};\n //exports.console = console;\n }\n} catch(e) {\n window = {};\n exports.consol"
},
{
"path": "lib/persistence.sync.js",
"chars": 14689,
"preview": "/**\n * @license\n * Copyright (c) 2010 Zef Hemel <zef@zef.me>\n * \n * Permission is hereby granted, free of charge, to any"
},
{
"path": "lib/persistence.sync.server.js",
"chars": 7484,
"preview": "/**\n * Copyright (c) 2010 Zef Hemel <zef@zef.me>\n * \n * Permission is hereby granted, free of charge, to any person\n * o"
},
{
"path": "lib/persistence.sync.server.php",
"chars": 4085,
"preview": "<?php\n\n/**\n * Copyright (c) 2010 Zef Hemel <zef@zef.me>\n *\n * Permission is hereby granted, free of charge, to any perso"
},
{
"path": "lib/persistence.sync.server.php.sql",
"chars": 465,
"preview": "-- This table must exist in the database for synchronization with the php version of the server to run.\n-- This is defin"
},
{
"path": "package.json",
"chars": 140,
"preview": "{\n \"name\": \"persistencejs\",\n \"version\": \"0.3.0\",\n \"engine\": \"node >=0.2.0\",\n \"author\": \"Zef Hemel\",\n \"directories\":"
},
{
"path": "test/appengine/test.js",
"chars": 13501,
"preview": "// Run with RingoJS: http://ringojs.org\n// Set path below to AppEngine Java SDK path\nvar appEngineSdkPath = '/Users/zef/"
},
{
"path": "test/browser/qunit/jquery.js",
"chars": 72174,
"preview": "/*!\n * jQuery JavaScript Library v1.4.2\n * http://jquery.com/\n *\n * Copyright 2010, John Resig\n * Dual licensed under th"
},
{
"path": "test/browser/qunit/qunit.css",
"chars": 2950,
"preview": "\nol#qunit-tests {\n\tfont-family:\"Helvetica Neue Light\", \"HelveticaNeue-Light\", \"Helvetica Neue\", Calibri, Helvetica, Aria"
},
{
"path": "test/browser/qunit/qunit.js",
"chars": 29115,
"preview": "/*\n * QUnit - A JavaScript Unit Testing Framework\n * \n * http://docs.jquery.com/QUnit\n *\n * Copyright (c) 2009 John Resi"
},
{
"path": "test/browser/tasks.client.js",
"chars": 569,
"preview": "\n// Data model\nvar Task = persistence.define('Task', {\n name: \"TEXT\",\n done: \"BOOL\",\n lastChange: \"DATE\"\n});\n\np"
},
{
"path": "test/browser/tasks.html",
"chars": 584,
"preview": "<html>\n <head>\n <script src=\"http://code.jquery.com/jquery-1.4.2.min.js\" type=\"application/x-javascript\" charset=\"ut"
},
{
"path": "test/browser/test.jquery-persistence.html",
"chars": 1234,
"preview": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \n \"http://www.w3.org/TR/html4/loose.dt"
},
{
"path": "test/browser/test.jquery-persistence.js",
"chars": 16811,
"preview": "$(document).ready(function(){\n persistence.store.websql.config(persistence, 'persistencetest', 'My db', 5 * 1024 * 1024"
},
{
"path": "test/browser/test.migrations.html",
"chars": 996,
"preview": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \n \"http://www.w3.org/TR/html4/loose.dt"
},
{
"path": "test/browser/test.migrations.js",
"chars": 10789,
"preview": "function createMigrations(starting, amount, actions){\n var amount = starting+amount;\n \n for (var i = starting; i < am"
},
{
"path": "test/browser/test.mixin.html",
"chars": 1061,
"preview": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \n \"http://www.w3.org/TR/html4/loose.dt"
},
{
"path": "test/browser/test.mixin.js",
"chars": 6049,
"preview": "$(document).ready(function(){\n persistence.store.websql.config(persistence, 'persistencetest', 'My db', 5 * 1024 * 1024"
},
{
"path": "test/browser/test.persistence.html",
"chars": 1066,
"preview": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \n \"http://www.w3.org/TR/html4/loose.dt"
},
{
"path": "test/browser/test.persistence.js",
"chars": 28254,
"preview": "$(document).ready(function(){\n persistence.store.websql.config(persistence, 'persistencetest', 'My db', 5 * 1024 * 1024"
},
{
"path": "test/browser/test.search.html",
"chars": 1057,
"preview": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \n \"http://www.w3.org/TR/html4/loose.dt"
},
{
"path": "test/browser/test.search.js",
"chars": 2146,
"preview": "$(document).ready(function(){\n persistence.store.websql.config(persistence, 'searchtest', 'My db', 5 * 1024 * 1024);\n "
},
{
"path": "test/browser/test.sync.html",
"chars": 1021,
"preview": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \n \"http://www.w3.org/TR/html4/loose.dt"
},
{
"path": "test/browser/test.sync.js",
"chars": 8933,
"preview": "$(document).ready(function(){\n persistence.store.websql.config(persistence, 'persistencetest', 'My db', 5 * 1024 * 1024"
},
{
"path": "test/browser/test.uki-persistence.html",
"chars": 1266,
"preview": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \n \"http://www.w3.org/TR/html4/loose.dt"
},
{
"path": "test/browser/test.uki-persistence.js",
"chars": 16329,
"preview": "$(document).ready(function(){\n persistence.store.websql.config(persistence, 'persistencetest', 'My db', 5 * 1024 * 1024"
},
{
"path": "test/browser/uki/uki-persistence.js",
"chars": 2533,
"preview": "// Original file at: http://github.com/rsaccon/uki/tree/master/src/uki-persistence/persistence.js\n\n\n/**\n * persistencejs"
},
{
"path": "test/browser/util.js",
"chars": 2885,
"preview": "function tableExists(name, callback){\n var sql = 'select name from sqlite_master where type = \"table\" and name == \"'+na"
},
{
"path": "test/node/node-blog.js",
"chars": 6500,
"preview": "/**\n * Copyright (c) 2010 Zef Hemel <zef@zef.me>\n * \n * Permission is hereby granted, free of charge, to any person\n * o"
},
{
"path": "test/node/partial.sync.schema.sql",
"chars": 6064,
"preview": "--\r\n-- Table structure for table `_syncremovedobject`\r\n--\r\n\r\nDROP TABLE IF EXISTS `_syncremovedobject`;\r\n/*!40101 SET @s"
},
{
"path": "test/node/test.error.handling.js",
"chars": 1915,
"preview": "// $ expresso -s test/test.error.handling.js\n\nvar assert = require('assert');\nvar persistence = require('../lib/persiste"
},
{
"path": "test/node/test.memory.store.js",
"chars": 1335,
"preview": "// $ expresso -s test.memory.store.js\n\nvar assert = require('assert');\nvar persistence = require('../../lib/persistence'"
},
{
"path": "test/node/test.sqlite.store.js",
"chars": 1569,
"preview": "// $ expresso -s test.sqlite.store.js\n\nvar assert = require('assert');\nvar persistence = require('../../lib/persistence'"
},
{
"path": "test/node/test.sqlite3.store.js",
"chars": 2174,
"preview": "// $ expresso -s test.sqlite3.store.js\n\nvar assert = require('assert');\nvar persistence = require('../../lib/persistence"
},
{
"path": "test/node/test.store.config.js",
"chars": 929,
"preview": "// $ expresso test.store.config.js\n\nvar assert = require('assert');\nvar persistence = require('../../lib/persistence').p"
},
{
"path": "test/node/test.sync.server.js",
"chars": 4882,
"preview": "/**\n * Copyright (c) 2010 Zef Hemel <zef@zef.me>\n * \n * Permission is hereby granted, free of charge, to any person\n * o"
},
{
"path": "test/titanium/.gitignore",
"chars": 4,
"preview": "tmp\n"
},
{
"path": "test/titanium/Resources/app.js",
"chars": 92,
"preview": "var win = Titanium.UI.createWindow({\n url:'runner.js',\n title: 'Unit Test'\n});\nwin.open();"
},
{
"path": "test/titanium/Resources/qunit/qunit.js",
"chars": 33182,
"preview": "/*\n * QUnit - A JavaScript Unit Testing Framework\n *\n * http://docs.jquery.com/QUnit\n *\n * Copyright (c) 2009 John Resig"
},
{
"path": "test/titanium/Resources/qunit/titanium_adaptor.js",
"chars": 2775,
"preview": "Titanium.include('qunit/qunit.js');\n\n// =============================================================================\n//"
},
{
"path": "test/titanium/Resources/runner.js",
"chars": 186,
"preview": "// This file needs to sit in the Resources directory so that when\n// it is used as a URL to a window, the include struct"
},
{
"path": "test/titanium/Resources/test/tests_to_run.js",
"chars": 747,
"preview": "//setting up stuff so that the environment kind of looks like a browser\nvar window = {};\nvar console = {\n log: function"
},
{
"path": "test/titanium/manifest",
"chars": 220,
"preview": "#appname: titanium\n#publisher: staugaard\n#url: https://github.com/zefhemel/persistencejs\n#image: appicon.png\n#appid: per"
},
{
"path": "test/titanium/tiapp.xml",
"chars": 1205,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ti:app xmlns:ti=\"http://ti.appcelerator.org\">\n<id>persistencejs.titanium.test</i"
}
]
About this extraction
This page contains the full source code of the zefhemel/persistencejs GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 78 files (558.7 KB), approximately 144.1k tokens, and a symbol index with 181 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.