├── .gitignore ├── engines ├── rhino │ ├── jars │ │ ├── perstore.jar │ │ ├── commons-dbcp-1.2.2.jar │ │ ├── commons-pool-1.5.2.jar │ │ └── mysql-connector-java-5.1.6-bin.jar │ └── store-engine │ │ ├── sql.js │ │ └── full-text.js └── node │ └── store-engine │ ├── mongodb-c.js │ └── sql.js ├── tests ├── data │ └── TestStore ├── store │ ├── perf │ │ ├── memory.js │ │ ├── mongodb.js │ │ └── base.js │ ├── all-stores.js │ ├── readonly-memory.js │ ├── js-file.js │ ├── cache.js │ ├── aggregate.js │ └── inherited.js ├── index.js ├── local.json ├── perf.js ├── facet.js ├── model.js └── query.js ├── store ├── converter.js ├── README.md ├── slow.js ├── mirrored.js ├── aggregate.js ├── map-index.js ├── cache.js ├── replicated.js ├── notifying.js ├── inherited.js ├── remote.js ├── filesystem.js ├── adaptive-index.js ├── couch-backup.js ├── memory.js ├── patched_sql.js ├── redis.js ├── sql.js ├── mongodb.js └── couchdb.js ├── stores.js ├── template.local.json ├── jsgi └── transactional.js ├── util ├── extend-error.js ├── settings.js ├── copy.js ├── es5-helper.js └── json-ext.js ├── coerce.js ├── errors.js ├── package.js ├── package.json ├── json-rpc.js ├── model.js ├── transaction.js ├── path.js └── facet.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /engines/rhino/jars/perstore.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kriszyp/perstore/master/engines/rhino/jars/perstore.jar -------------------------------------------------------------------------------- /tests/data/TestStore: -------------------------------------------------------------------------------- 1 | {1:{id:1, foo:2, rand:0.7013372050189607}, 2:{id:2, foo:1, bar:"hi"}, 3:{id:3, foo:1, bar:"hello"}} -------------------------------------------------------------------------------- /tests/store/perf/memory.js: -------------------------------------------------------------------------------- 1 | var testStore = require("../../../store/memory").Memory(); 2 | require("./base").testStore(testStore); -------------------------------------------------------------------------------- /engines/rhino/jars/commons-dbcp-1.2.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kriszyp/perstore/master/engines/rhino/jars/commons-dbcp-1.2.2.jar -------------------------------------------------------------------------------- /engines/rhino/jars/commons-pool-1.5.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kriszyp/perstore/master/engines/rhino/jars/commons-pool-1.5.2.jar -------------------------------------------------------------------------------- /engines/rhino/jars/mysql-connector-java-5.1.6-bin.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kriszyp/perstore/master/engines/rhino/jars/mysql-connector-java-5.1.6-bin.jar -------------------------------------------------------------------------------- /store/converter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Conversion utilities for converting JS types to something that can be handled by simpler data stores 3 | */ 4 | 5 | function Converter(){ 6 | } -------------------------------------------------------------------------------- /tests/store/perf/mongodb.js: -------------------------------------------------------------------------------- 1 | var testStore = require("../../../store/mongodb").MongoDB({collection:"Test"}); 2 | testStore.ready().then(function(){ 3 | require("./base").testStore(testStore); 4 | }); -------------------------------------------------------------------------------- /stores.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This provides utilities for stores 3 | */ 4 | 5 | exports.DefaultStore = function(options){ 6 | return require("./store/replicated").Replicated(require("./store/memory").Persistent(options)); 7 | }; 8 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | exports.testPersistence = require("./model"); 2 | exports.testFacet = require("./facet"); 3 | exports.testStores = require("./store/all-stores"); 4 | 5 | if (require.main === module) 6 | require("patr/runner").run(exports); 7 | 8 | -------------------------------------------------------------------------------- /tests/store/all-stores.js: -------------------------------------------------------------------------------- 1 | exports.testReadonlyMemory = require("./readonly-memory"); 2 | exports.testInherited = require("./inherited"); 3 | exports.testInherited = require("./cache"); 4 | 5 | if (require.main === module) 6 | require("patr/runner").run(exports); -------------------------------------------------------------------------------- /tests/store/readonly-memory.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"), 2 | testStore = require("../../store/memory").ReadOnly({ 3 | index: { 4 | 1: {id: 1, foo: 2} 5 | } 6 | }); 7 | exports.testGet = function(){ 8 | assert.equal(testStore.get(1).foo, 2); 9 | }; 10 | if (require.main === module) 11 | require("patr/runner").run(exports); -------------------------------------------------------------------------------- /tests/store/js-file.js: -------------------------------------------------------------------------------- 1 | var testStore = require("../../store/js-file").JSFile("data/TestStore"), 2 | CreateQueryTests = require("../query").CreateQueryTests; 3 | 4 | var tests = CreateQueryTests(testStore); 5 | for(var i in tests){ 6 | exports[i] = tests[i]; 7 | } 8 | 9 | if (require.main === module) 10 | require("patr/runner").run(exports); -------------------------------------------------------------------------------- /store/README.md: -------------------------------------------------------------------------------- 1 | This folder contains various data stores and store wrappers that implement 2 | the [WebSimpleDB](http://www.w3.org/TR/WebSimpleDB/) API. These data 3 | stores form the underlying storage for the Pintura framework. Data stores provide 4 | facilities for accessing, querying, and storing data. Data store wrappers can augment 5 | other stores with extra functionality. -------------------------------------------------------------------------------- /tests/local.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "connection":"jdbc:mysql://localhost/wiki?user=root&password=&useUnicode=true&characterEncoding=utf-8&autoReconnect=true", 4 | "type": "mysql", 5 | "host": "localhost", 6 | "port": 27017, 7 | "name": "wiki" 8 | }, 9 | "mail": { 10 | "host":"mail.site.com", 11 | "defaultFrom": "app@site.com" 12 | }, 13 | "bypassSecurity": true, 14 | "admins":["admin"], 15 | "dataFolder": "data" 16 | } 17 | -------------------------------------------------------------------------------- /template.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "connection":"jdbc:mysql://localhost/wiki?user=root&password=&useUnicode=true&characterEncoding=utf-8&autoReconnect=true", 4 | "type": "mysql", 5 | "host": "localhost", 6 | "port": 27017, 7 | "name": "wiki" 8 | }, 9 | "mail": { 10 | "host":"mail.site.com", 11 | "defaultFrom": "app@site.com" 12 | }, 13 | "bypassSecurity": true, 14 | "admins":["admin"], 15 | "dataFolder": "data" 16 | } 17 | -------------------------------------------------------------------------------- /tests/perf.js: -------------------------------------------------------------------------------- 1 | var Q = require("promised-io/promise"); 2 | var queue = require("event-loop-engine"); 3 | var i = 0,j = 0, count = 10000; 4 | var startTime = new Date().getTime(); 5 | for(var i = 0; i < count;i++){ 6 | d = Q.defer(); 7 | Q.when(d.promise, function(){ 8 | j++; 9 | if(j==count){ 10 | print("finished " + (new Date().getTime() - startTime)); 11 | } 12 | }); 13 | d.resolve(3); 14 | } 15 | queue.enterEventLoop(function(){ 16 | queue.shutdown(); 17 | }); -------------------------------------------------------------------------------- /tests/store/perf/base.js: -------------------------------------------------------------------------------- 1 | var store; 2 | var testQuery = require("rql/parser").parse("foo=hi"); 3 | exports.tests = { 4 | iterations: 1000, 5 | testPut: function(){ 6 | store.put({id:1,foo:3}); 7 | }, 8 | testGet: function(){ 9 | store.get(1); 10 | }, 11 | testQuery: function(){ 12 | store.query(testQuery); 13 | } 14 | }; 15 | exports.testStore = function(storeToTest, args){ 16 | store = storeToTest; 17 | require("patr/runner").run(exports.tests, args); 18 | }; -------------------------------------------------------------------------------- /tests/store/cache.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"), 2 | baseStore = require("../../store/memory").Memory(), 3 | cachingStore = require("../../store/memory").Memory(), 4 | cachedStore = require("../../store/cache").Cache(baseStore, cachingStore); 5 | 6 | cachedStore.put({name:"Instance of cached store"}); 7 | exports.testCached = function(){ 8 | assert.equal(cachedStore.query("", {}).length, 1); 9 | }; 10 | exports.testBase = function(){ 11 | assert.equal(baseStore.query("", {}).length, 1); 12 | }; 13 | if (require.main === module) 14 | require("patr/runner").run(exports); -------------------------------------------------------------------------------- /jsgi/transactional.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This executes the next app in a transaction, adding a transaction object 3 | * as the interface for accessing persistent and commiting the transaction 4 | * if successful, otherwise if an error is thrown, the transaction will be aborted 5 | */ 6 | exports.Transactional = Transactional; 7 | var transaction = require("../transaction").transaction; 8 | 9 | function Transactional(nextApp){ 10 | return function(request){ 11 | if(request.jsgi.multithreaded){ 12 | print("Warning: Running in a multithreaded environment may cause non-deterministic behavior"); 13 | } 14 | return transaction(function(){ 15 | return nextApp(request); 16 | }); 17 | }; 18 | } 19 | 20 | -------------------------------------------------------------------------------- /util/extend-error.js: -------------------------------------------------------------------------------- 1 | ({define:typeof define!="undefined"?define:function(factory){module.exports=factory(require)}}). 2 | define(function(require){ 3 | // Creates a custom error that extends JS's Error 4 | function ErrorConstructor(name, superError){ 5 | superError = superError || Error; 6 | function ExtendedError(message){ 7 | var e = new Error(message); 8 | e.name = name; 9 | var ee = Object.create(ExtendedError.prototype); 10 | ee.stack = e.stack; 11 | ee.message = e.message; 12 | return ee; 13 | } 14 | ExtendedError.prototype = Object.create(superError.prototype); 15 | ExtendedError.prototype.name = name; 16 | return ExtendedError; 17 | }; 18 | return ErrorConstructor.ErrorConstructor = ErrorConstructor; 19 | }); -------------------------------------------------------------------------------- /store/slow.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a wrapper store that makes all the operations slow to help debug and test asynchronicity 3 | */ 4 | require.def||(require.def=function(deps, factory){module.exports = factory.apply(this, deps.map(require));}); 5 | require.def(["promised-io/promise"],function(promise){ 6 | var defer = promise.defer; 7 | function Slow(store, delay){ 8 | ["get", "put", "query"].forEach(function(i){ 9 | var method = store[i]; 10 | store[i] = function(){ 11 | var results = method.apply(store, arguments); 12 | var deferred = defer(); 13 | setTimeout(function(){ 14 | deferred.resolve(results); 15 | },delay || 1000); 16 | return deferred.promise; 17 | } 18 | }); 19 | return store; 20 | }; 21 | Slow.Slow = Slow; 22 | return Slow; 23 | }); -------------------------------------------------------------------------------- /tests/store/aggregate.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"), 2 | storeA = require("../../store/memory").Memory(), 3 | storeB = require("../../store/memory").Memory(), 4 | combinedStore = require("../../store/aggregate").Aggregate( 5 | [storeA, storeB], 6 | [["foo"],["bar"]]); 7 | 8 | 9 | var id = combinedStore.put({foo:"foo",bar:"bar"}); 10 | exports.testWhole = function(){ 11 | assert.equal(combinedStore.get(id).foo, "foo"); 12 | assert.equal(combinedStore.get(id).bar, "bar"); 13 | }; 14 | exports.testParts = function(){ 15 | assert.equal(storeA.get(id).foo, "foo"); 16 | assert.equal(storeA.get(id).bar, undefined); 17 | assert.equal(storeB.get(id).foo, undefined); 18 | assert.equal(storeB.get(id).bar, "bar"); 19 | }; 20 | if (require.main === module) 21 | require("patr/runner").run(exports); -------------------------------------------------------------------------------- /coerce.js: -------------------------------------------------------------------------------- 1 | require("json-schema/lib/validate").coerce = function(instance, schema){ 2 | switch(schema.type){ 3 | case "string": 4 | instance = instance ? instance.toString() : ""; 5 | break; 6 | case "number": 7 | if(!isNaN(instance)){ 8 | instance = +instance; 9 | } 10 | break; 11 | case "boolean": 12 | instance = !!instance; 13 | break; 14 | case "null": 15 | instance = null; 16 | break; 17 | case "object": 18 | // can't really think of any sensible coercion to an object 19 | break; 20 | case "array": 21 | instance = instance instanceof Array ? instance : [instance]; 22 | break; 23 | case "date": 24 | var date = new Date(instance); 25 | if(!isNaN(date.getTime())){ 26 | instance = date; 27 | } 28 | break; 29 | } 30 | return instance; 31 | }; 32 | -------------------------------------------------------------------------------- /util/settings.js: -------------------------------------------------------------------------------- 1 | try{ 2 | var read = require("fs").readFileSync; 3 | }catch(e){ 4 | } 5 | if(!read){ 6 | read = require("fs").read; 7 | } 8 | if(!read){ 9 | read = require("file").read; 10 | } 11 | try{ 12 | var settings = JSON.parse(read("local.json").toString("utf8")); 13 | }catch(e){ 14 | try{ 15 | if(typeof java == "object" && typeof getResource == "function"){ 16 | settings = JSON.parse(getResource("local.json").content); 17 | }else{ 18 | settings = require("rc")("persvr",{ 19 | "processes": 1, 20 | "port": 8082, 21 | "repl": true, 22 | "replPort": 5555, 23 | "security":{ 24 | }, 25 | "dataFolder": "data" 26 | }); 27 | } 28 | }catch(e2){ 29 | console.error("A local.json file could not be found or parsed, and rc was not available to load configuration settings", e); 30 | } 31 | } 32 | for(var i in settings){ 33 | exports[i] = settings[i]; 34 | } 35 | -------------------------------------------------------------------------------- /errors.js: -------------------------------------------------------------------------------- 1 | var ErrorConstructor = require("./util/extend-error").ErrorConstructor; 2 | var AccessError = exports.AccessError = ErrorConstructor("AccessError"); 3 | 4 | var MethodNotAllowedError = exports.MethodNotAllowedError = ErrorConstructor("MethodNotAllowedError", AccessError); 5 | 6 | var DatabaseError = exports.DatabaseError = ErrorConstructor("DatabaseError"); 7 | 8 | var NotFoundError = exports.NotFoundError = ErrorConstructor("NotFoundError", DatabaseError); 9 | NotFoundError.prototype.code = 2; 10 | 11 | var PreconditionFailed = exports.PreconditionFailed = ErrorConstructor("PreconditionFailed", DatabaseError); 12 | PreconditionFailed.prototype.code = 3; 13 | 14 | var DuplicateEntryError = exports.DuplicateEntryError = ErrorConstructor("DuplicateEntryError", DatabaseError); 15 | DuplicateEntryError.prototype.code = 4; 16 | 17 | var AcceptError = exports.AcceptError = ErrorConstructor("AcceptError"); -------------------------------------------------------------------------------- /util/copy.js: -------------------------------------------------------------------------------- 1 | exports.deepCopy = function deepCopy(source, target, overwrite){ 2 | for(var i in source){ 3 | if(source.hasOwnProperty(i)){ 4 | if(typeof source[i] === "object" && typeof target[i] === "object"){ 5 | deepCopy(source[i], target[i], overwrite); 6 | } 7 | else if(overwrite || !target.hasOwnProperty(i)){ 8 | target[i] = source[i]; 9 | } 10 | } 11 | } 12 | return target; 13 | }; 14 | 15 | //TODO: should branch to using Object.keys if a native version is available. The 16 | // native version is slightly faster than doing a for-in loop (but a simulated version 17 | // wouldn't be) for rhino (but not v8). We could also have a branch for java-based copier that would 18 | // certainly be much faster 19 | exports.copy = function(source, target){ 20 | for(var i in source){ 21 | if(source.hasOwnProperty(i)){ 22 | target[i] = source[i]; 23 | } 24 | } 25 | return target; 26 | } 27 | -------------------------------------------------------------------------------- /tests/store/inherited.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"), 2 | baseStore = require("../../store/memory").Memory(), 3 | superSchema = {id:"A"}, 4 | superStore = require("../../store/inherited").Inherited(baseStore), 5 | subStore = require("../../store/inherited").Inherited(baseStore), 6 | superModel = require("../../model").Model(superStore,superSchema), 7 | subModel = require("../../model").Model(subStore, {id:"B","extends": superSchema}); 8 | 9 | 10 | superStore.put({name:"Instance of super store"}); 11 | subStore.put({name:"Instance of sub store"}); 12 | exports.testSub = function(){ 13 | assert.equal(subStore.query("", {}).length, 1); 14 | subStore.query("", {}).forEach(function(object){ 15 | assert.equal(object.name, "Instance of sub store"); 16 | }); 17 | }; 18 | exports.testSuper = function(){ 19 | assert.equal(superStore.query("", {}).length, 2); 20 | }; 21 | if (require.main === module) 22 | require("patr/runner").run(exports); -------------------------------------------------------------------------------- /tests/facet.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"), 2 | model = require("./model").model, 3 | CreateTests = require("./model").CreateTests, 4 | Restrictive = require("../facet").Restrictive, 5 | Permissive = require("../facet").Permissive; 6 | 7 | var permissiveFacet = Permissive(model, { 8 | extraStaticMethod: function(){ 9 | return 4; 10 | } 11 | }); 12 | var permissiveTests = CreateTests(permissiveFacet); 13 | for(var i in permissiveTests){ 14 | exports[i + "Permissive"] = permissiveTests[i]; 15 | } 16 | exports.testExtraStaticMethod = function(){ 17 | assert.equal(permissiveFacet.extraStaticMethod(), 4); 18 | } 19 | 20 | var restrictiveFacet = Restrictive(model); 21 | var restrictiveTests = CreateTests(restrictiveFacet); 22 | 23 | exports.testGetRestrictive = restrictiveTests.testGet; 24 | exports.testLoadRestrictive = restrictiveTests.testLoad; 25 | exports.testSaveRestrictive = shouldFail(restrictiveTests.testSave); 26 | exports.testMethodRestrictive = shouldFail(restrictiveTests.testMethod); 27 | exports.testStaticMethodRestrictive = shouldFail(restrictiveTests.testStaticMethod); 28 | 29 | function shouldFail(test){ 30 | return function(){ 31 | assert["throws"](test); 32 | }; 33 | }; 34 | 35 | if (require.main === module) 36 | require("patr/runner").run(exports); -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | exports.Package = function(name, store, schema, options){ 2 | if (!store) { 3 | options = schema; 4 | schema = store; 5 | store = {}; 6 | } 7 | schema = schema || {}; 8 | options = options || {}; 9 | var entityStores = {}; 10 | schema.get = function(storeName) { 11 | var entityStore = entityStores[storeName]; 12 | if(entityStore){ 13 | return entityStore; 14 | } 15 | }; 16 | if (!schema.SubPackage) { 17 | schema.SubPackage = function(name, subStore, subSchema) { 18 | subSchema = subSchema || {}; 19 | subSchema.parentStore = model; 20 | return entityStores[name] = exports.Package(name, subStore, subSchema, {nested: true}); 21 | }; 22 | } 23 | if (!schema.SubModel) { 24 | schema.SubModel = function(name, subStore, subSchema) { 25 | subSchema = subSchema || {}; 26 | subSchema.parentStore = model; 27 | return entityStores[name] = require("./model").SubModel(name, subStore, subSchema); 28 | }; 29 | } 30 | if (!options.nested) { 31 | var model = require("./model").Model(name, store, schema); 32 | } 33 | else { 34 | var model = require("./model").SubModel(name, store, schema); 35 | } 36 | return model; 37 | }; 38 | -------------------------------------------------------------------------------- /store/mirrored.js: -------------------------------------------------------------------------------- 1 | /** 2 | *Takes a set of stores and writes to all. 3 | */ 4 | exports.Mirrored = function(primary, replicas, options){ 5 | options = options || {}; 6 | var store = Object.create(primary); 7 | store.put = function(object, directives){ 8 | var id = primary.put(object, directives); 9 | replicas.forEach(function(replica){ 10 | replica.put(object, directives); // do a PUT because we require an exact replica 11 | }); 12 | return id; 13 | }; 14 | ["delete", "subscribe", "startTransaction", "commitTransaction", "abortTransaction"].forEach(function(methodName){ 15 | store[methodName] = options.replicateFirst ? 16 | function(){ 17 | var returned; 18 | replicas.forEach(function(replica){ 19 | returned = replica[methodName] && replica[methodName].apply(primary, arguments); 20 | }); 21 | primary[methodName] && primary[methodName].apply(primary, arguments); 22 | return returned; 23 | } : 24 | function(){ 25 | var returned = primary[methodName] && primary[methodName].apply(primary, arguments); 26 | replicas.forEach(function(replica){ 27 | replica[methodName] && replica[methodName].apply(primary, arguments); 28 | }); 29 | return returned; 30 | } 31 | }); 32 | replicas.forEach(function(replica){ 33 | if(replica.subscribe){ 34 | replica.subscribe("", function(action){ 35 | primary[action.event](action.body); 36 | }); 37 | } 38 | }); 39 | return store; 40 | }; -------------------------------------------------------------------------------- /engines/node/store-engine/mongodb-c.js: -------------------------------------------------------------------------------- 1 | /** 2 | * MongoDB data store. Depends on 3 | * http://github.com/orlandov/node-mongodb 4 | */ 5 | var mongodb = require("mongodb"); // requires mongodb from http://github.com/orlandov/node-mongodb 6 | // this will return a data store 7 | exports.MongoDB = function(options){ 8 | if(options.database){ 9 | var database = options.database; 10 | } 11 | else{ 12 | var database = new mongodb.MongoDB(); 13 | mongo.connect(options); 14 | } 15 | var idAttribute = options.idAttribute || "id"; 16 | var collection = database.getCollection(options.collection); 17 | function createIdSearch(id){ 18 | var search = {}; 19 | search[idAttribute] = id; 20 | return search; 21 | } 22 | return { 23 | get: function(id){ 24 | return collection.find(createIdSearch(id)).then(function(results){ 25 | return results[0]; 26 | }); 27 | }, 28 | put: function(object, id){ 29 | id = id || object[idAttribute]; 30 | var search = createIdSearch(id); 31 | return collection.count(search).then(function(count){ 32 | if(count === 0){ 33 | return collection.insert(object); 34 | } 35 | else{ 36 | return collection.update(search, object); 37 | } 38 | }); 39 | }, 40 | "delete": function(id){ 41 | collection.remove(createIdSearch(id)); 42 | }, 43 | query: function(query){ 44 | var search = {}; 45 | if(typeof query === "string"){ 46 | query = parseQuery(query); 47 | } 48 | query.forEach(function(term){ 49 | if(term.type == "comparison"){ 50 | search[term.name] = term.value; 51 | } 52 | // TODO: add support for alternate comparators, sorting, etc. 53 | }); 54 | } 55 | } 56 | }; -------------------------------------------------------------------------------- /engines/rhino/store-engine/sql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is an SQL database engine for Rhino 3 | * based on http://www.w3.org/TR/webdatabase/ 4 | * This relies on the jar file included with Perstore 5 | */ 6 | 7 | addToClasspath(module.resolve("../jars/commons-dbcp-1.2.2.jar")); 8 | addToClasspath(module.resolve("../jars/commons-pool-1.5.2.jar")); 9 | addToClasspath(module.resolve("../jars/perstore.jar")); 10 | var LazyArray = require("promised-io/lazy-array").LazyArray; 11 | var drivers = { 12 | mysql: "com.mysql.jdbc.Driver", 13 | sqlite: "org.sqlite.JDBC", 14 | derby: "org.apache.derby.jdbc.EmbeddedDriver", 15 | hsqldb: "org.hsqldb.jdbcDriver", 16 | oracle: "oracle.jdbc.driver.OracleDriver", 17 | postgres: "org.postgresql.Driver", 18 | mssql: "net.sourceforge.jtds.jdbc.Driver" 19 | } 20 | exports.SQLDatabase = function(parameters){ 21 | var adapter = new Packages.org.persvr.store.SQLStore(); 22 | if(drivers[parameters.type]){ 23 | parameters.driver = drivers[parameters.type]; 24 | } 25 | adapter.initParameters(parameters); 26 | return { 27 | executeSql: function(query, parameters, callback, errback){ 28 | // should roughly follow executeSql in http://www.w3.org/TR/webdatabase/ 29 | try{ 30 | var rawResults = adapter.executeSql(query, parameters); 31 | var results = {rows:LazyArray(rawResults)}; 32 | if(rawResults.insertId){ 33 | results.insertId = rawResults.insertId; 34 | } 35 | }catch(e){ 36 | return errback(e); 37 | } 38 | callback(results); 39 | }, 40 | transaction: function(){ 41 | adapter.startTransaction(); 42 | return { 43 | commit: function(){ 44 | adapter.commitTransaction(); 45 | }, 46 | abort: function(){ 47 | adapter.abortTransaction(); 48 | } 49 | }; 50 | } 51 | }; 52 | } 53 | 54 | -------------------------------------------------------------------------------- /tests/model.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"), 2 | store = require("../stores").DefaultStore("TestStore"), 3 | model = require("../model").Model(store, { 4 | prototype: { 5 | testMethod: function(){ 6 | return this.foo; 7 | } 8 | }, 9 | staticMethod: function(id){ 10 | return this.get(id); 11 | }, 12 | properties: { 13 | foo: { 14 | type: "number" 15 | }, 16 | bar: { 17 | optional: true, 18 | unique: true 19 | } 20 | }, 21 | links: [ 22 | { 23 | rel: "foo", 24 | href: "{foo}" 25 | } 26 | ] 27 | }); 28 | model.setPath("TestStore"); 29 | exports.model = model; 30 | exports.CreateTests = function(model){ 31 | return { 32 | testGet: function(){ 33 | assert.equal(model.get(1).foo, 2); 34 | }, 35 | 36 | testQuery: function(){ 37 | var count = 0; 38 | model.query("bar=hi").forEach(function(item){ 39 | assert.equal(item.bar, "hi"); 40 | count++; 41 | }); 42 | assert.equal(count, 1); 43 | }, 44 | 45 | testSave: function(){ 46 | var object = model.get(1); 47 | var newRand = Math.random(); 48 | object.rand = newRand; 49 | object.save(); 50 | object = model.get(1); 51 | assert.equal(object.rand, newRand); 52 | }, 53 | 54 | testSchemaEnforcement: function(){ 55 | var object = model.get(1); 56 | object.foo = "not a number"; 57 | assert["throws"](function(){ 58 | object.save(); 59 | }); 60 | }, 61 | 62 | testSchemaUnique: function(){ 63 | assert["throws"](function(){ 64 | model.put({foo:3, bar:"hi"}); 65 | }); 66 | }, 67 | 68 | testMethod: function(){ 69 | var object = model.get(1); 70 | assert.equal(model.get(1).testMethod(), 2); 71 | }, 72 | 73 | testStaticMethod: function(){ 74 | var object = model.staticMethod(1); 75 | assert.equal(object.id, 1); 76 | } 77 | }; 78 | }; 79 | var modelTests = exports.CreateTests(model); 80 | for(var i in modelTests){ 81 | exports[i] = modelTests[i]; 82 | } 83 | if (require.main === module) 84 | require("patr/runner").run(exports); -------------------------------------------------------------------------------- /store/aggregate.js: -------------------------------------------------------------------------------- 1 | /** 2 | *An aggregate store can combine multiple stores, where each store holds different parts 3 | * of the data for each object 4 | */ 5 | exports.Aggregate = function(stores, properties){ 6 | var store = { 7 | split: function(object){ 8 | // splits an object into different objects for each store 9 | var objects = []; 10 | for(var i = 0; i < stores.length; i++){ 11 | var propertyNames = properties[i]; 12 | if(propertyNames){ 13 | var part = {}; 14 | for(var j = 0; j < propertyNames.length; j++){ 15 | var propertyName = propertyNames[j]; 16 | if(propertyName in object){ 17 | part[propertyName] = object[propertyName]; 18 | } 19 | } 20 | objects.push(part); 21 | } 22 | else{ 23 | objects.push(object); 24 | } 25 | } 26 | return objects; 27 | }, 28 | 29 | combine: function(objects){ 30 | var combined= {}; 31 | objects.forEach(function(object){ 32 | for(var i in object){ 33 | combined[i] = object[i]; 34 | } 35 | }); 36 | return combined; 37 | }, 38 | get: function(id){ 39 | return this.combine(stores.map(function(store){ 40 | return store.get(id); 41 | })); 42 | }, 43 | query: function(query, options){ 44 | // default query just pulls from the first store (and just uses aggregation for gets) 45 | return stores[0].query(query, options); 46 | }, 47 | put: function(object, id){ 48 | var objects = this.split(object); 49 | for(var i = 0; i < objects.length; i++){ 50 | id = stores[i].put(objects[i], {id: id}) || id; 51 | } 52 | return id; 53 | } 54 | 55 | }; 56 | ["subscribe", "delete", "startTransaction", "commitTransaction", "abortTransaction"].forEach(function(methodName){ 57 | store[methodName] = function(){ 58 | var returned, args = arguments; 59 | stores.forEach(function(eachStore){ 60 | returned = eachStore[methodName] && eachStore[methodName].apply(eachStore, args) || returned; 61 | }); 62 | return returned; 63 | }; 64 | }); 65 | return store; 66 | }; -------------------------------------------------------------------------------- /store/map-index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a wrapper store that adds indexing through map functions 3 | */ 4 | var when = require("promised-io/promise").when; 5 | 6 | module.exports = function(store, options){ 7 | options = options || {}; 8 | var IndexConstructor = options.IndexConstructor || require("../stores").DefaultStore; 9 | store.deriveView = function deriveView(map){ 10 | var index = new IndexConstructor(); 11 | store.setPath(map.toString()); 12 | var getRevision = index.getRevision || function(){ 13 | return index.get("__revision__"); 14 | }; 15 | var getRevision = index.setRevision || function(revision){ 16 | return index.put("__revision__", revision); 17 | }; 18 | var getRevisions = derivedFrom.getRevisions || store.getRevisions || function(from){ 19 | return store.query("revisions(" + from + ")"); 20 | }; 21 | var revision; 22 | return { 23 | cursor: function(){ 24 | var storeRevision = store.getRevision(); 25 | // TODO: Might use a vclock type of strategy to determine if we really need to update 26 | if(storeRevision > getRevision()){ 27 | var transaction = index.transaction(true); 28 | // make sure we still need to update after getting the lock 29 | if(storeRevision > getRevision()){ 30 | getRevisions(getRevisition()).forEach(function(object){ 31 | var old = store.getPrevisionVersion(object); 32 | if(old){ 33 | map(old, function(key, value){ 34 | index.remove(key + '/' + old.id); 35 | }); 36 | } 37 | if(object){ 38 | if(maybeAlreadyApplied){ 39 | map(object, function(key, value){ 40 | index.remove(key + '/' + object.id); 41 | }); 42 | } 43 | map(object, function(key, value){ 44 | index.put(key + '/' + object.id, value); 45 | }); 46 | } 47 | }); 48 | } 49 | setRevision(storeRevision); 50 | transaction.commit(); 51 | } 52 | return index.cursor(); 53 | }, 54 | deriveView: function(){ 55 | 56 | } 57 | 58 | } 59 | }; 60 | return store; 61 | }; -------------------------------------------------------------------------------- /engines/rhino/store-engine/full-text.js: -------------------------------------------------------------------------------- 1 | var LazyArray = require("lazy-array").LazyArray; 2 | var FullText = exports.FullText = function(store, name){ 3 | searcher = new org.persvr.store.LuceneSearch("lucene/" + name); 4 | var defaultPut = store.put; 5 | store.put = function(object, id){ 6 | id = defaultPut.call(store, object, id); 7 | searcher.remove(id); 8 | searcher.create(id, object); 9 | return id; 10 | }; 11 | store.fulltext = function(query, field, options){ 12 | var idResults = LazyArray(searcher.query(query, field, options.start || 0, options.end || 100000000, null)); 13 | return { 14 | query: "?id.in(" + idResults.join(",") + ")", 15 | totalCount: idResults.totalCount 16 | }; 17 | /*return LazyArray({ 18 | some: function(callback){ 19 | idResults.some(function(id){ 20 | try{ 21 | callback(store.get(id)); 22 | } 23 | catch(e){ 24 | print(e.message); 25 | } 26 | }); 27 | }, 28 | totalCount: idResults.totalCount 29 | });*/ 30 | }; 31 | var defaultCommitTransaction = store.commitTransaction; 32 | store.commitTransaction = function(){ 33 | if(defaultCommitTransaction){ 34 | defaultCommitTransaction.call(store); 35 | } 36 | searcher.commitTransaction(); 37 | } 38 | var defaultDelete = store["delete"]; 39 | store["delete"] = function(id){ 40 | defaultDelete.call(store, id); 41 | searcher.remove(id); 42 | }; 43 | return store; 44 | }; 45 | 46 | var QueryRegExp = require("../json-query").QueryRegExp; 47 | 48 | var FullTextRegExp = exports.FullTextRegExp = QueryRegExp(/\?(.*&)?fulltext\($value\)(&.*)?/); 49 | exports.JsonQueryToFullTextSearch = function(tableName, indexedProperties){ 50 | return function(query, options){ 51 | var matches; 52 | query = decodeURIComponent(query); 53 | if((matches = query.match(FullTextRegExp))){ 54 | print(matches); 55 | var fulltext = eval(matches[2]); 56 | if(matches[1] || matches[3]){ 57 | (matches[1] || matches[3]).replace(QueryRegExp(/&?$prop=$value&?/g), function(t, prop, value){ 58 | fulltext += " AND " + prop + ":" + value; 59 | }); 60 | } 61 | return fulltext; 62 | } 63 | }; 64 | }; -------------------------------------------------------------------------------- /store/cache.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a wrapper store that can add caching to a store 3 | */ 4 | var when = require("promised-io/promise").when; 5 | 6 | exports.Cache = function(store, cacheStore, options){ 7 | options = options || {}; 8 | var defaultExpiresTime = options.defaultExpiresTime || 2000; 9 | var cacheWrites = "cacheWrites" in options ? options.cacheWrites : true; 10 | var cleanupInterval = options.cleanupInterval || 1000; 11 | var lastAccess = {}; 12 | var nextCheck = new Date().getTime(); 13 | var now = 0; 14 | cleanup(); 15 | function cleanup(){ 16 | now = new Date().getTime(); 17 | if(now > nextCheck){ 18 | nextCheck = now + cleanupInterval; 19 | return when(cacheStore.query("_expires<$1", {parameters:[now]}), function(results){ 20 | results.forEach(function(object){ 21 | cacheStore["delete"](object.id); 22 | }); 23 | }); 24 | } 25 | } 26 | return { 27 | get: function(id){ 28 | var cached = cacheStore.get(id); 29 | lastAccess[id] = now++; 30 | if(!cached){ 31 | if(store){ 32 | var cached = store.get(id); 33 | if(cached){ 34 | cacheStore.put(cached, {id:id}); 35 | } 36 | } 37 | } 38 | return cached; 39 | }, 40 | put: function(object, id){ 41 | cleanup(); 42 | if(!object._expires){ 43 | setExpires(object); 44 | } 45 | if(cacheWrites){ 46 | cacheStore.put(object, id); 47 | } 48 | if(store){ 49 | return store.put(object, id); 50 | } 51 | }, 52 | add: function(object, id){ 53 | cleanup(); 54 | if(!object._expires){ 55 | setExpires(object); 56 | } 57 | if(cacheWrites){ 58 | cacheStore.add(object, id); 59 | } 60 | if(store){ 61 | return store.add(object, id); 62 | } 63 | }, 64 | query: function(query, options){ 65 | return store.query(query, options); 66 | }, 67 | "delete": function(id){ 68 | cleanup(); 69 | if(store){ 70 | store["delete"](id); 71 | } 72 | cacheStore["delete"](id); 73 | } 74 | }; 75 | function setExpires(object){ 76 | Object.defineProperty(object, '_expires', { 77 | value: new Date().getTime() + defaultExpiresTime, 78 | enumerable: false 79 | }); 80 | Object.defineProperty(object, '_autoExpires', { 81 | value: true, 82 | enumerable: false 83 | }); 84 | } 85 | }; 86 | 87 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "perstore", 3 | "version": "0.3.3", 4 | "author": "Kris Zyp", 5 | "email": "kriszyp@gmail.com", 6 | "contributors": ["Vladimir Dronnikov "], 7 | "keywords": [ 8 | "persistence", 9 | "object", 10 | "store", 11 | "persevere" 12 | ], 13 | "maintainers": [ 14 | { 15 | "name": "Kris Zyp", 16 | "email": "kriszyp@gmail.com" 17 | } 18 | ], 19 | "engines": {"node":">=0.1.30", "rhino": true}, 20 | "mappings": { 21 | "tunguska": "http://github.com/kriszyp/tunguska/zipball/v0.2.2", 22 | "rql": "jar:http://github.com/kriszyp/rql/zipball/v0.2.2!/", 23 | "patr": "jar:http://github.com/kriszyp/patr/zipball/v0.2.2!/", 24 | "promised-io": "jar:http://github.com/kriszyp/promised-io/zipball/v0.2.2!/", 25 | "json-schema": "http://github.com/kriszyp/json-schema/zipball/v0.2.1", 26 | "mysql-native": "jar:http://github.com/sidorares/nodejs-mysql-native/zipball/master!/lib/mysql-native/", 27 | "mongodb": "jar:http://github.com/christkv/node-mongodb-native/zipball/V0.9.4.4!/lib/mongodb/", 28 | "redis": "jar:https://github.com/mranney/node_redis/zipball/master!/lib/redis/" 29 | }, 30 | "overlay": { 31 | "narwhal": { 32 | "mappings": { 33 | "fs-promise": "./engines/rhino/lib/fs-promise", 34 | "store-engine": "./engines/rhino/lib/store-engine/" 35 | } 36 | }, 37 | "node": { 38 | "mappings": { 39 | "store-engine": "./engines/node/lib/store-engine/" 40 | } 41 | } 42 | }, 43 | "usesSystemModules": ["path"], 44 | "licenses": [ 45 | { 46 | "type": "AFLv2.1", 47 | "url": "http://trac.dojotoolkit.org/browser/dojo/trunk/LICENSE#L43" 48 | }, 49 | { 50 | "type": "BSD", 51 | "url": "http://trac.dojotoolkit.org/browser/dojo/trunk/LICENSE#L13" 52 | } 53 | ], 54 | "repository": { 55 | "type":"git", 56 | "url":"http://github.com/kriszyp/tunguska" 57 | }, 58 | "directories": { 59 | "lib": "." 60 | }, 61 | "dependencies":{ 62 | "tunguska": ">=0.3.0", 63 | "rql": ">=0.3.1", 64 | "promised-io": ">=0.3.0", 65 | "json-schema": ">=0.2.1" 66 | }, 67 | "devDependencies": { 68 | "patr": ">0.2.6" 69 | }, 70 | "jars":["jars/perstore.jar"] 71 | } 72 | -------------------------------------------------------------------------------- /util/es5-helper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Various global cleanup operations 3 | */ 4 | // the problem with the narwhal es5 shim is that it throws when you set a property 5 | // that can't be handled by the VM. We want to be able to set enumerable: false 6 | // and other functions if possible, but not trigger errors 7 | exports.defineProperties = Object.defineProperties && Object.defineProperties.toString().match(/native code/) ? 8 | Object.defineProperties : 9 | function(target, props){ 10 | for(var i in props){ 11 | var def = props[i]; 12 | if(def.get){ 13 | target.__defineGetter__(i, def.get); 14 | } 15 | if(def.set){ 16 | target.__defineSetter__(i, def.set); 17 | } 18 | else if ("value" in def){ 19 | target[i] = def.value; 20 | } 21 | } 22 | 23 | }; 24 | exports.defineProperty = Object.defineProperty && Object.defineProperty.toString().match(/native code/) ? 25 | Object.defineProperty : 26 | function(target, property, def){ 27 | if(def.get){ 28 | target.__defineGetter__(property, def.get); 29 | } 30 | if(def.set){ 31 | target.__defineSetter__(property, def.set); 32 | } 33 | else if ("value" in def){ 34 | target[property] = def.value; 35 | } 36 | }; 37 | 38 | /*(function(){ 39 | var secureRandom = new java.security.SecureRandom(); 40 | Math.random = function(){ 41 | return secureRandom.nextDouble(); 42 | }; 43 | /* should be handled by Narwhal's global function 44 | if(!Object.defineProperty){ 45 | Object.defineProperty = function(target, property, def){ 46 | if(def.get || def.set){ 47 | target.__defineGetter__(property, def.get); 48 | target.__defineSetter__(property, def.set); 49 | } 50 | else if ("value" in def){ 51 | target[property] = def.value; 52 | } 53 | }; 54 | } 55 | if(!Object.defineProperties){ 56 | Object.defineProperties = function(target, props){ 57 | for(var i in props){ 58 | Object.defineProperty(target, i, props[i]); 59 | } 60 | }; 61 | } 62 | if(!Object.create){ 63 | Object.create = function(proto, properties){ 64 | // classic beget/delegate function (albiet with the function declared inside for thread safety) 65 | function temp(){} 66 | temp.prototype = proto; 67 | var instance = new temp; 68 | Object.defineProperties(instance, properties); 69 | return instance; 70 | } 71 | } 72 | })();*/ 73 | -------------------------------------------------------------------------------- /store/replicated.js: -------------------------------------------------------------------------------- 1 | /** 2 | *Replicates this store based on incoming data change messages from the pubsub hub. 3 | */ 4 | var Notifying = require("./notifying").Notifying, 5 | when = require("promised-io/promise").when, 6 | connector = require("tunguska/connector"); 7 | 8 | exports.Replicated = function(store, options){ 9 | var originalPut = store.put; 10 | var originalDelete = store["delete"]; 11 | var notifyingStore = Notifying(store); 12 | options = options || {}; 13 | var originalSetPath = store.setPath; 14 | notifyingStore.setPath = function(path){ 15 | if(originalSetPath){ 16 | originalSetPath.call(store, path); 17 | } 18 | var subscription = notifyingStore.subscribe("**", {"client-id": "local-store"}); 19 | when(subscription, function(){ 20 | if(store.getRevision){ 21 | // if the store supports indicating its revision, than we can try to query it's 22 | // replicas for revisions since the last time it was synced 23 | var revision = store.getRevision(); 24 | var revisionPath = path + "/?revisions(" + revision + ")"; 25 | connector.on("connection", function(connection){ 26 | // request the revisions since last sync 27 | connection.send({ 28 | method: "get", 29 | to: revisionPath 30 | }); 31 | var listener = connection.on("message", function(message){ 32 | if(message.from == revisionPath){ 33 | // got the response, don't need to listen anymore 34 | listener.dismiss(); 35 | // iterate through the results, making updates to our underlying store. 36 | message.result.forEach(function(revision){ 37 | if(revision.__deleted__){ 38 | store["delete"](revision.__deleted__); 39 | } 40 | else{ 41 | store.put(revision); 42 | } 43 | }); 44 | } 45 | }); 46 | }); 47 | } 48 | subscription.on("message" , function(message){ 49 | // listen for subscriptions to update our local store 50 | if(options.checkUpdate){ 51 | options.checkUpdate(message); 52 | } 53 | if(message.type == "put"){ 54 | return originalPut.call(store, message.result, {id: message.channel, replicated: true}); 55 | }else if(message.type == "delete"){ 56 | return originalDelete.call(store, message.channel, {replicated: true}); 57 | }else{ 58 | throw new Error("unexpected message type"); 59 | } 60 | }); 61 | }); 62 | }; 63 | return notifyingStore; 64 | }; -------------------------------------------------------------------------------- /tests/query.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"); 2 | 3 | exports.setupTest = function(store){ 4 | 5 | }; 6 | 7 | function assertConditionAndCount(array, condition, expectedCount){ 8 | var count = 0; 9 | array.forEach(function(item){ 10 | assert.ok(condition(item), condition.toString()); 11 | count++; 12 | }); 13 | assert.equal(count, expectedCount); 14 | } 15 | exports.CreateQueryTests = function(store){ 16 | return { 17 | testEqual : function(){ 18 | assertConditionAndCount(store.query("foo=2"), function(item){ 19 | return item.foo === 2; 20 | }, 1); 21 | }, 22 | testEqualString : function(){ 23 | assertConditionAndCount(store.query("bar=hi"), function(item){ 24 | return item.bar === "hi"; 25 | }, 1); 26 | }, 27 | testLessThan : function(){ 28 | assertConditionAndCount(store.query("foo=lt=2"), function(item){ 29 | return item.foo < 2; 30 | }, 2); 31 | }, 32 | testLessThanRaw : function(){ 33 | assertConditionAndCount(store.query("foo<2"), function(item){ 34 | return item.foo < 2; 35 | }, 2); 36 | }, 37 | testLessThanOrEqual : function(){ 38 | assertConditionAndCount(store.query("foo=le=2"), function(item){ 39 | return item.foo <= 2; 40 | }, 3); 41 | }, 42 | testGreaterThan : function(){ 43 | assertConditionAndCount(store.query("foo=gt=1"), function(item){ 44 | return item.foo > 1; 45 | }, 1); 46 | }, 47 | testGreaterThanRaw : function(){ 48 | assertConditionAndCount(store.query("foo>1"), function(item){ 49 | return item.foo > 1; 50 | }, 1); 51 | }, 52 | testGreaterThanOrEqual : function(){ 53 | assertConditionAndCount(store.query("foo=ge=1"), function(item){ 54 | return item.foo >= 1; 55 | }, 3); 56 | }, 57 | testAnd: function(){ 58 | assertConditionAndCount(store.query("foo=ge=1&foo=lt=2"), function(item){ 59 | return item.foo >= 1; 60 | }, 2); 61 | }, 62 | testOr: function(){ 63 | assertConditionAndCount(store.query("foo=1|foo=ge=2"), function(item){ 64 | return item.foo = 1 || this.foo >= 2; 65 | }, 3); 66 | }, 67 | testSortAsc: function(){ 68 | var lastFoo = 0; 69 | assertConditionAndCount(store.query("sort(+foo)"), function(item){ 70 | var valid = item.foo >= lastFoo; 71 | lastFoo = item.foo; 72 | return valid; 73 | }, 3); 74 | }, 75 | testSortDesc: function(){ 76 | var lastFoo = Infinity; 77 | assertConditionAndCount(store.query("sort(-foo)"), function(item){ 78 | var valid = item.foo <= lastFoo; 79 | lastFoo = item.foo; 80 | return valid; 81 | }, 3); 82 | } 83 | }; 84 | } 85 | if (require.main === module) 86 | require("patr/runner").run(exports); -------------------------------------------------------------------------------- /store/notifying.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This should wrap data stores that connect to a central repository, in order 3 | * to distribute data change notifications to all store subscribers. 4 | */ 5 | var getChildHub = require("tunguska/hub").getChildHub, 6 | when = require("promised-io/promise").when; 7 | 8 | exports.Notifying = function(store, options){ 9 | if(store.subscribe){ 10 | // already notifying 11 | return store; 12 | } 13 | var hub; 14 | var localHub; 15 | var originalSetPath = store.setPath; 16 | store.setPath = function(id){ 17 | hub = getChildHub(id); 18 | localHub = hub.fromClient("local-store"); 19 | if(originalSetPath){ 20 | originalSetPath(id); 21 | } 22 | }; 23 | store.subscribe = function(path, directives){ 24 | var clientHub = hub; 25 | if(directives && directives['client-id']){ 26 | clientHub = hub.fromClient(directives['client-id']); 27 | } 28 | return clientHub.subscribe(path, /*directives.body || */["put", "delete"]); 29 | }; 30 | store.unsubscribe = function(path, directives){ 31 | var clientHub = hub; 32 | if(directives && directives['client-id']){ 33 | clientHub = hub.fromClient(directives['client-id']); 34 | } 35 | return clientHub.unsubscribe(path, ["put", "delete"]); 36 | }; 37 | var originalPut = store.put; 38 | if(originalPut){ 39 | store.put= function(object, directives){ 40 | if(options && options.revisionProperty){ 41 | object[options.revisionProperty] = (object[options.revisionProperty] || 0) + 1; 42 | } 43 | var result = originalPut(object, directives) || object.id; 44 | if(directives && directives.replicated){ 45 | return result; 46 | } 47 | return when(result, function(id){ 48 | localHub.publish({ 49 | channel: id, 50 | result: object, 51 | type: "put" 52 | }); 53 | return id; 54 | }); 55 | }; 56 | } 57 | var originalAdd = store.add; 58 | if(originalAdd){ 59 | store.add= function(object, directives){ 60 | var result = originalAdd(object, directives) || object.id; 61 | if(directives && directives.replicated){ 62 | return result; 63 | } 64 | return when(result, function(id){ 65 | localHub.publish({ 66 | channel: id, 67 | result: object, 68 | type: "put" 69 | }); 70 | return id; 71 | }); 72 | }; 73 | } 74 | var originalDelete = store["delete"]; 75 | if(originalDelete){ 76 | store["delete"] = function(id, directives){ 77 | var result = originalDelete(id, directives); 78 | if(directives && directives.replicated){ 79 | return result; 80 | } 81 | return when(result, function(){ 82 | localHub.publish({ 83 | channel: id, 84 | type: "delete" 85 | }); 86 | }); 87 | }; 88 | } 89 | return store; 90 | }; 91 | -------------------------------------------------------------------------------- /store/inherited.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This store wrapper provides a means for creating a set of stores (from a single store) 3 | * that can inherit from each other with a superset/subset relation. One can use 4 | * schemas to indicate the hierarchy (with the "extends" property), and a property 5 | * is added to the instances to indicate what schema/model each instance belongs to. 6 | * See tests/inherited.js for an example. 7 | */ 8 | var getLink = require("json-schema/lib/validate").getLink, 9 | promise = require("promised-io/promise"), 10 | subSchemas = {}; 11 | exports.Inherited = function(store, schemaProperty){ 12 | // TODO: determine the schemaProperty from the schema's "schema" relation 13 | schemaProperty = schemaProperty || "__schema__"; 14 | var hierarchy = []; 15 | var id = promise.defer(); 16 | var inheritingStore = {}; 17 | for(var i in store){ 18 | inheritingStore[i] = store[i]; 19 | } 20 | var schema; 21 | var originalSetSchema = store.setSchema; 22 | inheritingStore.setSchema = function(newSchema){ 23 | schema = newSchema; 24 | originalSetSchema && originalSetSchema.call(store, schema); 25 | if(schema.id && !schema.id.then){ 26 | id.resolve(schema.id); 27 | id = schema.id; 28 | }else{ 29 | schema.id = id; 30 | } 31 | promise.when(id, function(id){ 32 | function addToHierarchy(superSchema){ 33 | promise.when(superSchema.id, function(superId){ 34 | var subs = subSchemas[superId]; 35 | if(!subs){ 36 | subs = subSchemas[superId] = []; 37 | } 38 | if(subs.indexOf(id) == -1){ 39 | subs.push(id); 40 | } 41 | superSchema = superSchema["extends"]; 42 | if(superSchema){ 43 | if(superSchema.instanceSchema){ 44 | superSchema = superSchema.instanceSchema; 45 | } 46 | if(superSchema instanceof Array){ 47 | // handle multiple inheritance 48 | superSchema.forEach(addToHierarchy); 49 | }else{ 50 | addToHierarchy(superSchema); 51 | } 52 | } 53 | }); 54 | } 55 | addToHierarchy(schema); 56 | }); 57 | }; 58 | var originalSetPath = store.setPath; 59 | inheritingStore.setPath = function(path){ 60 | originalSetPath && originalSetPath.call(store, path); 61 | if(id && id.then){ 62 | try{ 63 | id.resolve(path); 64 | id = path; 65 | }catch(e){ 66 | // squelch repeated resolve errors 67 | } 68 | } 69 | }; 70 | inheritingStore.query = function(query, directives){ 71 | query = query + "&in(" + encodeURIComponent(schemaProperty) + ",(" + subSchemas[id] + "))"; 72 | return store.query(query, directives); 73 | }; 74 | inheritingStore.put = function(object, directives){ 75 | object[schemaProperty] = id; 76 | return store.put(object, directives); 77 | }; 78 | if(store.add){ 79 | inheritingStore.add = function(object, directives){ 80 | object[schemaProperty] = id; 81 | return store.add(object, directives); 82 | }; 83 | } 84 | return inheritingStore; 85 | }; 86 | -------------------------------------------------------------------------------- /store/remote.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A remote client store that uses JSGI to retrieve data from remote sources 3 | */ 4 | 5 | ({define:typeof define!="undefined"?define:function(factory){module.exports=factory(require);}}). 6 | define(function(require){ 7 | var JSONExt = require("../util/json-ext"); 8 | var httpRequest = require("promised-io/http-client").request; 9 | var when = require("promised-io/promise").when; 10 | 11 | function Remote(request, contextUrl){ 12 | contextUrl = contextUrl || ""; 13 | request = request || httpRequest; 14 | var entityStores = {}; 15 | function remoteSubscribe(){ 16 | request({ 17 | method:"SUBSCRIBE", 18 | uri: options.query 19 | }).then(notification, notification, function(message){ 20 | remoteSubscribe(); 21 | notification(message); 22 | }); 23 | } 24 | //remoteSubscribe(); 25 | var listeners = []; 26 | function notification(message){ 27 | for(var i = 0;i < listeners.length; i++){ 28 | var listener = listeners[i]; 29 | try{ 30 | if(listener.query(message.target)){ 31 | listener.callback(message); 32 | } 33 | } 34 | catch(e){ 35 | onerror(e); 36 | } 37 | } 38 | } 39 | 40 | return { 41 | get: function(id){ 42 | // handle nested stores with nested paths 43 | var store = entityStores[storeName]; 44 | if(store){ 45 | return store; 46 | } 47 | store = entityStores[storeName] = Remote(function(req){ 48 | req.uri = storeName + '/' + req.uri; 49 | return request(req); 50 | }); 51 | // fulfill the role of an id provider as well 52 | store.then = function(callback, errback){ 53 | when(request({ 54 | method:"GET", 55 | pathInfo: '/' + id 56 | }), function(response){ 57 | try{ 58 | callback(JSONExt.parse(response.body.join(""))); 59 | }catch(e){ 60 | errback(e); 61 | } 62 | }, errback); 63 | }; 64 | }, 65 | put: function(object, id){ 66 | id = id || (object.getId ? object.getId() : object.id); 67 | var responsePromise= id ? 68 | request({ 69 | method: "PUT", 70 | pathInfo: '/' + id, 71 | body: JSONExt.stringify(object) 72 | }) : 73 | request({ 74 | method: "POST", 75 | pathInfo: contextUrl, 76 | body: JSONExt.stringify(object) 77 | }); 78 | return when(responsePromise, function(response){ 79 | return JSONExt.parse(response.body.join("")) 80 | }); 81 | }, 82 | query: function(query, options){ 83 | var headers = {}; 84 | if(options.start || options.end){ 85 | headers.range = "items=" + options.start + '-' + options.end; 86 | } 87 | query = query.replace(/\$[1-9]/g, function(t){ 88 | return JSONExt.stringify(options.parameters[t.substring(1) - 1]); 89 | }); 90 | return when(request({ 91 | method:"GET", 92 | queryString: query, 93 | headers: headers 94 | }), function(response){ 95 | return JSONExt.parse(response.body.join("")) 96 | }); 97 | }, 98 | "delete": function(id){ 99 | return request({ 100 | method:"DELETE", 101 | pathInfo: '/' + id 102 | }); 103 | }, 104 | subscribe: function(options){ 105 | listeners.push(options); 106 | } 107 | 108 | }; 109 | }; 110 | return Remote.Remote = Remote; 111 | }); 112 | -------------------------------------------------------------------------------- /json-rpc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TODO: Integrate this with tlrobinson's json-rpc project 3 | * Module for interacting with a WebWorker through JSON-RPC. 4 | * You can make a module accessible through JSON-RPC as easily as: 5 | * some-module.js: 6 | * require("./json-rpc-worker").server(exports); 7 | * 8 | * And to create this worker and fire it off: 9 | * var Worker = require("worker"), 10 | * client = require("./json-rpc-worker").client; 11 | * var workerInterface = client(new Worker("some-module")); 12 | * workerInterface.call("foo", [1, 3]).then(function(result){ 13 | * ... do something with the result ... 14 | * }); 15 | * 16 | */ 17 | var observe = require("promised-io/observe").observe, 18 | print = require("promised-io/process").print, 19 | when = require("promised-io/promise").when, 20 | currentMetadata; 21 | 22 | var invoke = exports.invoke = function(target, rpc, metadata){ 23 | currentMetadata = metadata; 24 | try{ 25 | return when(when(target, function(target){ 26 | var result; 27 | if(target[rpc.method] || !target.__noSuchMethod__){ 28 | return target[rpc.method].apply(target, rpc.params); 29 | } 30 | else{ 31 | return target.__noSuchMethod__.call(target, rpc.method, rpc.params); 32 | } 33 | }), function(result){ 34 | if(result instanceof Response){ 35 | result.body = { 36 | result: result.body, 37 | error: null, 38 | id: rpc.id 39 | } 40 | return result; 41 | } 42 | return { 43 | result: result, 44 | error: null, 45 | id: rpc.id 46 | }; 47 | }, error); 48 | } 49 | catch(e){ 50 | return error(e); 51 | } 52 | function error(e){ 53 | print(e.stack || (e.rhinoException && e.rhinoException.printStackTrace()) || (e.name + ": " + e.message)); 54 | return { 55 | result: null, 56 | error: e.message, 57 | id: rpc.id 58 | }; 59 | 60 | }; 61 | }; 62 | 63 | exports.server = function(rpcObject){ 64 | Titanium.API.debug("json-rpc exports.server global"); 65 | if (global.onmessage) // dedicated worker 66 | observe(global, "onmessage", handleMessage); 67 | else // shared worker 68 | observe(global, "onconnect", function (e) { e.port.onmessage = handleMessage; }); 69 | 70 | function handleMessage(event){ 71 | var data = event.data; 72 | if("id" in data && "method" in data && "params" in data){ 73 | postMessage(invoke(rpcObject, event.data)); 74 | } 75 | } 76 | }; 77 | 78 | var nextId = 1; 79 | exports.client = function(worker){ 80 | if(worker.port){ 81 | worker = worker.port; 82 | } 83 | var requestsWaiting = {}; 84 | observe(worker, "onmessage", function(event){ 85 | var data = event.data; 86 | if(requestsWaiting[data.id]){ 87 | if(data.error === null){ 88 | requestsWaiting[data.id].fulfill(data.result); 89 | } 90 | else{ 91 | requestsWaiting[data.id].error(data.error); 92 | } 93 | delete requestsWaiting[data.id]; 94 | } 95 | }); 96 | return { 97 | call: function(method, params){ 98 | var id = nextId++; 99 | 100 | worker.postMessage({ 101 | id: id, 102 | method: method, 103 | params: params 104 | }); 105 | promise = new Promise(); 106 | requestsWaiting[id] = promise; 107 | return promise; 108 | } 109 | }; 110 | }; 111 | try{ 112 | var Response = require("pintura/jsgi/response").Response; 113 | }catch(e){ 114 | Response = function(){}; 115 | } 116 | 117 | exports.getMetadata = function(){ 118 | return currentMetadata; 119 | }; 120 | -------------------------------------------------------------------------------- /engines/node/store-engine/sql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is an SQL database engine for Node 3 | * based on http://www.w3.org/TR/webdatabase/ 4 | * Currently only supports MySQL. 5 | */ 6 | 7 | var DatabaseError = require('perstore/errors').DatabaseError, 8 | DuplicateEntryError = require('perstore/errors').DuplicateEntryError; 9 | 10 | var engines = { 11 | mysql: MysqlWrapper 12 | }; 13 | 14 | exports.SQLDatabase = function(params) { 15 | if (params.type in engines) 16 | return engines[params.type](params); 17 | throw new DatabaseError("Unsupported database engine"); 18 | }; 19 | 20 | function MysqlWrapper(params) { 21 | var currentConnection; 22 | var x=0; 23 | 24 | // adapted from http://github.com/sidorares/nodejs-mysql-native/lib/mysql-native/websql.js 25 | return { 26 | executeSql: function(query, args, callback, errback) { 27 | var conn = currentConnection; 28 | if(!conn) { 29 | errback(new DatabaseError("No transactional context has been created")); 30 | return; 31 | } 32 | if (!conn.clean) { 33 | errback(new DatabaseError("Cannot commit a transaction with an error")); 34 | return; 35 | } 36 | var charset = require("mysql-native/lib/mysql-native/charset").Charset.by_name(conn.get("charset")); 37 | if(charset && charset.name=="utf8") conn.execute("SET NAMES utf8"); 38 | var cmd = conn.execute(query,args); 39 | 40 | // use result from callback 41 | cmd.on('result', function(result) { 42 | if (conn.clean && callback) { 43 | callback({ 44 | insertId: result.insert_id, 45 | rowsAffected: result.affected_rows, 46 | rows: result.rows 47 | }); 48 | } 49 | }); 50 | cmd.on('error', function(err) { 51 | conn.clean = false; 52 | if(errback) { 53 | var patt=/^duplicate entry/ig; 54 | if(err && patt.test(err.message)) { 55 | errback(new DuplicateEntryError(err.message)); 56 | } else { 57 | errback(err); 58 | } 59 | } 60 | }); 61 | }, 62 | transaction: function() { 63 | var conn = connectMysql(params); 64 | currentConnection = conn; 65 | throwOnError(conn.query('SET autocommit=0;'), 'disable autocommit'); 66 | throwOnError(conn.query('BEGIN'), 'initialize transaction'); 67 | 68 | return { 69 | commit: function() { 70 | throwOnError(conn.query("COMMIT"), 'commit SQL transaction'); 71 | throwOnError(conn.close(), 'close connection'); 72 | }, 73 | abort: function() { 74 | throwOnError(conn.query("ROLLBACK"), 'rollback SQL transaction'); 75 | throwOnError(conn.close(), 'close connection'); 76 | }, 77 | suspend: function(){ 78 | currentConnection = null; 79 | }, 80 | resume: function(){ 81 | currentConnection = conn; 82 | } 83 | }; 84 | } 85 | }; 86 | 87 | function throwOnError(cmd, action) { 88 | cmd.on('error', function(err) { 89 | console.log('Failed to ' + action + 90 | (err && err.message ? ': ' + err.message : '')); 91 | throw new DatabaseError('Failed to ' + action + 92 | (err && err.message ? ': ' + err.message : '')); 93 | }); 94 | } 95 | 96 | function connectMysql(params) { 97 | var ret = require("mysql-native/lib/mysql-native/client").createTCPClient(params.host, params.port); 98 | ret.auto_prepare = true; 99 | ret.row_as_hash = true; 100 | ret.clean = true; 101 | 102 | // use charset if available 103 | if(params.charset) ret.set("charset",params.charset); 104 | throwOnError(ret.connection, 'connect to DB'); 105 | throwOnError(ret.auth(params.name, params.username, params.password), 'authenticate'); 106 | 107 | return ret; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /model.js: -------------------------------------------------------------------------------- 1 | var Permissive = require("./facet").Permissive; 2 | 3 | var DefaultStore = require("./stores").DefaultStore, 4 | transaction = require("./transaction").transaction, 5 | NotFoundError = require("./errors").NotFoundError, 6 | defineProperty = require("./util/es5-helper").defineProperty, 7 | JSONExt = require("./util/json-ext"), 8 | fs = require("promised-io/fs"); 9 | 10 | var Model = function(store, schema) { 11 | if(typeof store == "string"){ 12 | throw new Error("Models should no longer be named, remove the name argument"); 13 | } 14 | if(!schema){ 15 | schema = store; 16 | store = null; 17 | } 18 | if(!store){ 19 | store = DefaultStore(); 20 | } 21 | if(typeof store.setSchema === "function"){ 22 | store.setSchema(schema); 23 | } 24 | if(typeof schema !== "function"){ 25 | schema = Permissive(store, schema); 26 | } 27 | defineProperty(schema, "transaction", { 28 | get: function(){ 29 | return require("./transaction").currentTransaction; 30 | } 31 | }); 32 | 33 | return schema; 34 | }; 35 | Model.Model = Model; 36 | Model.Store = function(store){ 37 | return Model(store, {});//(store.getSchema ? store.getSchema() : {}); 38 | } 39 | 40 | var modelPaths = {}; 41 | Model.initializeRoot = function(dataModel, addClass){ 42 | if(addClass){ 43 | dataModel.Class = {instanceSchema: Model.modelSchema}; 44 | dataModel.Class = Model.ModelsModel(dataModel); 45 | } 46 | modelPaths = {}; // reset model paths 47 | setPath(dataModel); 48 | dataModel.id = "root"; 49 | }; 50 | function setPath(model, path, name){ 51 | if (!model) return; 52 | modelPaths[path] = model; 53 | for(var key in model){ 54 | var target = model[key]; 55 | // FIXME would be nice to have a brand to detect Facet 56 | if(typeof target === "object" || target && target._baseFacetedStore){ 57 | var blacklist = [ 58 | "extends", 59 | "_baseFacetedStore", 60 | "instanceSchema" 61 | ]; 62 | if (blacklist.indexOf(key) >= 0) continue; 63 | setPath(target, path ? path + '/' + key : key, key); 64 | } 65 | } 66 | if(model.setPath){ 67 | model.setPath(path || "root"); 68 | } 69 | if(model.instanceSchema){ 70 | model.instanceSchema.id = name; 71 | } 72 | } 73 | 74 | Model.createModelsFromModel = function(sourceModel, models, constructor){ 75 | // this allows you to create a set of models from another source model. This makes 76 | // it easy to have a RESTful interface for creating new models 77 | constructor = constructor || Model; 78 | models = models || {}; 79 | sourceModel.query("").forEach(createSchema); 80 | if(sourceModel.subscribe){ 81 | sourceModel.subscribe("*").observe(function(events){ 82 | createSchema(events.result); 83 | }); 84 | } 85 | function createSchema(schema){ 86 | var name = schema.id; 87 | // TODO: get the path from the parent models 88 | setPath(models[name] = constructor(schema), name, name); 89 | } 90 | return models; 91 | } 92 | 93 | Model.modelSchema = { 94 | maxLimit: Infinity, 95 | id: "Class", 96 | properties:{ 97 | schemaLinks: "http://json-schema.org/links" 98 | } 99 | }; 100 | 101 | Model.ModelsModel = function(models){ 102 | var schemas = {}; 103 | for(var i in models){ 104 | schemas[i] = models[i].instanceSchema; 105 | if(typeof schemas[i] == "object"){ 106 | Object.defineProperty(schemas[i], "schema", { 107 | value: Model.modelSchema, 108 | enumerable: false 109 | }); 110 | } 111 | } 112 | var modelStore = require("./store/memory").Memory({index: schemas}); 113 | return Model.Model(modelStore, Model.modelSchema); 114 | }; 115 | /*var classStore = require("./store/memory").Memory({index: schemas}); 116 | classStore.put = function(object, directives){ 117 | fs.write("lib/model/" + object.id.toLowerCase() + ".js", 118 | 'var Model = require("perstore/model").Model;\n' + 119 | 'Model("' + object.id + '", ' + (directives.store || null) + ', ' + JSONExt.stringify(object) + ');'); 120 | var oldApp = fs.read("lib/app.js"); 121 | fs.write("lib/app.js", oldApp + '\nrequire("model/' + object.id + '");'); 122 | }; 123 | Model.classModel = Model.Model("Class", classStore, Model.classSchema); 124 | */ 125 | Model.getModelByPath = function(path) { 126 | return modelPaths[path]; 127 | }; 128 | module.exports = Model; -------------------------------------------------------------------------------- /store/filesystem.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A very simple filesystem based storage 3 | */ 4 | var fs = require("promised-io/fs"), 5 | MIME_TYPES = require("pintura/jsgi/mime").MIME_TYPES, 6 | when = require("promised-io/promise").when, 7 | AutoTransaction = require("../transaction").AutoTransaction; 8 | 9 | function BinaryFile(){ 10 | } 11 | 12 | var FileSystem = exports.FileSystem = function(options){ 13 | var fsRoot = require("../util/settings").dataFolder || "data" 14 | if(options.dataFolder){ 15 | fsRoot = options.dataFolder.charAt(0) == '/' ? options.dataFolder : fsRoot + '/' + options.dataFolder; 16 | } 17 | 18 | var store = AutoTransaction({ 19 | get: function(id, metadata){ 20 | //print("FileSystem get(): " + id); 21 | var filename,extraParts; 22 | var parts = getFilePathAndMetadata(id); 23 | extraParts = parts.extra; 24 | var fp= parts.file; 25 | try{ 26 | if (fs.statSync(filename).isFile()){ 27 | filename = fp; 28 | } 29 | }catch(e){ 30 | if (!options.defaultExtension) return; 31 | 32 | fp += options.defaultExtension ? ("."+options.defaultExtension):""; 33 | try { 34 | if (fs.statSync(fp).isFile()) { 35 | filename = fp; 36 | } 37 | }catch(e){ 38 | return; 39 | } 40 | } 41 | 42 | var extension = filename.match(/\.[^\.]+$/); 43 | var f = new BinaryFile(); 44 | 45 | f.forEach = function(callback){ 46 | var file = fs.open(filename, "br"); 47 | return file.forEach(callback); 48 | }; 49 | f.forEach.binary = true; 50 | 51 | f.getMetadata = function(){ 52 | return f; 53 | } 54 | var pathParts = filename.split("/") 55 | var fname = pathParts[pathParts.length-1]; 56 | Object.defineProperty(f,"alternates",{ 57 | value: [f] 58 | }); 59 | f.id = id; 60 | var explicitType = extraParts && extraParts[0]; 61 | var explicitDisposition = extraParts && extraParts[1]; 62 | if(!explicitDisposition && explicitType && explicitType.indexOf("/") == -1){ 63 | explicitDisposition = explicitType; 64 | explicitType = false; 65 | } 66 | f['content-type']= explicitType || MIME_TYPES[extension] ; 67 | 68 | f['content-disposition']= ((explicitDisposition && (explicitDisposition.charAt(0)=="a")) ? "attachment" : "inline") + "; filename=" + fname; 69 | f["content-length"]=fs.statSync(filename).size; 70 | 71 | return f; 72 | }, 73 | put: function(object, directives){ 74 | if(object.id){ 75 | return object.id; 76 | } 77 | var id = object.id = directives.id || generateId(object); 78 | var filename = getFilePathAndMetadata(id).file; 79 | return when(fs.stat(filename), 80 | function(){ 81 | if(directives.overwrite === false){ 82 | throw new Error("Can not overwrite existing file"); 83 | } 84 | return writeFile(); 85 | }, 86 | function(){ 87 | if(directives.overwrite === true){ 88 | throw new Error("No existing file to overwrite"); 89 | } 90 | return writeFile(); 91 | }); 92 | function writeFile(){ 93 | var path = object.path || object.tempfile, forEach = object.forEach; 94 | if(path || forEach){ 95 | store.addToTransactionQueue(function(){ 96 | if(object.__stored__){ 97 | return; 98 | } 99 | Object.defineProperty(object, "__stored__",{ 100 | value: true, 101 | enumerable: false 102 | }); 103 | fs.makeTree(filename.substring(0, filename.lastIndexOf("/"))); 104 | if(path){ 105 | return fs.move(path, filename); 106 | } 107 | var file = fs.open(filename, "wb"); 108 | return when(forEach.call(object, function(buffer){ 109 | file.write(buffer); 110 | }), function(){ 111 | file.close(); 112 | }); 113 | }); 114 | return id; 115 | } 116 | } 117 | }, 118 | "delete": function(id, directives){ 119 | var path = getFilePathAndMetadata(id).file; 120 | store.addToTransactionQueue(function(){ 121 | fs.remove(path); 122 | }); 123 | } 124 | }); 125 | return store; 126 | function getFilePathAndMetadata(id){ 127 | var extra = id.split("$"); 128 | var fp = id; 129 | if (extra[1]){ 130 | var extraParts = extra[1].split(","); 131 | fp= fsRoot + "/" + extra[0]; 132 | } 133 | 134 | var fp= [fsRoot,fp].join('/'); 135 | return { 136 | file:fp, 137 | extra: extraParts 138 | }; 139 | } 140 | 141 | } 142 | var REVERSE_MIME_TYPES = {}; 143 | for(var i in MIME_TYPES){ 144 | REVERSE_MIME_TYPES[MIME_TYPES[i]] = i; 145 | } 146 | exports.depth = 1; // depth of file directory paths to use 147 | 148 | function generateId(object){ 149 | var id = []; 150 | for(var i = 0; i < exports.depth; i++){ 151 | id.push(Math.random().toString().substring(2,6)); 152 | } 153 | var filename = object.name || Math.random().toString().substring(2); 154 | id.push(filename); 155 | id = id.join("/"); 156 | var extension = filename.match(/\.[^\.]+$/); 157 | var checkedAttachment; 158 | if(object.type && object.type !== MIME_TYPES[extension && extension[0]]){ 159 | if(object.name|| !REVERSE_MIME_TYPES[object["content-type"]]){ 160 | id += "$" + object["content-type"]; 161 | checkedAttachment = true; 162 | if(object["content-disposition"] == "attachment"){ 163 | id += ",attachment"; 164 | } 165 | }else{ 166 | id += REVERSE_MIME_TYPES[object["content-type"]]; 167 | } 168 | } 169 | if(!checkedAttachment && object["content-disposition"] == "attachment"){ 170 | id += "$attachment"; 171 | } 172 | return id; 173 | } 174 | -------------------------------------------------------------------------------- /transaction.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the transaction manager, for handling transactions across stores and databases 3 | */ 4 | var promiseModule = require("promised-io/promise"), 5 | when = promiseModule.when, 6 | NotFoundError = require("./errors").NotFoundError; 7 | 8 | var nextDatabaseId = 1; 9 | 10 | var defaultDatabase = { 11 | transaction: function(){ 12 | // these for independent stores, and the main transaction handler calls the commit and abort for us 13 | //TODO: Should this functionality be switched the main transaction handler? 14 | return { 15 | commit: function(){}, 16 | abort: function(){}, 17 | resume: function(){}, 18 | suspend: function(){} 19 | }; 20 | }, 21 | id:0 22 | }; 23 | exports.registerDatabase = function(database){ 24 | var previousDatabase = defaultDatabase; 25 | while(previousDatabase.nextDatabase){ 26 | previousDatabase = previousDatabase.nextDatabase 27 | } 28 | previousDatabase.nextDatabase = database; 29 | database.id = nextDatabaseId++; 30 | }; 31 | 32 | exports.transaction = function(callback){ 33 | 34 | var transactions = {}; 35 | 36 | var context = promiseModule.currentContext; 37 | if(!context){ 38 | context = promiseModule.currentContext = {}; 39 | } 40 | 41 | context.suspend = function(){ 42 | try{ 43 | for(var i in transactions){ 44 | if(transactions[i].suspend){ 45 | transactions[i].suspend(); 46 | } 47 | } 48 | for(var i in usedStores){ 49 | if(usedStores[i].suspend){ 50 | usedStores[i].suspend(); 51 | } 52 | } 53 | } 54 | finally{ 55 | exports.currentTransaction = null; 56 | } 57 | }; 58 | 59 | context.resume = function(){ 60 | exports.currentTransaction = transaction; 61 | for(var i in transactions){ 62 | if(transactions[i].resume){ 63 | transactions[i].resume(); 64 | } 65 | } 66 | for(var i in usedStores){ 67 | if(usedStores[i].resume){ 68 | usedStores[i].resume(); 69 | } 70 | } 71 | }; 72 | 73 | var throwing = true; 74 | function done(){ 75 | delete context.resume; 76 | delete context.suspend; 77 | exports.currentTransaction = null; 78 | } 79 | 80 | var database = defaultDatabase; 81 | do{ 82 | transactions[database.id] = database.transaction(); 83 | }while(database = database.nextDatabase); 84 | var transaction, usedStores = []; 85 | try{ 86 | var result = callback(transaction = exports.currentTransaction = { 87 | usedStores: usedStores, 88 | commit: function(){ 89 | try{ 90 | for(var i in transactions){ 91 | if(transactions[i].prepareCommit){ 92 | transactions[i].prepareCommit(); 93 | } 94 | } 95 | for(var i in usedStores){ 96 | if(usedStores[i].prepareCommit){ 97 | usedStores[i].prepareCommit(); 98 | } 99 | } 100 | for(var i in transactions){ 101 | transactions[i].commit(); 102 | } 103 | for(var i in usedStores){ 104 | if(usedStores[i].commit){ 105 | usedStores[i].commit(); 106 | } 107 | } 108 | var success = true; 109 | }finally{ 110 | if(!success){ 111 | this.abort(); 112 | }else{ 113 | done(); 114 | } 115 | } 116 | }, 117 | abort: function(){ 118 | try{ 119 | for(var i in transactions){ 120 | transactions[i].abort(); 121 | } 122 | for(var i in usedStores){ 123 | if(usedStores[i].abort){ 124 | usedStores[i].abort(); 125 | } 126 | } 127 | } 128 | finally{ 129 | done(); 130 | } 131 | }, 132 | }); 133 | throwing = false; 134 | return when(result, function(result){ 135 | transaction.commit(); 136 | return result; 137 | }, function(e){ 138 | transaction.abort(); 139 | return result; 140 | }); 141 | } 142 | finally{ 143 | if(throwing){ 144 | transaction.abort(); 145 | } 146 | } 147 | 148 | }; 149 | var nextStoreId = 0; 150 | 151 | exports.AutoTransaction = function(store, database){ 152 | database = database || defaultDatabase; 153 | 154 | //setup the store if it isn't already 155 | var prototype = { 156 | transaction: function(){ 157 | var queue = transactionQueue = []; 158 | return { 159 | commit: function() { 160 | store.commitTransactionQueue(queue); 161 | queue.length = 0; 162 | }, 163 | abort: function() { 164 | queue.length = 0; 165 | }, 166 | suspend: function(){ 167 | transactionQueue = null; 168 | }, 169 | resume: function(){ 170 | transactionQueue = queue; 171 | } 172 | }; 173 | }, 174 | addToTransactionQueue: function(action){ 175 | transactionQueue.push(action); 176 | }, 177 | commitTransactionQueue: function(queue){ 178 | queue.forEach(function(action){ 179 | action(); 180 | }); 181 | } 182 | }; 183 | for(var i in prototype){ 184 | if(!(i in store)){ 185 | store[i] = prototype[i]; 186 | } 187 | } 188 | 189 | //issue the call in a transaction if appropriate 190 | for(var i in store){ 191 | if(typeof store[i] === "function" && i != "setSchema" && i != "setPath" && !prototype.hasOwnProperty(i)){ 192 | (function(i, defaultMethod){ 193 | store[i] = function(){ 194 | if(!exports.currentTransaction){ 195 | var args = arguments; 196 | return exports.transaction(function(){ 197 | return startAndCall(args); 198 | }); 199 | } 200 | return startAndCall(arguments); 201 | }; 202 | function startAndCall(args){ 203 | if(!store.id){ 204 | store.id = "__auto__" + (nextStoreId++); 205 | } 206 | if(!exports.currentTransaction.usedStores[store.id] && store.transaction){ 207 | exports.currentTransaction.usedStores[store.id] = store.transaction(); 208 | } 209 | return defaultMethod.apply(store, args); 210 | } 211 | })(i, store[i]); 212 | } 213 | } 214 | var transactionQueue; 215 | 216 | return store; 217 | } 218 | -------------------------------------------------------------------------------- /store/adaptive-index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This will be a wrapper store to implement adaptive indexing 3 | */ 4 | var parseQuery = require("../resource-query").parseQuery, 5 | when = require("promised-io/promise").when, 6 | setInterval = require("browser/timeout").setInterval, 7 | settings = require("settings"); 8 | 9 | settings = settings || {}; 10 | settings = settings.adaptiveIndexing || {}; 11 | 12 | exports.AdaptiveIndex = function(store, options) { 13 | /** 14 | * In order to wrap a store with adaptive indexing the underlying store must 15 | * implement a setIndex function. If setIndex is not idempotent stores may 16 | * implement a getIndex function to prevent breaking existing indexes. 17 | * 18 | * For storage efficiency the adaptive indexer should be able to remove 19 | * indexes which go unused for extended periods. An underlying store can 20 | * implement a removeIndex function to facilite this. 21 | * 22 | * For each indexed property timestamps of when the index was first tracked 23 | * and when the index was last utilized are kept, as well as total accesses. 24 | * 25 | * These statistics are kept in memory for now, so every server restart will 26 | * flush them. A better approach would be to allow a store to be passed in 27 | * via the options object to keep the stats persistent. Stats need not be 28 | * flushed to the store on every query and could be buffered. 29 | */ 30 | 31 | if (typeof store.setIndex !== "function") 32 | throw new Error("Adaptive indexing requires store to implement a setIndex function"); // what kind of error? 33 | 34 | options = options || {}; 35 | 36 | // instantiates the statistics object where index accesses are tracked 37 | options.statistics = options.statistics || {}; 38 | 39 | // defines the default length of time an index can go unused before removal 40 | options.idlePeriod = options.idlePeriod || settings.idlePeriod || 604800000; 41 | 42 | // an expiration function can be provided which gets all the stats and options 43 | var expireIndexes = options.expireIndexes || function(store, options) { 44 | var stats = options.statistics; 45 | for (var i in stats) { 46 | if (new Date() - stats[i].lastAccess > options.idlePeriod) { 47 | when(store.removeIndex(i), 48 | function() { 49 | delete stats[i]; 50 | delete store.indexedProperties[i]; 51 | }, 52 | function(e) { 53 | print("Failed removing index for " + i + ": " + e); //TODO store.id? 54 | } 55 | ); 56 | } 57 | } 58 | } 59 | 60 | // defines the wait interval between running the method to expire indexes 61 | options.expirationInterval = options.expirationInterval || settings.expirationInterval || 3600000; 62 | if (typeof store.removeIndex === "function") { 63 | expireIndexes(store, options); 64 | setInterval(function() { 65 | expireIndexes(store, options); 66 | }, options.expirationInterval); 67 | } 68 | 69 | // reference the currently-defined store 70 | var wrapper = {}; 71 | for (var key in store) { 72 | wrapper[key] = store[key]; 73 | } 74 | 75 | // add a catchall in case the underlying store changes out from under us 76 | wrapper.__noSuchMethod__ = function(name, params) { 77 | return store[name].apply(store, params); 78 | } 79 | 80 | function updateStatistics(i) { 81 | var now = new Date(); 82 | var stats = options.statistics; 83 | if (stats[i]) { 84 | stats[i].lastAccess = now; 85 | stats[i].counter++; 86 | } 87 | else { 88 | var currentIndex = store.getIndex && store.getIndex(i) || null; 89 | when(currentIndex, function(response) { 90 | // don't try to create the index if it exists 91 | if (response) { 92 | // TODO confirm index is right, e.g. unique, collation? 93 | // index already exists, add to stats 94 | stats[i] = { 95 | created: now, 96 | lastAccess: now, 97 | counter: 1 98 | } 99 | store.indexedProperties[i] = "adaptive"; 100 | } 101 | else { 102 | // index may not exist, try to create it 103 | if (typeof store.setIndex !== "function") 104 | throw new Error("Adaptive indexing requires store to implement a setIndex function"); // what kind of error? 105 | when(store.setIndex(i), 106 | function() { 107 | stats[i] = { 108 | created: now, 109 | lastAccess: now, 110 | counter: 1 111 | } 112 | store.indexedProperties[i] = "adaptive"; 113 | }, 114 | function(e) { 115 | print("Failed setting index for " + i + ": " + e); //TODO store.id? 116 | } 117 | ); 118 | } 119 | }); 120 | } 121 | } 122 | 123 | wrapper.query = function(query, options) { 124 | if (typeof query === "string") query = parseQuery(query); 125 | var indexedProperties = store.indexedProperties; 126 | // for each query item log its usage, if it doesn't exist create it 127 | query.forEach(function(component) { 128 | if (component.type === "call" && component.name === "sort") { 129 | component.parameters.forEach(function(parameter) { 130 | if (parameter.charAt(0) === "+" || parameter.charAt(0) === "-") { 131 | parameter = parameter.substring(1); 132 | } 133 | if (!(parameter in store.indexedProperties)) 134 | updateStatistics(parameter); 135 | }); 136 | } 137 | else if (component.type === "comparison") { 138 | if (!(component.name in store.indexedProperties)) 139 | updateStatistics(component.name); 140 | } 141 | }); 142 | 143 | return store.query(query, options) 144 | } 145 | 146 | return wrapper; 147 | }; 148 | -------------------------------------------------------------------------------- /store/couch-backup.js: -------------------------------------------------------------------------------- 1 | /* 2 | * CouchDB store 3 | */ 4 | 5 | var request = require("jsgi-client").request, 6 | when = require("promised-io/promise").when, 7 | error = require("jsgi/error"); 8 | 9 | 10 | var decode = exports.decode = function(source) { 11 | var object = JSON.parse(source); // use JSONExt? 12 | object.id = object._id; // TODO use jsonschema's "self" 13 | delete object._id; 14 | delete object._rev; 15 | // TODO translate other _ properties? 16 | return object; 17 | }; 18 | 19 | var encode = exports.encode = function(object) { 20 | if (object.id) { 21 | object._id = object.id; 22 | delete object.id; 23 | } 24 | return JSON.stringify(object); 25 | }; 26 | 27 | 28 | exports.Server = function(config) { 29 | var server = {}; 30 | server.url = config.url; 31 | server.getConfig = function() { 32 | return when( 33 | request({ 34 | method: "GET", 35 | uri: url + "_config" 36 | }), 37 | function(response) { 38 | error.handle(response); 39 | return JSON.parse(response.body.join("")); 40 | } 41 | ); 42 | }; 43 | return server; 44 | }; 45 | 46 | // TODO get from settings 47 | var defaultServer = exports.Server({uri: "http://127.0.0.1:5984/"}); 48 | 49 | exports.Database = function(name, config) { 50 | config = config || {} 51 | var db = {}; 52 | db.server = config.server || defaultServer; 53 | db.url = db.server.url + name + "/"; 54 | 55 | db.get = function(id) { 56 | return when( 57 | request({ 58 | method: "GET", 59 | uri: db.url + id 60 | }), 61 | function(response) { 62 | error.handle(response); 63 | return decode(response.body.join("")); 64 | } 65 | ); 66 | }; 67 | /* TODO 68 | db.query = function(query, options) { 69 | var headers = {}; 70 | if(options.start || options.end){ 71 | headers.range = "items=" + options.start + '-' + options.end; 72 | } 73 | return when( 74 | request({ 75 | method: "GET", 76 | queryString: query, 77 | headers: headers 78 | }), 79 | function(response){ 80 | return decode(response.body.join("")) 81 | } 82 | ); 83 | }; 84 | */ 85 | db.put = function(object, id) { 86 | var etag = object.getMetadata().etag; 87 | if (etag) object._rev = etag; 88 | return when( 89 | request({ 90 | method: "PUT", 91 | uri: db.url + id, 92 | body: encode(object) 93 | }), 94 | function(response) { 95 | if (response.status != 201) throw new Error("PUT failed"); 96 | return decode(response.body.join("")); 97 | } 98 | ); 99 | }; 100 | 101 | db.post = function(object) { 102 | return when( 103 | request({ 104 | method: "POST", 105 | uri: db.url, 106 | body: encode(object) 107 | }), 108 | function(response) { 109 | if (response.status != 201) throw new Error("POST failed"); 110 | return decode(response.body.join("")); 111 | } 112 | ); 113 | }; 114 | 115 | db["delete"] = function(object) { 116 | return when( 117 | request({ 118 | method: "DELETE", 119 | uri: db.url + id, 120 | headers: {"if-match": object.getMetadata()["if-match"]} 121 | }), 122 | function(response) { 123 | // TODO try to get error messages from json response 124 | if (response.status != 200) throw new Error("DELETE failed"); 125 | var info = JSON.parse(response.body.join("")); 126 | return { 127 | getMetadata: function() { 128 | return {etag: info.rev}; 129 | } 130 | } 131 | } 132 | ); 133 | }; 134 | 135 | /* 136 | * CouchDB-specific API extensions 137 | */ 138 | 139 | db.copy = function(object) { 140 | var convertDestination = function(destination) { 141 | /* Convert parameterized Destination header into Couch ?rev= form 142 | * source: some_other_doc; etag=rev_id 143 | * target: some_other_doc?rev=rev_id 144 | */ 145 | var parsed = destination.split(";"); 146 | if (parsed.length > 1) { 147 | if (parsed[1].trim().toLowerCase().indexOf("etag=") === 0) { 148 | parsed[1] = "?rev=" + parsed[1].trim().substring(5); 149 | destination = parsed[0] + parsed.slice(1).join(";"); 150 | } 151 | } 152 | return destination; 153 | }; 154 | var headers = object.getMetadata(); 155 | headers.destination = convertDestination(headers.destination); 156 | return when( 157 | request({ 158 | method: "COPY", 159 | uri: db.url + id, 160 | headers: headers 161 | }), 162 | function(response) { 163 | if (response.status != 201) throw new Error("COPY failed"); 164 | return { 165 | getMetadata: function() { 166 | return {etag: response.headers.etag}; 167 | } 168 | } 169 | } 170 | ); 171 | } 172 | 173 | db.getDesigns = function() { 174 | return when( 175 | request({ 176 | method: "GET", 177 | uri: db.url + "_all_docs?_all_docs?startkey=%22_design%2F%22&endkey=%22_design0%22&include_docs=true" 178 | }), 179 | function(response) { 180 | error.handle(response); 181 | var view = JSON.parse(response.body.join("")); 182 | var docs = {}; 183 | view.rows && view.rows.forEach(function(row) { 184 | var key = row.id.split("/")[1]; // TODO confirm there can't be more than one slash 185 | delete row.doc._id; 186 | delete row.doc._rev; 187 | docs[key] = row.doc; 188 | }); 189 | print(docs.toSource()); 190 | return docs; 191 | } 192 | ); 193 | }; 194 | 195 | db.getDesign = function(name) { 196 | return when( 197 | db.get("_design/" + name), 198 | function(response) { 199 | response.name = response.id.split("/")[1]; 200 | delete response.id; 201 | return response; 202 | } 203 | ); 204 | }; 205 | 206 | // TODO get design, if not there or not up to date, put design 207 | //var designUrl = db.url + "_design/" + (config.design || "perstore") + "/"; 208 | 209 | return db; 210 | }; 211 | -------------------------------------------------------------------------------- /path.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module for looking up objects by path-based identifiers 3 | */ 4 | var all = require("promised-io/promise").all, 5 | when = require("promised-io/promise").when, 6 | Promise = require("promised-io/promise").Promise, 7 | LazyArray = require("promised-io/lazy-array").LazyArray, 8 | getLink = require("json-schema/lib/links").getLink; 9 | 10 | exports.resolver = function resolver(model, getDataModel){ 11 | // Creates a function for resolving ids that have slash-delimited paths, 12 | // and resolves any links in those paths 13 | // store: The name of the store to use to resolve ids 14 | // getDataModel: Optional parameter for resolving cross-model references 15 | 16 | var originalGet = model.get; 17 | 18 | return function(id, metadata){ 19 | var self = this; 20 | metadata = metadata || {}; 21 | id = '' + id; 22 | var schema = this; 23 | if(id.indexOf('/') > -1 && (id.indexOf('?') == -1 || id.indexOf('/') < id.indexOf('?'))){ 24 | var parts = id.split('/'); 25 | var value = originalGet.call(this, parts.shift()); 26 | parts.forEach(function(part){ 27 | value = when(value, function(value){ 28 | return resolveLink(schema, value, part, originalGet, getDataModel); 29 | }); 30 | }); 31 | return value; 32 | } 33 | if(id === '' || id.charAt(0) == "?"){ 34 | return model.query(id.substring(1), metadata); 35 | } 36 | if(id.match(/^\(.*\)$/)){ 37 | // handle paranthesis embedded, comma separated ids 38 | if(id.length == 2){ // empty array 39 | return []; 40 | } 41 | var parts = id.substring(1, id.length -1).split(','); 42 | return all(parts.map(function(part){ 43 | return originalGet.call(self, part, metadata); 44 | })); 45 | } 46 | return originalGet.call(this, id, metadata); 47 | }; 48 | }; 49 | 50 | exports.resolve = function(id, metadata, store, dataModel){ 51 | return resolver(store, dataModel)(id, metadata); 52 | } 53 | 54 | function resolveLink(model, obj, linkId, getFunction, getDataModel){ 55 | var id = (model.getId) ? model.getId(obj) : obj.id; 56 | id = ""+id; 57 | //console.log("resolving " + id + "/" + linkId); 58 | var value = obj; 59 | var linkTarget = getLink(linkId, value, model); 60 | if(!linkTarget){ 61 | value = value && value[linkId]; 62 | linkTarget = (value!==undefined) && value.$ref; 63 | } 64 | if(linkTarget){ 65 | if((linkTarget.charAt(0) == '/' || linkTarget.substring(0,3) == '../') && getDataModel){ 66 | value = getFromDataModel(getDataModel(), linkTarget.substring(linkTarget.charAt(0) == '/' ? 1 : 3)); 67 | }else{ 68 | value = getFunction.call(self, linkTarget); 69 | } 70 | } 71 | return value; 72 | 73 | } 74 | 75 | function getFromDataModel(dataModel, path){ 76 | var model = dataModel; 77 | var part; 78 | do{ 79 | var proceed = false; 80 | var slashIndex = path.indexOf("/"); 81 | if(slashIndex > -1){ 82 | part = path.substring(0, slashIndex); 83 | if(model[part]){ 84 | model = model[part]; 85 | path = path.substring(slashIndex + 1); 86 | proceed = true; 87 | } 88 | } 89 | }while(proceed); 90 | 91 | if(model._linkResolving){ 92 | return model.get(path); 93 | }else{ 94 | return exports.resolver(model, function(){; 95 | return dataModel; 96 | }).call(model, path); 97 | } 98 | } 99 | 100 | exports.LinkResolving = function(model, getDataModel){ 101 | //Model wrapper that uses schema links to incorporate 102 | //sub-objects or references into the object 103 | // model: the model object to wrap 104 | // getDataModel: Optional parameter for resolving cross-model references 105 | 106 | model.links = model.links || []; 107 | model._linkResolving = true; 108 | 109 | var originalGet = model.get; 110 | var resolvingGet = exports.resolver(model, getDataModel); 111 | 112 | var resolve = function(obj,metadata){ 113 | var self = this; 114 | var promises = model.links.filter(function(link){ 115 | return link.resolution !== undefined && (link.resolution == "eager" || link.resolution == "lazy"); 116 | }).map(function(link){ 117 | if(link.resolution == "eager"){ 118 | //put the resolved sub-object into the object 119 | var id = (model.getId) ? model.getId(obj) : obj.id; 120 | var value = resolveLink(model, obj, link.rel, originalGet, getDataModel); 121 | var promise = when(value, function(subObject){ 122 | if(subObject instanceof LazyArray){ 123 | //unpack the lazy array 124 | promise = when(subObject.toRealArray(), function(arr){ obj[link.rel]=arr;}); 125 | }else{ 126 | obj[link.rel] = subObject; 127 | } 128 | }); 129 | return promise; 130 | }else if(link.resolution == "lazy"){ 131 | //put a reference to the sub-object into the object 132 | var addLinkTask = new Promise(); 133 | obj[link.rel] = {"$ref": getLink(link.rel, obj, model)}; 134 | addLinkTask.callback(); 135 | return addLinkTask; 136 | } 137 | }); 138 | return when(all(promises), function(){return obj;}); 139 | } 140 | 141 | 142 | model.get = function(id, metadata){ 143 | id=""+id; 144 | var self = this; 145 | var rawResult = resolvingGet.call(this, id, metadata); 146 | if(id.indexOf("/")<0 && id.indexOf("?")<0){ 147 | return when(rawResult,function(rawResult){ 148 | return resolve.call(self, rawResult, metadata); 149 | }); 150 | } 151 | return rawResult; 152 | }; 153 | 154 | var originalQuery = model.query; 155 | 156 | model.query = function(query, metadata){ 157 | var self = this; 158 | var rawResult = originalQuery.call(this, query, metadata); 159 | if(model.links.some(function(link){ return link.resolution!==undefined; })){ 160 | return when(rawResult, function(rawResult){ 161 | var promises = rawResult.map(function(obj){ 162 | return resolve.call(self, obj, metadata); 163 | }); 164 | return when(all(promises), function(){ return rawResult; }); 165 | }); 166 | } 167 | return rawResult; 168 | } 169 | 170 | var originalPut = model.put; 171 | 172 | model.put = function(obj, directives){ 173 | var putObj = obj; 174 | var links = model.links.filter(function(link){ 175 | return link.resolution !== undefined && (link.resolution == "eager" || link.resolution == "lazy"); 176 | }); 177 | if(links.length >0){ 178 | putObj = {}; 179 | //clone the object 180 | for(key in obj){ 181 | if(obj.hasOwnProperty(key)){ 182 | putObj[key] = obj[key]; 183 | } 184 | } 185 | //remove the objects that were added by links 186 | links.forEach(function(link){ 187 | delete putObj[link.rel]; 188 | }); 189 | } 190 | originalPut.call(this, putObj, directives); 191 | } 192 | 193 | return model; 194 | } 195 | -------------------------------------------------------------------------------- /store/memory.js: -------------------------------------------------------------------------------- 1 | var executeQuery = require("rql/js-array").executeQuery, 2 | when = require("promised-io/promise").when, 3 | LazyArray = require("promised-io/lazy-array").LazyArray; 4 | 5 | function MemoryObject(){} 6 | MemoryObject.prototype = { 7 | getId: function(object){ 8 | return this.id; 9 | } 10 | } 11 | 12 | // ReadOnly memory store 13 | 14 | var ReadOnly = exports.ReadOnly = function(options){ 15 | return { 16 | index: (options && options.index) || {}, 17 | get: function(id){ 18 | var object = new MemoryObject; 19 | var current = this.index[id]; 20 | if(!current){ 21 | return; 22 | } 23 | for(var i in current){ 24 | if(current.hasOwnProperty(i)){ 25 | object[i] = current[i]; 26 | } 27 | } 28 | return object; 29 | }, 30 | query: function(query, directives){ 31 | directives = directives || {}; 32 | var all = []; 33 | for(var i in this.index){ 34 | all.push(this.index[i]); 35 | } 36 | all.log = options.log; 37 | if(directives.id){ 38 | query += "&id=" + encodeURIComponent(directives.id); 39 | } 40 | var result = executeQuery(query, directives, all); 41 | if(result instanceof Array){ 42 | // make a copy 43 | return LazyArray({ 44 | some: function(callback){ 45 | result.some(function(item){ 46 | if(item && typeof item === "object"){ 47 | var object = {}; 48 | for(var i in item){ 49 | if(item.hasOwnProperty(i)){ 50 | object[i] = item[i]; 51 | } 52 | } 53 | return callback(object); 54 | } 55 | return callback(item); 56 | }); 57 | }, 58 | length: result.length, 59 | totalCount: result.totalCount 60 | }); 61 | } 62 | return result; 63 | } 64 | }; 65 | }; 66 | 67 | 68 | // Memory store extends ReadOnly to add support for writes 69 | 70 | var PreconditionFailed = require("../errors").PreconditionFailed; 71 | var Memory = exports.Memory = function(options){ 72 | options = options || {}; 73 | var store = ReadOnly(options); 74 | var uniqueKeys = {}; 75 | var log = ("log" in options) ? options.log : (options.log = []); 76 | // start with the read-only memory store and add write support 77 | var put = store.put = function(object, directives){ 78 | directives = directives || {}; 79 | var id = object.id = "id" in object ? object.id : 80 | "id" in directives ? directives.id : Math.round(Math.random()*10000000000000); 81 | var isNew = !(id in store.index); 82 | if("overwrite" in directives){ 83 | if(directives.overwrite){ 84 | if(isNew){ 85 | throw new PreconditionFailed(id + " does not exist to overwrite"); 86 | } 87 | } 88 | else{ 89 | if(!isNew){ 90 | throw new PreconditionFailed(id + " exists, and can't be overwritten"); 91 | } 92 | } 93 | } 94 | updateIndexes.call(store, id, object); 95 | store.index[id] = object; 96 | return isNew && id; 97 | }; 98 | store.add = function(object, directives){ 99 | if(log){ 100 | log.push(object); 101 | } 102 | directives = directives || {}; 103 | directives.overwrite = false; 104 | put(object, directives); 105 | }; 106 | store["delete"] = function(id){ 107 | if(log){ 108 | log.push({__deleted__: id,id: id}); 109 | } 110 | updateIndexes.call(this, id); 111 | delete this.index[id]; 112 | }; 113 | store.setSchema = function(schema){ 114 | for(var i in schema.properties){ 115 | if(schema.properties[i].unique){ 116 | uniqueKeys[i] = true; 117 | store.indexes[i] = {}; 118 | } 119 | } 120 | }; 121 | store.indexes = {}; 122 | store.setIndex = function(index){ 123 | if(index instanceof Array){ 124 | if(log){ 125 | log.push.apply(log, index); 126 | } 127 | index.forEach(function(object){ 128 | if("__deleted__" in object){ 129 | delete store.index[object.__deleted__]; 130 | }else{ 131 | store.index[object.id] = object; 132 | } 133 | }); 134 | index.forEach(function(object){ 135 | updateIndexes.call(store, object.id, object); 136 | }) 137 | } 138 | else{ 139 | this.index = index; 140 | for(var id in index){ 141 | updateIndexes.call(this, id, index[id]); 142 | } 143 | } 144 | }; 145 | store.setIndex(store.index); 146 | if(log){ 147 | store.getRevision = function(){ 148 | return log.length; 149 | }; 150 | } 151 | return store; 152 | 153 | function updateIndexes(id, object){ 154 | var indexes = this.indexes; 155 | var current = this.index[id]; 156 | // update the indexes 157 | for(var i in indexes){ 158 | var index = indexes[i]; 159 | if(uniqueKeys.hasOwnProperty(i)){ 160 | if(current){ 161 | delete index[current[i]]; 162 | } 163 | if(object && object.hasOwnProperty(i)){ 164 | if(index.hasOwnProperty(object[i])){ 165 | throw new Error("Unique key constraint error duplicate " + JSON.stringify(object[i]) + " for key " + JSON.stringify(i)); 166 | } 167 | index[object[i]] = object; 168 | } 169 | } 170 | else{ 171 | // multi-valued indexes, each entry is an array 172 | if(current){ 173 | var forKey = index[current[i]]; 174 | if(forKey){ 175 | var position = forKey.indexOf(current); 176 | if(position > -1){ 177 | forKey.splice(position, 1); 178 | } 179 | } 180 | } 181 | if(object){ 182 | (index[object[i]] = index[object[i]] || []).push(object); 183 | } 184 | } 185 | } 186 | } 187 | } 188 | 189 | require("rql/js-array").operators.revisions = function(revision){ 190 | return this.log.slice(revision || 0, this.log.length); 191 | } 192 | // Persistent store extends Memory to persist writes to fs 193 | 194 | var JSONExt = require("../util/json-ext"), 195 | fs = require("promised-io/fs"), 196 | AutoTransaction = require("../transaction").AutoTransaction; 197 | 198 | var Persistent = exports.Persistent = function(options) { 199 | options = options || {}; 200 | var path = options.path || require("../util/settings").dataFolder || "data", 201 | store = Memory(options); 202 | if(options.filename){ 203 | initializeFile(options.filename); 204 | } 205 | function initializeFile(filename){ 206 | if(!filename){ 207 | throw new Error("A path/filename must be provided to the store"); 208 | } 209 | if(!writeStream){ 210 | if(filename.charAt(0) != '/'){ 211 | filename = path + '/' + filename; 212 | } 213 | writeStream = fs.openSync(filename, "a"); 214 | // set up a memory store and populate with line-separated json 215 | var buffer; 216 | try { 217 | buffer = fs.read(filename); 218 | } 219 | catch(e) {} 220 | if(buffer && buffer.trim() === "[") { 221 | } 222 | else if(buffer && buffer.length > 1){ 223 | if(buffer.charAt(0) == '{'){ 224 | buffer = '[' + buffer; 225 | } 226 | if(buffer.match(/,\r?\n$/)){ 227 | buffer = buffer.replace(/,\r?\n$/,']'); 228 | } 229 | try{ 230 | var data = eval(buffer); 231 | }catch(e){ 232 | e.message += " trying to parse " + filename; 233 | throw e; 234 | } 235 | // populate the store 236 | store.setIndex(data); 237 | if(options.log === false){ 238 | // rewrite the file if loging is disabled 239 | data = []; 240 | for(var i in store.index){ 241 | data.push(store.index[i]); 242 | } 243 | buffer = JSONExt.stringify(data); 244 | buffer = buffer.substring(0, buffer.length - 1) + ',\n'; 245 | writeStream.close(); 246 | writeStream = fs.openSync(filename, "w"); 247 | writeStream.write(buffer); 248 | } 249 | }else if(!buffer || buffer.length == 0){ 250 | writeStream.write("["); 251 | } 252 | } 253 | } 254 | 255 | var writeStream; 256 | store.setPath = function(path){ 257 | initializeFile(path); 258 | } 259 | var originalPut = store.put; 260 | store.put = function(object) { 261 | var result = originalPut.apply(store, arguments); 262 | store.addToTransactionQueue(JSONExt.stringify(object) + ",\n"); 263 | return result; 264 | } 265 | var originalAdd = store.add; 266 | store.add = function(object) { 267 | var result = originalAdd.apply(store, arguments); 268 | store.addToTransactionQueue(JSONExt.stringify(object) + ",\n"); 269 | return result; 270 | } 271 | var originalDelete = store["delete"]; 272 | store["delete"] = function(id) { 273 | var result = originalDelete.apply(store, arguments); 274 | store.addToTransactionQueue(JSONExt.stringify({__deleted__: id, id: id}) + ",\n"); 275 | return result; 276 | }; 277 | 278 | store.commitTransactionQueue = function(queue) { 279 | if(!writeStream){ 280 | throw new Error("Store was not initialized. Store's setPath should be called or it should be included as part of a data model package") 281 | } 282 | queue.forEach(function(block){ 283 | writeStream.write(block); 284 | }); 285 | if(writeStream.flush){ 286 | writeStream.flush(); 287 | } 288 | }; 289 | 290 | return AutoTransaction(store); 291 | }; 292 | -------------------------------------------------------------------------------- /store/patched_sql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is an SQL store that (partially) implements: 3 | * http://www.w3.org/TR/WebSimpleDB/ 4 | * and wraps an SQL database engine based on: 5 | * based on http://www.w3.org/TR/webdatabase/ 6 | */ 7 | var SQLDatabase = require("./sql-engine").SQLDatabase, 8 | first = require("lazy-array").first, 9 | AutoTransaction = require("../transaction").AutoTransaction, 10 | parseQuery = require("../resource-query").parseQuery, 11 | print = require("system").print, 12 | settings = require("../util/settings"), 13 | defineProperty = require("es5-helper").defineProperty; 14 | 15 | exports.SQLStore = function(config){ 16 | var database = config.database || exports.defaultDatabase(); 17 | var idColumn = config.idColumn || "id"; 18 | config.indexPrefix = config.indexPrefix || "idx_"; 19 | var store = { 20 | indexedProperties: {id: true}, 21 | selectColumns: ["*"], 22 | get: function(id){ 23 | var object = first(store.executeSql("SELECT * FROM " + config.table + " WHERE " + idColumn + "=?", [id]).rows); 24 | if(object){ 25 | defineProperty(object.__proto__ = { 26 | getId: function(object){ 27 | return this[idColumn]; 28 | } 29 | }, "getId", {enumerable: false}); 30 | } 31 | return object; 32 | }, 33 | "delete": function(id){ 34 | store.executeSql("DELETE FROM " + config.table + " WHERE " + idColumn + "=?", [id]); 35 | }, 36 | put: function(object, directives){ 37 | id = directives.id || object[config.idColumn]; 38 | var overwrite = directives.overwrite; 39 | if(overwrite === undefined){ 40 | overwrite = this.get(id); 41 | } 42 | var params = []; 43 | var valuesPlacement = ""; 44 | var columnsString = ""; 45 | if(!overwrite){ 46 | var first = true; 47 | for(var i in object){ 48 | if(object.hasOwnProperty(i)){ 49 | params.push(object[i]); 50 | valuesPlacement += first ? "?" : ",?"; 51 | columnsString += (first ? "" : ",") + i; 52 | first = false; 53 | } 54 | } 55 | params.idColumn = config.idColumn; 56 | var results = store.executeSql("INSERT INTO " + config.table + " (" + columnsString + ") values (" + valuesPlacement + ")", params); 57 | id = results.insertId; 58 | object[idColumn] = id; 59 | return id; 60 | } 61 | var sql = "UPDATE " + config.table + " SET "; 62 | var first = true; 63 | for(var i in object){ 64 | if(object.hasOwnProperty(i)){ 65 | if(first){ 66 | first = false; 67 | } 68 | else{ 69 | sql += ","; 70 | } 71 | sql += i + "=?"; 72 | params.push(object[i]); 73 | } 74 | } 75 | sql += " WHERE " + idColumn + "=?"; 76 | params.push(object[idColumn]); 77 | store.executeSql(sql, params); 78 | }, 79 | query: function(query, options){ 80 | options = options || {}; 81 | var selectColumns = this.selectColumns; 82 | var indexedProperties = this.indexedProperties; 83 | if(typeof query === "string"){ 84 | query = parseQuery(query); 85 | } 86 | var filter = this.getWhereClause(query, options); 87 | var order = []; 88 | query.forEach(function(term) { 89 | if (term.type == "call") { 90 | if(term.name == "sort") { 91 | if(term.parameters.length === 0) 92 | throw new URIError("Must specify a sort criteria"); 93 | term.parameters.forEach(function(sortAttribute) { 94 | var firstChar = sortAttribute.charAt(0); 95 | var orderDir = "ASC"; 96 | if(firstChar == "-" || firstChar == "+") { 97 | if(firstChar == "-") { 98 | orderDir = "DESC"; 99 | } 100 | sortAttribute = sortAttribute.substring(1); 101 | } 102 | if(!indexedProperties[sortAttribute]) { 103 | throw new URIError("Can only sort by " + Object.keys(indexedProperties)); 104 | } 105 | order.push(config.table + "." + sortAttribute + " " + orderDir); 106 | }); 107 | } 108 | } 109 | }); 110 | var slice = []; 111 | if (options && typeof options.start === "number") { 112 | slice.push(options.start); 113 | if (typeof options.end === "number") { 114 | slice.push(options.end); 115 | } 116 | } 117 | var selectObject = { 118 | select: selectColumns, 119 | from: config.table, 120 | where: filter, 121 | order: order, 122 | slice: slice, 123 | dialect: settings.database.type, 124 | toString: function(count) { 125 | var sql, 126 | start = this.slice[0], 127 | end = this.slice[1]; 128 | 129 | if (this.dialect == "mssql" && !count) { 130 | sql = "SELECT " + this.select; 131 | sql += " FROM (SELECT ROW_NUMBER() OVER (ORDER BY "; 132 | if (this.order.length) { 133 | sql += this.order.join(", "); 134 | } 135 | else { 136 | sql += this.from + "." + idColumn; 137 | } 138 | sql += ") AS __rownum__, " + this.select; 139 | sql += " FROM " + this.from; 140 | sql += " WHERE " + this.where; 141 | sql += ") AS " + this.from; 142 | if (start) 143 | sql += " WHERE __rownum__ >= " + start; 144 | if (end) 145 | sql += (start && " AND" || " WHERE") + " __rownum__ <= " + (end + 1); 146 | return sql; 147 | } 148 | 149 | sql = " FROM " + this.from; 150 | sql += " WHERE " + this.where; 151 | if (count) { 152 | return "SELECT COUNT(*) AS count" + sql; 153 | } 154 | 155 | sql = "SELECT " + this.select + sql; 156 | if (this.order.length) sql += " ORDER BY " + this.order.join(", "); 157 | if (end) sql += " LIMIT " + (options.end - options.start + 1); 158 | if (start) sql += " OFFSET " + options.start; 159 | return sql; 160 | } 161 | }; 162 | return this.executeQuery(selectObject, options); 163 | }, 164 | getWhereClause: function(query, options){ 165 | var sql = ""; 166 | if(!options){ 167 | throw new Error("Values must be set as parameters on the options argument, which was not provided"); 168 | } 169 | var indexedProperties = this.indexedProperties; 170 | var params = (options.parameters = options.parameters || []); 171 | query.forEach(function(term){ 172 | if(term.type == "comparison"){ 173 | addClause(term.name, config.table + '.' + term.name + term.comparator + "?"); 174 | params.push(term.value); 175 | } 176 | else if(term.type == "call") { 177 | if (term.name == "sort") { 178 | // handling somewhere else 179 | } 180 | else if (term.name instanceof Array && term.name[1] === "in") { 181 | var name = term.name[0]; 182 | if(term.parameters.length == 0){ 183 | // an empty IN clause is considered invalid SQL 184 | if(sql){ 185 | sql += term.logic == "&" ? " AND " : " OR "; 186 | } 187 | sql += "0=1"; 188 | } 189 | else{ 190 | addClause(name, name + " IN (" + term.parameters.map(function(param){ 191 | params.push(param); 192 | return "?"; 193 | }).join(",") + ")"); 194 | } 195 | } 196 | else{ 197 | throw new URIError("Invalid query syntax, " + term.method + " not implemented"); 198 | } 199 | } 200 | else{ 201 | throw new URIError("Invalid query syntax, unknown type"); 202 | } 203 | function addClause(name, sqlClause){ 204 | if(!indexedProperties[name]){ 205 | throw new URIError("Can only query by " + Object.keys(indexedProperties)); 206 | } 207 | if(sql){ 208 | sql += term.logic == "&" ? " AND " : " OR "; 209 | } 210 | sql += sqlClause; 211 | } 212 | }); 213 | return sql || "1=1"; 214 | }, 215 | executeQuery: function(selectObject, options) { 216 | // executes a query with provide start and end parameters, calculating the total number of rows 217 | if (selectObject.slice.length) { 218 | var results = this.executeSql(selectObject+"", options.parameters).rows; 219 | var lengthObject = first(this.executeSql(selectObject.toString(true), options.parameters).rows); 220 | results.totalCount = lengthObject.count; 221 | return results; 222 | } 223 | var results = this.executeSql(selectObject+"", options.parameters).rows; 224 | results.totalCount = results.length; 225 | return results; 226 | }, 227 | executeSql: function(sql, parameters){ 228 | return database.executeSql(sql, parameters); 229 | }, 230 | setSchema: function(schema) { 231 | for(var i in schema.properties) { 232 | if (schema.properties[i].index) { 233 | this.indexedProperties[i] = schema.properties[i].index; 234 | } 235 | } 236 | }, 237 | getSchema: function(){ 238 | if(config.type == "mysql"){ 239 | store.startTransaction(); 240 | var results = store.executeSql("DESCRIBE " + config.table, {}); 241 | store.commitTransaction(); 242 | var schema = {properties:{}}; 243 | results.some(function(column){ 244 | schema.properties[column.Field] = { 245 | "default": column.Default, 246 | type: [column.Type.match(/(char)|(text)/) ? "string" : 247 | column.Type.match(/tinyint/) ? "boolean" : 248 | column.Type.match(/(int)|(number)/) ? "number" : 249 | "any", "null"] 250 | }; 251 | if(column.Key == "PRI"){ 252 | schema.links = [{ 253 | rel: "full", 254 | hrefProperty: column.Field 255 | }]; 256 | } 257 | }); 258 | return schema; 259 | } 260 | return {properties:{}}; 261 | }, 262 | setIndex: function(column) { 263 | var sql = "CREATE INDEX " + config.indexPrefix + column + " ON " + config.table + " (" + column + ")"; 264 | print(sql); 265 | //print( first(this.executeSql(sql).rows) ); 266 | 267 | } 268 | }; 269 | for(var i in config){ 270 | store[i] = config[i]; 271 | } 272 | return AutoTransaction(store, database); 273 | } 274 | 275 | try{ 276 | var DATABASE = require("settings").database; 277 | }catch(e){ 278 | print("No settings file defined"); 279 | } 280 | 281 | var defaultDatabase; 282 | exports.defaultDatabase = function(parameters){ 283 | parameters = parameters || {}; 284 | for(var i in DATABASE){ 285 | if(!(i in parameters)){ 286 | parameters[i] = DATABASE[i]; 287 | } 288 | } 289 | 290 | if(defaultDatabase){ 291 | return defaultDatabase; 292 | } 293 | defaultDatabase = SQLDatabase(parameters); 294 | require("stores").registerDatabase(defaultDatabase); 295 | return defaultDatabase; 296 | }; 297 | exports.openDatabase = function(name){ 298 | throw new Error("not implemented yet"); 299 | }; -------------------------------------------------------------------------------- /util/json-ext.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Declarative subset of JavaScript with a few extras beyond JSON, including 3 | * dates, non-finite numbers, etc. 4 | * Derived from and uses: 5 | http://www.JSON.org/json2.js 6 | 2008-11-19 7 | 8 | Public Domain. 9 | 10 | NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. 11 | 12 | */ 13 | 14 | if(typeof JSON === "undefined"){ 15 | require("json"); 16 | } 17 | 18 | var nativeJson = !!JSON.parse.toString().match(/native code/); 19 | exports.parse = function (text) { 20 | 21 | // The parse method takes a text and an optional reviver function, and returns 22 | // a JavaScript value if the text is a valid JSON text. 23 | 24 | var j; 25 | 26 | function walk(value) { 27 | 28 | // The walk method is used to recursively walk the resulting structure so 29 | // that modifications can be made. 30 | 31 | var k; 32 | if (value && typeof value === 'object') { 33 | for (k in value) { 34 | var v = value[k]; 35 | if (typeof v === 'string') { 36 | var a = 37 | /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(v); 38 | if (a) { 39 | value[k] = new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], 40 | +a[5], +a[6])); 41 | } 42 | } 43 | else if (typeof v === 'object') { 44 | walk(v); 45 | } 46 | } 47 | } 48 | } 49 | 50 | if (!text) { 51 | return; 52 | } 53 | // Parsing happens in four stages. In the first stage, we replace certain 54 | // Unicode characters with escape sequences. JavaScript handles many characters 55 | // incorrectly, either silently deleting them, or treating them as line endings. 56 | 57 | cx.lastIndex = 0; 58 | if (cx.test(text)) { 59 | text = text.replace(cx, function (a) { 60 | return '\\u' + 61 | ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 62 | }); 63 | } 64 | 65 | // In the second stage, we run the text against regular expressions that look 66 | // for non-JSON patterns. We are especially concerned with '()' and 'new' 67 | // because they can cause invocation, and '=' because it can cause mutation. 68 | // But just to be safe, we want to reject all unexpected forms. 69 | 70 | // We split the second stage into 4 regexp operations in order to work around 71 | // crippling inefficiencies in IE's and Safari's regexp engines. First we 72 | // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we 73 | // replace all simple value tokens with ']' characters. Third, we delete all 74 | // open brackets that follow a colon or comma or that begin the text. Finally, 75 | // we look to see that the remaining characters are only whitespace or ']' or 76 | // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. 77 | var backSlashRemoved = text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@'); 78 | if (/^[\],:{}\s]*$/. 79 | test(backSlashRemoved. 80 | replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'). 81 | replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { 82 | // it is pure JSON 83 | if(nativeJson){ 84 | // use the native parser if available 85 | j = JSON.parse(text); 86 | } 87 | else{ 88 | // revert to eval 89 | j = eval('(' + text + ')'); 90 | } 91 | walk(j); 92 | return j; 93 | } 94 | else if (/^[\],:{}\s]*$/. 95 | test(backSlashRemoved. 96 | replace(/"[^"\\\n\r]*"|'[^'\\\n\r]*'|\(?new +Date\([0-9]*\)+|[\w$]+\s*:(?:\s*\[)*|true|false|null|undefined|-?Infinity|NaN|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'). 97 | replace(/(?:^|:|,|&&)(?:\s*\[)+/g, ''))) { 98 | // not pure JSON, but safe declarative JavaScript 99 | j = eval('(' + text + ')'); 100 | walk(j); 101 | return j; 102 | } 103 | 104 | // If the text is not JSON parseable, then a SyntaxError is thrown. 105 | 106 | throw new SyntaxError('JSON.parse'); 107 | }; 108 | 109 | var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; 110 | 111 | var nativeConstructors = {"String":String, "Object":Object, "Number":Number, "Boolean":Boolean, "Array":Array, "Date":Date}; 112 | exports.stringify = ({}).toSource ? 113 | // we will use toSource if it is available 114 | (function(){ 115 | Object.keys(nativeConstructors).forEach(function(name){ 116 | (global[name] || global()[name]).toSource = function(){ // you have to call global() in Rhino. Why?!? 117 | return name; 118 | }; 119 | }); 120 | return function(value){ 121 | if(value && typeof value == "object" || typeof value == "function"){ 122 | var source = value.toSource(); 123 | if(source.charAt(0) == "("){ 124 | // remove the surrounding paranthesis that are produced 125 | source = source.substring(1, source.length - 1); 126 | } 127 | return source; 128 | } 129 | if(typeof value === "number" && !isFinite(value)){ 130 | return value.toString(); 131 | } 132 | if(typeof value === "undefined"){ 133 | return "undefined"; 134 | } 135 | return JSON.stringify(value); 136 | }; 137 | })() : 138 | (function(){ 139 | 140 | var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 141 | escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 142 | gap, 143 | indent, 144 | meta = { // table of character substitutions 145 | '\b': '\\b', 146 | '\t': '\\t', 147 | '\n': '\\n', 148 | '\f': '\\f', 149 | '\r': '\\r', 150 | '"' : '\\"', 151 | '\\': '\\\\' 152 | }, 153 | rep; 154 | 155 | 156 | function quote(string) { 157 | 158 | // If the string contains no control characters, no quote characters, and no 159 | // backslash characters, then we can safely slap some quotes around it. 160 | // Otherwise we must also replace the offending characters with safe escape 161 | // sequences. 162 | 163 | escapable.lastIndex = 0; 164 | return escapable.test(string) ? 165 | '"' + string.replace(escapable, function (a) { 166 | var c = meta[a]; 167 | return typeof c === 'string' ? c : 168 | '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 169 | }) + '"' : 170 | '"' + string + '"'; 171 | } 172 | 173 | 174 | function str(key, holder) { 175 | 176 | // Produce a string from holder[key]. 177 | 178 | var i, // The loop counter. 179 | k, // The member key. 180 | v, // The member value. 181 | length, 182 | mind = gap, 183 | partial, 184 | value = holder ? holder[key] : key; 185 | 186 | 187 | // If we were called with a replacer function, then call the replacer to 188 | // obtain a replacement value. 189 | 190 | if (typeof rep === 'function') { 191 | value = rep.call(holder, key, value); 192 | } 193 | 194 | // What happens next depends on the value's type. 195 | 196 | switch (typeof value) { 197 | case 'function': 198 | if(nativeConstructors[value.name] === value){ 199 | return value.name; 200 | } 201 | value = value.toString(); 202 | 203 | case 'string': 204 | return quote(value); 205 | 206 | case 'number': 207 | case 'boolean': 208 | case 'undefined': 209 | case 'null': 210 | 211 | return String(value); 212 | 213 | // If the type is 'object', we might be dealing with an object or an array or 214 | // null. 215 | 216 | case 'object': 217 | 218 | // Due to a specification blunder in ECMAScript, typeof null is 'object', 219 | // so watch out for that case. 220 | 221 | if (!value) { 222 | return 'null'; 223 | } 224 | 225 | // Make an array to hold the partial results of stringifying this object value. 226 | 227 | gap += indent; 228 | partial = []; 229 | 230 | // Is the value an array? 231 | 232 | if (value.forEach) { 233 | 234 | // The value is an array (or forEach-able). Stringify every element. Use null as a placeholder 235 | // for non-JSON values. 236 | length = value.length; 237 | // TODO: properly handle async forEach 238 | value.forEach(function(value, i){ 239 | partial[i] = str(value) || 'null'; 240 | }); 241 | 242 | // Join all of the elements together, separated with commas, and wrap them in 243 | // brackets. 244 | 245 | v = partial.length === 0 ? '[]' : 246 | gap ? '[\n' + gap + 247 | partial.join(',\n' + gap) + '\n' + 248 | mind + ']' : 249 | '[' + partial.join(',') + ']'; 250 | gap = mind; 251 | return v; 252 | 253 | } 254 | if (value instanceof Date){ 255 | return "new Date(" + value.getTime() + ")"; 256 | } 257 | 258 | for (k in value) { 259 | if (Object.hasOwnProperty.call(value, k)) { 260 | v = str(k, value); 261 | partial.push(quote(k) + (gap ? ': ' : ':') + v); 262 | } 263 | } 264 | 265 | // Join all of the member texts together, separated with commas, 266 | // and wrap them in braces. 267 | 268 | v = partial.length === 0 ? '{}' : 269 | gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + 270 | mind + '}' : '{' + partial.join(',') + '}'; 271 | gap = mind; 272 | return v; 273 | } 274 | } 275 | 276 | // If the JSON object does not yet have a stringify method, give it one. 277 | 278 | return function (value, replacer, space) { 279 | 280 | // The stringify method takes a value and an optional replacer, and an optional 281 | // space parameter, and returns a JSON text. The replacer can be a function 282 | // that can replace values, or an array of strings that will select the keys. 283 | // A default replacer method can be provided. Use of the space parameter can 284 | // produce text that is more easily readable. 285 | 286 | var i; 287 | gap = ''; 288 | indent = ''; 289 | 290 | // If the space parameter is a number, make an indent string containing that 291 | // many spaces. 292 | 293 | if (typeof space === 'number') { 294 | for (i = 0; i < space; i += 1) { 295 | indent += ' '; 296 | } 297 | 298 | // If the space parameter is a string, it will be used as the indent string. 299 | 300 | } else if (typeof space === 'string') { 301 | indent = space; 302 | } 303 | 304 | // If there is a replacer, it must be a function or an array. 305 | // Otherwise, throw an error. 306 | 307 | rep = replacer; 308 | if (replacer && typeof replacer !== 'function' && 309 | (typeof replacer !== 'object' || 310 | typeof replacer.length !== 'number')) { 311 | throw new Error('JSON.stringify'); 312 | } 313 | 314 | // Make a fake root object containing our value under the key of ''. 315 | // Return the result of stringifying the value. 316 | 317 | return str('', {'': value}); 318 | }; 319 | 320 | })(); -------------------------------------------------------------------------------- /store/redis.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Redis data store. Depends on 3 | * http://github.com/fictorial/redis-node-client 4 | * This can be automatically resolved by adding the following line to your 5 | * package.json "mappings" object if you are using a package mapping aware module 6 | * loader (like Nodules): 7 | * "redis": "jar:https://github.com/mranney/node_redis/zipball/master!/lib/", 8 | */ 9 | var convertNodeAsyncFunction = require('promised-io/promise').convertNodeAsyncFunction, 10 | when = require('promised-io/promise').when, 11 | defer = require('promised-io/promise').defer, 12 | jsArray = require('rql/js-array'), 13 | JSONExt = require('../util/json-ext'), 14 | redis = require('redis'), 15 | url = require('url'); 16 | 17 | var RQ = require('rql/parser'); 18 | //RQ.converters['default'] = exports.converters.auto; 19 | 20 | // candidate for commonjs-utils? 21 | function dir(){var sys=require('sys');for(var i=0,l=arguments.length;i collection+':*->'+field 87 | var parts = field.split('.'); 88 | if (parts.length > 1) { 89 | coll = parts.shift(); 90 | if (coll.indexOf(':') < 0) { 91 | coll = collection.substring(0, collection.indexOf(':')+1) + coll.substring(coll.indexOf(':')); 92 | } 93 | field = parts.join('.'); 94 | } 95 | // # -> ID 96 | result = (field === 'id' ? (coll != collection ? coll + ':*->' : '') + '#' : (coll + ':*->' + field)); 97 | //dir('FTHF:', field, result); 98 | return result; 99 | } 100 | 101 | // return DB proxy object 102 | return { 103 | ready: function(){ 104 | return ready; 105 | }, 106 | setSchema: function(arg){ 107 | schema = arg; 108 | }, 109 | get: function(id){ 110 | //dir('GET', arguments); 111 | var path = typeof id === "string" ? id.split('.') : [id]; 112 | var promise = defer(); 113 | //// 114 | // db.get(collection+':'+path.shift(), function(err, obj){ 115 | //// 116 | db.hgetall(collection+':'+path.shift(), function(err, obj){ 117 | if (err) {promise.reject(err); throw new URIError(err);} 118 | if (obj) { 119 | convertMultiBulkBuffersToUTF8Strings(obj); 120 | redisHashToRealHash(obj); 121 | //dir('GET', obj); 122 | if (!obj.id) obj.id = id; 123 | for (var i = 0; i < path.length; i++) { 124 | var p = decodeURIComponent(path[i]); 125 | if (!obj) break; 126 | obj = obj.get ? obj.get(p) : obj[p]; 127 | } 128 | } 129 | promise.resolve(obj||undefined); 130 | }); 131 | return promise; 132 | }, 133 | put: function(object, directives){ 134 | directives = directives || {}; 135 | var promise = defer(); 136 | function _put(id, object){ 137 | // store the object 138 | //// 139 | // db.set(collection+':'+id, JSONExt.stringify(object), function(err, result){ 140 | //// 141 | var args = [collection+':'+id]; 142 | args = args.concat(objToFlatArray(object)); 143 | args.push(function(err, result){ 144 | if (err) {promise.reject(err); throw new URIError(err);} 145 | // update collection index 146 | var score = isNaN(id) ? 0 : id; // TODO: more advanced score? 147 | db.zadd(collection, score, id, function(err, result){ 148 | if (err) {promise.reject(err); throw new URIError(err);} 149 | promise.resolve(id); 150 | }); 151 | }); 152 | db.hmset.apply(db, args); 153 | } 154 | // ID can come from URI, from object.id property or be autogenenerated 155 | var id = directives.id || object.id; 156 | if (!id) { 157 | // get a fresh ID from :id key 158 | // TODO: make use of UUIDs 159 | db.incr(collection+':id', function(err, result){ 160 | if (err) {promise.reject(err); throw new URIError(err);} 161 | id = object.id = result; 162 | _put(id, object); 163 | }); 164 | } else { 165 | _put(id, object); 166 | } 167 | return promise; 168 | }, 169 | 'delete': function(id, directives){ 170 | directives = directives || {}; 171 | var promise = defer(); 172 | /*if (id.charAt(0) === '?') { 173 | // FIXME: never happens -- redis won't accept ?name=value 174 | var ids = this.query(id.substring(1) + '&values(id)', directives); 175 | dir('IDS:', ids); 176 | // TODO: ids.map(function(id){remove id like below}) 177 | } else*/ { 178 | // drop : key 179 | db.del(collection+':'+id, function(err, result){ 180 | if (err) {promise.reject(err); throw new URIError(err);} 181 | // and remove id from the index 182 | db.zrem(collection, id, function(err, result){ 183 | if (err) {promise.reject(err); throw new URIError(err);} 184 | promise.resolve(undefined); 185 | }); 186 | }); 187 | } 188 | return promise; 189 | }, 190 | query: function(query, directives){ 191 | directives = directives || {}; 192 | if(typeof query === 'string'){ 193 | query = RQ.parseQuery(query); 194 | } 195 | //dir('QRYYY!', query); 196 | // compile search conditions 197 | var options = { 198 | skip: 0, 199 | limit: +Infinity, 200 | lastSkip: 0, 201 | lastLimit: +Infinity 202 | }; 203 | var jsArrayQuery = ''; // whether to fetch whole dataset to process it here 204 | //// 205 | //jsArrayQuery = query, query = ''; 206 | //// 207 | query && query.args.forEach(function(term){ 208 | var func = term.name; 209 | var args = term.args; 210 | // ignore bad terms 211 | if (!func || !args) return; 212 | //dir(['W:', func, args]); 213 | // process well-known functions 214 | if (func == 'sort' && args.length == 1) { 215 | options.sort = args[0]; 216 | } else if (func == 'limit') { 217 | // we calculate limit(s) combination 218 | options.lastSkip = options.skip; 219 | options.lastLimit = options.limit; 220 | // TODO: validate args, negative args 221 | var l = args[0] || Infinity, s = args[1] || 0; 222 | // N.B: so far the last seen limit() contains Infinity 223 | options.totalCount = args[2]; 224 | if (l <= 0) l = 0; 225 | if (s > 0) options.skip += s, options.limit -= s; 226 | if (l < options.limit) options.limit = l; 227 | //dir('LIMIT', options); 228 | } else if (func == 'select') { 229 | options.fields = args; 230 | } else if (func == 'values') { 231 | options.unhash = true; 232 | options.fields = args; 233 | // process basic criteria 234 | } else if (RQ.commonOperatorMap[func]) { 235 | // N.B. set directives.allowBulkFetch to allow 236 | // decent filtering in redis at the expense of slowdown 237 | if (directives.allowBulkFetch) 238 | jsArrayQuery += term; 239 | } else { 240 | // NYI: what to do? 241 | } 242 | }); 243 | 244 | var args = [collection]; 245 | 246 | // range of non-positive length is trivially empty 247 | //if (options.limit > options.totalCount) 248 | // options.limit = options.totalCount; 249 | if (options.limit <= 0) { 250 | var results = []; 251 | results.totalCount = 0; 252 | return results; 253 | } 254 | 255 | // request full recordset length 256 | //dir('RANGE', options); 257 | // N.B. due to collection.count doesn't respect options.skip and options.limit 258 | // we have to correct returned totalCount manually! 259 | // totalCount will be the minimum of unlimited query length and the limit itself 260 | var totalCountPromise = (!jsArrayQuery && options.totalCount) ? 261 | when(callAsync(db.zcard, args), function(totalCount){ 262 | totalCount -= options.lastSkip; 263 | if (totalCount < 0) 264 | totalCount = 0; 265 | if (options.lastLimit < totalCount) 266 | totalCount = options.lastLimit; 267 | return Math.min(totalCount, typeof options.totalCount === "number" ? options.totalCount : Infinity); 268 | }) : undefined; 269 | 270 | // apply sort 271 | args.push('by'); 272 | if (!jsArrayQuery && options.sort) { 273 | var field = options.sort; 274 | var firstChar = field.charAt(0); 275 | if (firstChar == '-' || firstChar == '+') { 276 | var descending = firstChar == '-'; 277 | field = field.substring(1); 278 | } 279 | args.push(fieldToHashField(field)); 280 | if (descending) args.push('desc'); 281 | args.push('alpha'); 282 | } else { 283 | args.push('nosort'); 284 | } 285 | 286 | // apply limit 287 | if (!jsArrayQuery) { 288 | args.push('limit'); 289 | args.push(options.skip); 290 | args.push(options.limit === Infinity ? -1 : options.limit); 291 | } 292 | 293 | // request lookup fields 294 | (options.fields||[]).forEach(function(field){ 295 | args.push('get'); 296 | args.push(fieldToHashField(field)); 297 | }); 298 | //// 299 | //args.push('get', collection + ':*'); 300 | //// 301 | 302 | // real request 303 | //dir('REQ:', args); 304 | return callAsync(db.sort, args).then(function(results){ 305 | // FIXME: should be async? 306 | convertMultiBulkBuffersToUTF8Strings(results); 307 | if (!results) results = []; 308 | //// 309 | /*results = results.toString('UTF8'); 310 | if (jsArrayQuery) results = JSONExt.parse('['+results+']');*/ 311 | //// 312 | //dir('RES?:', results); 313 | // convert flat array into array of objects 314 | var fields = options.fields || ['id']; 315 | var flen = fields.length; 316 | var len = results.length; 317 | var hash = {}; 318 | var r = []; 319 | for (var i = 0, j = 0; i < len; ++i) { 320 | var value = results[i]; 321 | // TODO: apply auto-conversions (number, boolean) here? 322 | // TODO: make use of schema 323 | /*if (flen == 1) { 324 | r.push(value); 325 | } else*/ { 326 | hash[fields[j++]] = value; 327 | if (j == flen) { 328 | redisHashToRealHash(hash); 329 | r.push(hash); 330 | j = 0; 331 | hash = {}; 332 | } 333 | } 334 | } 335 | results = r; 336 | //dir('RES!:', results); 337 | if (options.unhash) { 338 | results = jsArray.executeQuery('values('+fields+')', directives, results); 339 | } 340 | // process advanced query? 341 | if (jsArrayQuery) { 342 | // pass the lazy array to RQ executor 343 | //dir('RQL?:', query, results, jsArrayQuery); 344 | results = jsArray.executeQuery(jsArrayQuery, directives, results); 345 | //dir('RQL!:', query, results); 346 | } else { 347 | results.totalCount = totalCountPromise; 348 | } 349 | return results; 350 | }); 351 | } 352 | } 353 | }; 354 | -------------------------------------------------------------------------------- /store/sql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is an SQL store that (partially) implements: 3 | * http://www.w3.org/TR/WebSimpleDB/ 4 | * and wraps an SQL database engine based on: 5 | * based on http://www.w3.org/TR/webdatabase/ 6 | */ 7 | var first = require("promised-io/lazy-array").first, 8 | AutoTransaction = require("../transaction").AutoTransaction, 9 | parseQuery = require("rql/parser").parseQuery, 10 | print = require("promised-io/process").print, 11 | defineProperty = require("../util/es5-helper").defineProperty, 12 | when = require("promised-io/promise").when, 13 | defer = require("promised-io/promise").defer, 14 | sqlOperators = require("rql/parser").commonOperatorMap; 15 | 16 | var valueToSql = exports.valueToSql = function(value){ 17 | if(value instanceof Array){ 18 | return "(" + value.map(function(element){ 19 | return valueToSql(element); 20 | }).join(",") + ")"; 21 | } 22 | return typeof(value) == "string" ? "'" + value.replace(/'/g,"''") + "'" : value + ''; 23 | }; 24 | 25 | var safeSqlName = exports.safeSqlName = function(name){ 26 | if(name.match(/[^\w_]/)){ 27 | throw new URIError("Illegal column name " + name); 28 | } 29 | return name; 30 | }; 31 | 32 | exports.SQLDatabase = typeof process != "undefined" ? require("../engines/node/store-engine/sql").SQLDatabase : 33 | require("../engines/rhino/store-engine/sql").SQLDatabase; 34 | 35 | 36 | exports.SQLStore = function(config){ 37 | var database = config.database || exports.openDatabase(config); 38 | var idColumn = config.idColumn = config.idColumn || "id"; 39 | config.indexPrefix = config.indexPrefix || "idx_"; 40 | var store = { 41 | selectColumns: ["*"], 42 | get: function(id){ 43 | return when(store.executeSql("SELECT " + store.selectColumns.join(",") + " FROM " + config.table + " WHERE " + idColumn + "=?", [id]), function(result){ 44 | return first(result.rows); 45 | }); 46 | }, 47 | getId: function(object){ 48 | return object[idColumn]; 49 | }, 50 | "delete": function(id){ 51 | return store.executeSql("DELETE FROM " + config.table + " WHERE " + idColumn + "=?", [id]); // Promise 52 | }, 53 | identifyGeneratedKey: true, 54 | add: function(object, directives){ 55 | var params = [], vals = [], cols = []; 56 | for(var i in object){ 57 | if(object.hasOwnProperty(i)){ 58 | cols.push(i); 59 | vals.push('?'); 60 | params.push(object[i]); 61 | } 62 | } 63 | if(store.identifyGeneratedKey){ 64 | params.idColumn = config.idColumn; 65 | } 66 | var sql = "INSERT INTO " + config.table + " (" + cols.join(',') + ") VALUES (" + vals.join(',') + ")"; 67 | return when(store.executeSql(sql, params), function(results) { 68 | var id = results.insertId; 69 | object[idColumn] = id; 70 | return id; 71 | }); 72 | }, 73 | put: function(object, directives){ 74 | var id = directives.id || object[config.idColumn]; 75 | var overwrite = directives.overwrite; 76 | if(overwrite === undefined){ 77 | overwrite = this.get(id); 78 | } 79 | 80 | if(!overwrite){ 81 | store.add(object, directives); 82 | } 83 | var sql = "UPDATE " + config.table + " SET "; 84 | var first = true; 85 | var params = []; 86 | for(var i in object){ 87 | if(object.hasOwnProperty(i)){ 88 | if(first) first = false; 89 | else sql += ","; 90 | sql += i + "=?"; 91 | params.push(object[i]); 92 | } 93 | } 94 | sql += " WHERE " + idColumn + "=?"; 95 | params.push(object[idColumn]); 96 | 97 | return when(store.executeSql(sql, params), function(result){ 98 | return id; 99 | }); 100 | }, 101 | query: function(query, options){ 102 | options = options || {}; 103 | query = parseQuery(query); 104 | var limit, count, offset, postHandler, results = true; 105 | var where = ""; 106 | var select = this.selectColumns; 107 | var order = []; 108 | var params = (options.parameters = options.parameters || []); 109 | function convertRql(query){ 110 | var conjunction = query.name; 111 | query.args.forEach(function(term, index){ 112 | var column = term.args[0]; 113 | switch(term.name){ 114 | case "eq": 115 | if(term.args[1] instanceof Array){ 116 | if(term.args[1].length == 0){ 117 | // an empty IN clause is considered invalid SQL 118 | if(index > 0){ 119 | where += " " + conjunction + " "; 120 | } 121 | where += "0=1"; 122 | } 123 | else{ 124 | safeSqlName(column); 125 | addClause(column + " IN " + valueToSql(term.args[1])); 126 | } 127 | break; 128 | } 129 | // else fall through 130 | case "ne": case "lt": case "le": case "gt": case "ge": 131 | safeSqlName(column); 132 | addClause(config.table + '.' + column + sqlOperators[term.name] + valueToSql(term.args[1])); 133 | break; 134 | case "sort": 135 | if(term.args.length === 0) 136 | throw new URIError("Must specify a sort criteria"); 137 | term.args.forEach(function(sortAttribute){ 138 | var firstChar = sortAttribute.charAt(0); 139 | var orderDir = "ASC"; 140 | if(firstChar == "-" || firstChar == "+"){ 141 | if(firstChar == "-"){ 142 | orderDir = "DESC"; 143 | } 144 | sortAttribute = sortAttribute.substring(1); 145 | } 146 | safeSqlName(sortAttribute); 147 | order.push(config.table + "." + sortAttribute + " " + orderDir); 148 | }); 149 | break; 150 | case "and": case "or": 151 | where += "("; 152 | convertRql(term); 153 | where += ")"; 154 | break; 155 | case "in": 156 | print("in() is deprecated"); 157 | if(term.args[1].length == 0){ 158 | // an empty IN clause is considered invalid SQL 159 | if(index > 0){ 160 | where += " " + conjunction + " "; 161 | } 162 | where += "0=1"; 163 | } 164 | else{ 165 | safeSqlName(column); 166 | addClause(column + " IN " + valueToSql(term.args[1])); 167 | } 168 | break; 169 | case "select": 170 | term.args.forEach(safeSqlName); 171 | select = term.args.join(","); 172 | break; 173 | case "distinct": 174 | select = "DISTINCT " + select; 175 | break; 176 | case "count": 177 | count = true; 178 | results = false; 179 | postHandler = function(){ 180 | return count; 181 | }; 182 | break; 183 | case "one": case "first": 184 | limit = term.name == "one" ? 2 : 1; 185 | postHandler = function(){ 186 | var firstRow; 187 | return when(results.rows.some(function(row){ 188 | if(firstRow){ 189 | throw new TypeError("More than one object found"); 190 | } 191 | firstRow = row; 192 | }), function(){ 193 | return firstRow; 194 | }); 195 | }; 196 | break; 197 | case "limit": 198 | limit = term.args[0]; 199 | offset = term.args[1]; 200 | count = term.args[2] > limit; 201 | break; 202 | case "mean": 203 | term.name = "avg"; 204 | case "sum": case "max": case "min": 205 | select = term.name + "(" + safeSqlName(column) + ") as value"; 206 | postHandler = function(){ 207 | var firstRow; 208 | return when(results.rows.some(function(row){ 209 | firstRow = row; 210 | }), function(){ 211 | return firstRow.value; 212 | }); 213 | }; 214 | break; 215 | default: 216 | throw new URIError("Invalid query syntax, " + term.name+ " not implemented"); 217 | } 218 | function addClause(sqlClause){ 219 | if(where){ 220 | where += " " + conjunction + " "; 221 | } 222 | where += sqlClause; 223 | } 224 | }); 225 | } 226 | convertRql(query); 227 | var structure = { 228 | select: select, 229 | where: where, 230 | from: config.table, 231 | order: order, 232 | config: config 233 | }; 234 | if(count){ 235 | count = when(store.executeSql(store.generateSqlCount(structure)), function(results){ 236 | return first(results.rows).count; 237 | }); 238 | } 239 | var sql = limit ? 240 | store.generateSqlWithLimit(structure, limit, offset || 0) : 241 | store.generateSql(structure); 242 | return when(store.executeSql(sql), function(results){ 243 | if(postHandler){ 244 | return postHandler(); 245 | } 246 | results = results.rows; 247 | if(count){ 248 | results.totalCount = count; 249 | when(count,function(count) { 250 | results.length = Math.min(limit, count); 251 | }); 252 | } 253 | return results; 254 | }); 255 | }, 256 | generateSql: function(structure){ 257 | return "SELECT " + structure.select + " FROM " + structure.from + 258 | (structure.where && (" WHERE " + structure.where)) + (structure.order.length ? (" ORDER BY " + structure.order.join(", ")): ""); 259 | }, 260 | generateSqlCount: function(structure){ 261 | return "SELECT COUNT(*) as count FROM " + structure.from + 262 | (structure.where && (" WHERE " + structure.where)); 263 | }, 264 | generateSqlWithLimit: function(structure, limit, offset){ 265 | return store.generateSql(structure) + " LIMIT " + limit + " OFFSET " + offset; 266 | }, 267 | executeSql: function(sql, parameters){ 268 | var deferred = defer(); 269 | var result, error; 270 | database.executeSql(sql, parameters, function(value){ 271 | deferred.resolve(result = value); 272 | }, function(e){ 273 | deferred.reject(error = e); 274 | }); 275 | // return synchronously if the data is already available. 276 | if(result){ 277 | return result; 278 | } 279 | if(error){ 280 | throw error; 281 | } 282 | return deferred.promise; 283 | }, 284 | getSchema: function(){ 285 | return {properties:{}}; 286 | }, 287 | setIndex: function(column) { 288 | var sql = "CREATE INDEX " + config.indexPrefix + column + " ON " + config.table + " (" + column + ")"; 289 | print(sql); 290 | //print( first(this.executeSql(sql).rows) ); 291 | 292 | }, 293 | transaction: function(){ 294 | return database.transaction(); 295 | } 296 | }; 297 | var dialect = exports.dialects[config.type]; 298 | for(var i in dialect){ 299 | store[i] = dialect[i] 300 | } 301 | for(var i in config){ 302 | if(i != "type"){ 303 | store[i] = config[i]; 304 | } 305 | } 306 | 307 | return AutoTransaction(store, database); 308 | } 309 | 310 | try{ 311 | var DATABASE = require("../util/settings").database; 312 | }catch(e){ 313 | print("No settings file defined for a database " + e); 314 | } 315 | 316 | exports.openDatabase = function(parameters){ 317 | parameters = parameters || {}; 318 | for(var i in DATABASE){ 319 | if(!(i in parameters)){ 320 | parameters[i] = DATABASE[i]; 321 | } 322 | } 323 | 324 | var db = exports.SQLDatabase(parameters); 325 | return db; 326 | }; 327 | 328 | exports.dialects = { 329 | mysql:{ 330 | getSchema: function(){ 331 | this.startTransaction(); 332 | var results = this.executeSql("DESCRIBE " + config.table, {}); 333 | this.commitTransaction(); 334 | var schema = {properties:{}}; 335 | results.some(function(column){ 336 | schema.properties[column.Field] = { 337 | "default": column.Default, 338 | type: [column.Type.match(/(char)|(text)/) ? "string" : 339 | column.Type.match(/tinyint/) ? "boolean" : 340 | column.Type.match(/(int)|(number)/) ? "number" : 341 | "any", "null"] 342 | }; 343 | if(column.Key == "PRI"){ 344 | schema.links = [{ 345 | rel: "full", 346 | hrefProperty: column.Field 347 | }]; 348 | } 349 | }); 350 | return schema; 351 | }, 352 | identifyGeneratedKey: false 353 | }, 354 | mssql:{ 355 | generateSqlWithLimit: function(structure, limit, offset){ 356 | sql = "SELECT " + structure.select; 357 | sql += " FROM (SELECT ROW_NUMBER() OVER (ORDER BY "; 358 | if (structure.order.length) { 359 | sql += structure.order.join(", "); 360 | } 361 | else { 362 | sql += structure.from + "." + structure.config.idColumn; 363 | } 364 | sql += ") AS __rownum__, " + structure.select; 365 | sql += " FROM " + structure.from; 366 | sql += structure.where && " WHERE " + structure.where; 367 | sql += ") AS " + structure.from; 368 | if (offset) 369 | sql += " WHERE __rownum__ > " + offset; 370 | if (limit) 371 | sql += (offset && " AND" || " WHERE") + " __rownum__ <= " + (limit + offset); 372 | return sql; 373 | } 374 | } 375 | } 376 | -------------------------------------------------------------------------------- /store/mongodb.js: -------------------------------------------------------------------------------- 1 | /** 2 | * MongoDB data store. Depends on 3 | * http://github.com/christkv/node-mongodb-native 4 | * This can be automatically resolved by adding the following line to your 5 | * package.json "mappings" object if you are using a package mapping aware module 6 | * loader (like Nodules): 7 | * "mongodb": "jar:http://github.com/mongodb/node-mongodb-native/zipball/master!/lib/mongodb/" 8 | */ 9 | 10 | // 11 | // N.B. for the latest RQL parser for mongo please refer to https://github.com/dvv/underscore.query 12 | // 13 | 14 | var convertNodeAsyncFunction = require('promised-io/promise').convertNodeAsyncFunction, 15 | //Connection = require("mongodb/connection").Connection, 16 | mongo = require('mongodb'), 17 | ObjectID = require('bson/lib/bson/objectid').ObjectID, 18 | Server = mongo.Server, 19 | sys = require('util'), 20 | defer = require("promised-io/promise").defer, 21 | when = require("promised-io/promise").when, 22 | jsArray = require("rql/js-array"), 23 | PreconditionFailed = require("../errors").PreconditionFailed, 24 | DuplicateEntryError = require('perstore/errors').DuplicateEntryError; 25 | 26 | var RQ = require("rql/parser"); 27 | //RQ.converters["default"] = exports.converters.auto; 28 | 29 | // candidate for commonjs-utils? 30 | function dir(){var sys=require('sys');for(var i=0,l=arguments.length;i 0) { 81 | options.sort = args.map(function(sortAttribute){ 82 | var firstChar = sortAttribute.charAt(0); 83 | var orderDir = 'ascending'; 84 | if (firstChar == '-' || firstChar == '+') { 85 | if (firstChar == '-') { 86 | orderDir = 'descending'; 87 | } 88 | sortAttribute = sortAttribute.substring(1); 89 | } 90 | return [sortAttribute, orderDir]; 91 | }); 92 | } else if (func == 'select') { 93 | options.fields = args; 94 | } else if (func == 'values') { 95 | options.unhash = true; 96 | options.fields = args; 97 | // N.B. mongo has $slice but so far we don't allow it 98 | /*} else if (func == 'slice') { 99 | //options[args.shift()] = {'$slice': args.length > 1 ? args : args[0]};*/ 100 | } else if (func == 'limit') { 101 | // we calculate limit(s) combination 102 | options.lastSkip = options.skip; 103 | options.lastLimit = options.limit; 104 | // TODO: validate args, negative args 105 | var l = args[0] || Infinity, s = args[1] || 0; 106 | // N.B: so far the last seen limit() contains Infinity 107 | options.totalCount = args[2]; 108 | if (l <= 0) l = 0; 109 | if (s > 0) options.skip += s, options.limit -= s; 110 | if (l < options.limit) options.limit = l; 111 | //dir('LIMIT', options); 112 | // grouping 113 | } else if (func == 'group') { 114 | // TODO: 115 | // nested terms? -> recurse 116 | } else if (args[0] && typeof args[0] === 'object') { 117 | if (valid_operators.indexOf(func) > -1) 118 | search['$'+func] = walk(func, args); 119 | // N.B. here we encountered a custom function 120 | // ... 121 | // structured query syntax 122 | // http://www.mongodb.org/display/DOCS/Advanced+Queries 123 | } else { 124 | //dir(['F:', func, args]); 125 | // mongo specialty 126 | if (func == 'le') func = 'lte'; 127 | else if (func == 'ge') func = 'gte'; 128 | // the args[0] is the name of the property 129 | var key = args.shift(); 130 | // the rest args are parameters to func() 131 | if (requires_array.indexOf(func) >= 0) { 132 | args = args[0]; 133 | } else { 134 | // FIXME: do we really need to .join()?! 135 | args = args.length == 1 ? args[0] : args.join(); 136 | } 137 | // regexps: 138 | if (typeof args === 'string' && args.indexOf('re:') === 0) 139 | args = new RegExp(args.substr(3), 'i'); 140 | // regexp inequality means negation of equality 141 | if (func == 'ne' && args instanceof RegExp) { 142 | func = 'not'; 143 | } 144 | // TODO: contains() can be used as poorman regexp 145 | // E.g. contains(prop,a,bb,ccc) means prop.indexOf('a') >= 0 || prop.indexOf('bb') >= 0 || prop.indexOf('ccc') >= 0 146 | //if (func == 'contains') { 147 | // // ... 148 | //} 149 | // valid functions are prepended with $ 150 | if (valid_funcs.indexOf(func) > -1) { 151 | func = '$'+func; 152 | } 153 | // $or requires an array of conditions 154 | // N.B. $or is said available for mongodb >= 1.5.1 155 | if (name == 'or') { 156 | if (!(search instanceof Array)) 157 | search = []; 158 | var x = {}; 159 | x[func == 'eq' ? key : func] = args; 160 | search.push(x); 161 | // other functions pack conditions into object 162 | } else { 163 | // several conditions on the same property is merged into one object condition 164 | if (search[key] === undefined) 165 | search[key] = {}; 166 | if (search[key] instanceof Object && !(search[key] instanceof Array)) 167 | search[key][func] = args; 168 | // equality cancels all other conditions 169 | if (func == 'eq') 170 | search[key] = args; 171 | } 172 | } 173 | // TODO: add support for query expressions as Javascript 174 | }); 175 | return search; 176 | } 177 | //dir(['Q:',query]); 178 | search = walk(query.name, query.args); 179 | //dir(['S:',search]); 180 | return [options, search]; 181 | } 182 | 183 | // this will return a data store 184 | module.exports = function(options){ 185 | var ready = defer(); 186 | var collection, schema; 187 | 188 | function getCollection(db){ 189 | db.collection(options.collection, function(err, coll){ 190 | if(err){ 191 | sys.puts("Failed to load mongo database collection " + dbOptions.name + " collection " + options.collection + " error " + err.message); 192 | ready.reject(err); 193 | }else{ 194 | collection = coll; 195 | ready.resolve(coll); 196 | } 197 | }); 198 | } 199 | 200 | var dbOptions = require("../util/settings").database; 201 | var url = options.url || dbOptions.url; 202 | if(url){ 203 | sys.puts(url); 204 | mongo.connect(url, function(err, db){ 205 | if(err){ 206 | sys.puts('Failed to connect to mongo database ' + url + ' - error: ' + err.message); 207 | ready.reject(err); 208 | } 209 | else { 210 | getCollection(db); 211 | } 212 | }); 213 | } 214 | else { 215 | var database = options.database || new mongo.Db(dbOptions.name, 216 | new Server(dbOptions.host, dbOptions.port || 27017, {}), { w: 0 }); 217 | database.open(function(err, db){ 218 | if(err){ 219 | sys.puts("Failed to load mongo database " + dbOptions.name + " error " + err.message); 220 | ready.reject(err); 221 | } 222 | else{ 223 | getCollection(db); 224 | } 225 | }); 226 | } 227 | 228 | // async helper 229 | function callAsync(method, args){ 230 | return convertNodeAsyncFunction(method, true).apply(collection, args); 231 | } 232 | 233 | // interface 234 | return { 235 | ready: function(){ 236 | return ready; 237 | }, 238 | setSchema: function(arg){ 239 | schema = arg; 240 | }, 241 | get: function(id){ 242 | var deferred = defer(); 243 | collection.findOne({id: id}, function(err, obj){ 244 | if (err) return deferred.reject(err); 245 | if (obj) delete obj._id; 246 | if(obj === null){ 247 | obj = undefined; 248 | } 249 | //dir('GOT:', id, obj, query); 250 | //if (???.queryString) { 251 | // var query = ???.queryString; 252 | // if (query) 253 | // obj = jsArray.executeQuery(query, {}, [obj])[0]; 254 | //} 255 | deferred.resolve(obj); 256 | }); 257 | return deferred; 258 | }, 259 | put: function(object, directives){ 260 | var deferred = defer(); 261 | // N.B. id may come from directives (the primary valid source), 262 | // and from object.id 263 | directives = directives || {}; 264 | var id = directives.id || object.id; 265 | if (!object.id) object.id = id; 266 | var search = {id: id}; 267 | 268 | //dir('PUT:', object, directives.overwrite === false, !id); 269 | if (directives.overwrite === false || !id) {// === undefined) { 270 | // do an insert, and check to make sure no id matches first 271 | collection.findOne(search, function(err, found){ 272 | if (err) return deferred.reject(err); 273 | if (found === null) { 274 | if (!object.id) object.id = ObjectID.createPk().toJSON(); 275 | collection.insert(object, function(err, obj){ 276 | if (err) return deferred.reject(err); 277 | // .insert() returns array, we need the first element 278 | obj = obj && obj[0]; 279 | if (obj) delete obj._id; 280 | deferred.resolve(obj.id); 281 | }); 282 | } else { 283 | deferred.reject(new DuplicateEntryError(id + " exists, and can't be overwritten")); 284 | } 285 | }); 286 | } else { 287 | collection.update(search, object, {upsert: directives.overwrite}, function(err, obj){ 288 | if (err) return deferred.reject(err); 289 | if (obj) delete obj._id; 290 | deferred.resolve(id); 291 | }); 292 | } 293 | return deferred; 294 | }, 295 | "delete": function(id, directives){ 296 | var deferred = defer(); 297 | // compose search conditions 298 | //if (id === undefined) id = '?' + (this.req.queryString || ''); 299 | //if (id.charAt(0) === '?') { 300 | // var x = parse(id.substring(1), metadata); 301 | // var options = x[0], search = x[1]; 302 | //} else { 303 | var search = {id: id}; 304 | //} 305 | // remove matching documents 306 | collection.remove(search, function(err, result){ 307 | if (err) return deferred.reject(err); 308 | deferred.resolve(undefined); 309 | }); 310 | return deferred; 311 | }, 312 | query: function(query, directives){ 313 | //dir('QRY:', query); 314 | var deferred = defer(); 315 | // compose search conditions 316 | var x = parse(query, directives); 317 | var meta = x[0], search = x[1]; 318 | 319 | // range of non-positive length is trivially empty 320 | //if (options.limit > options.totalCount) 321 | // options.limit = options.totalCount; 322 | if (meta.limit <= 0) { 323 | var results = []; 324 | results.totalCount = 0; 325 | return results; 326 | } 327 | 328 | // request full recordset length 329 | //dir('RANGE', options, directives.limit); 330 | // N.B. due to collection.count doesn't respect meta.skip and meta.limit 331 | // we have to correct returned totalCount manually. 332 | // totalCount will be the minimum of unlimited query length and the limit itself 333 | var totalCountPromise = (meta.totalCount) ? 334 | when(callAsync(collection.count, [search]), function(totalCount){ 335 | totalCount -= meta.lastSkip; 336 | if (totalCount < 0) 337 | totalCount = 0; 338 | if (meta.lastLimit < totalCount) 339 | totalCount = meta.lastLimit; 340 | // N.B. just like in rql/js-array 341 | return Math.min(totalCount, typeof meta.totalCount === "number" ? meta.totalCount : Infinity); 342 | }) : undefined; 343 | //} 344 | 345 | // request filtered recordset 346 | //dir('QRY:', search); 347 | collection.find(search, meta, function(err, cursor){ 348 | if (err) return deferred.reject(err); 349 | cursor.toArray(function(err, results){ 350 | if (err) return deferred.reject(err); 351 | // N.B. results here can be [{$err: 'err-message'}] 352 | // the only way I see to distinguish from quite valid result [{_id:..., $err: ...}] is to check for absense of _id 353 | if (results && results[0] && results[0].$err !== undefined && results[0]._id === undefined) { 354 | return deferred.reject(results[0].$err); 355 | } 356 | var fields = meta.fields; 357 | var len = results.length; 358 | // damn ObjectIDs! 359 | for (var i = 0; i < len; i++) { 360 | delete results[i]._id; 361 | } 362 | // kick out unneeded fields 363 | if (fields) { 364 | // unhash objects to arrays 365 | if (meta.unhash) { 366 | results = jsArray.executeQuery('values('+fields+')', directives, results); 367 | } 368 | } 369 | // total count 370 | when(totalCountPromise, function(result){ 371 | results.count = results.length; 372 | results.start = meta.skip; 373 | results.end = meta.skip + results.count; 374 | results.schema = schema; 375 | results.totalCount = result; 376 | //dir('RESULTS:', results.slice(0,0)); 377 | deferred.resolve(results); 378 | }); 379 | }); 380 | }); 381 | return deferred; 382 | }, 383 | // directly expose collection for advanced functions 384 | collection: function(){ 385 | return collection; 386 | } 387 | } 388 | } 389 | module.exports.MongoDB = module.exports; 390 | -------------------------------------------------------------------------------- /store/couchdb.js: -------------------------------------------------------------------------------- 1 | /* 2 | * CouchDB store 3 | */ 4 | 5 | var http = require("promised-io/http-client"), 6 | defer = require("promised-io/promise").defer, 7 | when = require("promised-io/promise").when, 8 | LazyArray = require("promised-io/lazy-array").LazyArray, 9 | settings = require("../util/settings"); 10 | 11 | 12 | function bodyToString(body) { 13 | var buffer = []; 14 | return when( 15 | body.forEach(function(chunk) { 16 | buffer.push(chunk); 17 | } || []), 18 | function() { 19 | return buffer.join(""); 20 | } 21 | ); 22 | }; 23 | 24 | function bodyToObject(body) { 25 | return when(bodyToString(body), function(body) { 26 | return JSON.parse(body); 27 | }); 28 | }; 29 | 30 | function bodyToDocument(body, metadata, identityProperty) { 31 | /* converts a jsgi response body from couch to a document object */ 32 | return when(bodyToObject(body), function(object) { 33 | return objectToDocument(object, metadata, identityProperty); 34 | }); 35 | }; 36 | 37 | function responseToObject(response) { 38 | return when(response, function(response) { 39 | return bodyToObject(response.body); 40 | }); 41 | }; 42 | 43 | function responseToDocument(response, identityProperty) { 44 | /* converts a jsgi response from couch to a document object */ 45 | return when(response, function(response) { 46 | if (response.status === 404) return undefined; 47 | return bodyToDocument(response.body, response.headers, identityProperty); 48 | }); 49 | }; 50 | 51 | function objectToDocument(object, metadata, identityProperty) { 52 | metadata = metadata || {}; 53 | var id = identityProperty || "id"; 54 | if (id && id !== "_id") { 55 | object[id] = object._id; 56 | delete object._id; 57 | } 58 | if ("_rev" in object) { 59 | metadata.etag = object._rev; 60 | delete object._rev; 61 | } 62 | object.getMetadata = function() { 63 | return metadata; 64 | }; 65 | return object; 66 | }; 67 | 68 | function bodyToArray(body, metadata, expectedLength) { 69 | /* takes advantage of couch's consistent view formatting to stream json */ 70 | metadata = metadata || {}; 71 | var array = LazyArray({ 72 | some: function(write) { 73 | var remainder = "", 74 | header; 75 | return body.forEach(function(chunk) { 76 | chunk = remainder + chunk; 77 | if (!header) { 78 | // first capture the header object 79 | header = chunk.match(/^(.*?),"rows":\[/); 80 | if (!header) throw new Error("invalid response array"); 81 | chunk = chunk.substring(header[0].length); 82 | header = JSON.parse(header[1] + "}"); 83 | this.totalCount = header.total_rows; // is this legit? 84 | if (typeof expectedLength === "number") 85 | this.length = Math.min(expectedLength, totalCount); 86 | } 87 | var rows = chunk.split("\n").filter(function(row) { 88 | return row.trim(); 89 | }).map(function(row) { 90 | return row.replace(/,\s*$/, ""); 91 | }); 92 | remainder = rows.pop(); 93 | rows.forEach(function(row) { 94 | write(JSON.parse(row)); 95 | }); 96 | // FIXME do we ever need to handle the trailing "]}"? 97 | }); 98 | } 99 | }); 100 | if (metadata) { 101 | array.getMetadata = function() { 102 | return metadata; 103 | }; 104 | } 105 | return array; 106 | }; 107 | 108 | function responseToArray(response, expectedLength) { 109 | return when(response, function(response) { 110 | return bodyToArray(response.body, response.headers, expectedLength); 111 | }); 112 | }; 113 | 114 | function objectToBody(object) { 115 | return [JSON.stringify(object)]; 116 | }; 117 | 118 | function documentToBody(object, directives, identityProperty) { 119 | // FIXME is getIdentityProperty correct? if so should we use it? 120 | var id = identityProperty || "id"; 121 | if (id !== "_id") { 122 | object._id = object[id]; 123 | delete object[id]; 124 | } 125 | if (directives && directives["if-match"]) { 126 | object._rev = directives["if-match"]; 127 | delete directives["if-match"]; 128 | } 129 | return objectToBody(object); 130 | }; 131 | 132 | var Server = exports.Server = function(config) { 133 | if (!(this instanceof Server)) return new Server(config); 134 | var server = config || {}, 135 | host = server.host || (settings.couchdb && settings.couchdb.host) || "http://localhost:5984/"; 136 | 137 | // this method binds server host information to the rest of the url 138 | server.request = function(method, path, headers, body) { 139 | var request = { 140 | method: method, 141 | uri: host + path 142 | } 143 | if (headers) request.headers = headers; 144 | if (body) request.body = body; 145 | return http.request(request); 146 | }; 147 | 148 | // add simple method helpers 149 | ["GET", "POST", "PUT", "DELETE", "COPY"].forEach(function(method) { 150 | server[method] = function(path, headers, body) { 151 | return server.request(method, path, headers, body); 152 | } 153 | }); 154 | 155 | server.getInfo = function() { 156 | return responseToObject(server.GET("_config")); 157 | }; 158 | 159 | server.createDatabase = function(name) { 160 | // A database must be named with all lowercase letters (a-z), digits (0-9), or any of the _$()+-/ characters and must end with a slash in the URL. The name has to start with a lowercase letter (a-z). 161 | return when(server.PUT(name + "/"), function(response) { 162 | if (response.status === 201) { 163 | return true; 164 | } 165 | else if (response.status === 412) { 166 | return false; 167 | } 168 | else { 169 | // FIXME what other errors? auth? name errors? 170 | // TODO throw perstore-specific errors? 171 | throw new Error("database create failed"); 172 | } 173 | }); 174 | }; 175 | 176 | server.dropDatabase = function(name) { 177 | return when(server.DELETE(name + "/"), function(response) { 178 | if (response.status === 200) return true; 179 | }); 180 | }; 181 | 182 | for (var key in server) this[key] = server[key]; 183 | }; 184 | 185 | var defaultServer; 186 | var Database = exports.Database = function(name, config) { 187 | if (!name) throw new Error("No name defined for database"); 188 | if (!(this instanceof Database)) return new Database(name, config); 189 | var db = config || {}; 190 | db.name = name; 191 | db.mvcc = "mvcc" in db ? db.mvcc : true; 192 | 193 | if (!defaultServer) defaultServer = new Server(); 194 | var server = db.server || defaultServer; 195 | 196 | db.get = function(id, directives) { 197 | return responseToDocument(server.GET(db.name + "/" + id, directives)); 198 | }; 199 | 200 | db.query = function(query, directives) { 201 | if (!query) return getAllDocuments(); 202 | // TODO parse and inspect query 203 | return responseToArray(server.GET(db.name + "/_someview" + query, directives)).map(function(object) { 204 | function(object) { 205 | return objectToDocument(response); 206 | } 207 | }); 208 | }; 209 | 210 | function mvccOverride(directives) { 211 | if (!config.mvcc) { 212 | var previous = directives.previous, 213 | ifMatch = previous && previous.getMetadata && previous.getMetadata().etag; 214 | if (ifMatch) 215 | directives["if-match"] = directives["if-match"] || ifMatch; 216 | } 217 | }; 218 | 219 | db.put = function(object, directives) { 220 | var id = object.getId ? object.getId() : object.id; 221 | if (!id) throw new Error("Object being PUT must have an id"); 222 | directives = directives || {}; 223 | mvccOverride(directives); 224 | delete directives.previous; 225 | return responseToDocument(server.PUT(db.name + "/" + id, directives, documentToBody(object, directives))); 226 | }; 227 | 228 | db["delete"] = function(id, directives) { 229 | directives = directives || {}; 230 | // TODO we need directives.previous here too to override mvcc 231 | // presumably conditionals are being checked for deletes too? 232 | if (!db.mvcc) directives.previous = db.get(id); // HACK 233 | mvccOverride(directives); 234 | delete directives.previous; 235 | // BUG couch not respecting if-match header per the docs: 236 | // return when(server.DELETE(db.name + "/" + id, directives), function(response) { 237 | return when(server.DELETE(db.name + "/" + id + "?rev=" + directives["if-match"], directives), function(response) { 238 | if (response.status === 200) return true; 239 | }); 240 | }; 241 | 242 | var schema; 243 | db.setSchema = function(s) { 244 | return schema = s; 245 | }; 246 | 247 | /* 248 | * CouchDB-specific API extensions 249 | */ 250 | 251 | db.copy = function(id, directives) { 252 | // FIXME 253 | if (!directives || !directives.destination) 254 | throw new Error("A destination directive must be supplied"); 255 | if (directives["if-match"]) { 256 | // TODO file a couch issue re: supporting if-match with destination 257 | directives.destination += "?rev=" + directives["if-match"]; 258 | delete directives["if-match"]; 259 | } 260 | 261 | // TODO handle * by subverting MVCC? 262 | return when(server.COPY(db.name + "/" + id, directives), function(response) { 263 | // TODO 264 | // is it 201 created? 265 | // get old object and add getMetadata fn w/ etag 266 | var getMetadata = function() { 267 | return { 268 | etag: response.headers.etag 269 | } 270 | } 271 | return true; 272 | }); 273 | }; 274 | 275 | db.getAllDocuments = function(options) { 276 | options = options || {}; 277 | var results = responseToArray(server.GET(db.name + "/_all_docs?include_docs=true")).map(function(object) { 278 | return object.doc; 279 | }); 280 | if (!options.includeDesigns) { 281 | results = results.filter(function(object) { 282 | return !object._id || object._id.indexOf("_design/") !== 0; 283 | }); 284 | } 285 | return results.map(function(object) { 286 | return objectToDocument(object); 287 | }); 288 | } 289 | 290 | db.getAllDesigns = function() { 291 | var path = db.name + "/_all_docs?include_docs=true&startkey=%22_design%2F%22&endkey=%22_design0%22"; 292 | return responseToArray(server.GET(path)).map(function(object) { 293 | return objectToDocument(object.doc); 294 | }); 295 | }; 296 | 297 | db.getDesign = function(name) { 298 | if (!name) return db.getAllDesigns(); 299 | return responseToDocument(server.GET(db.name + "_design/" + name)); 300 | }; 301 | 302 | db.viewCleanup = function() { 303 | return responseToObject(server.POST(db.name + "_view_cleanup")); 304 | }; 305 | 306 | db.compactView = function(name) { 307 | return responseToObject(server.POST(db.name + "_compact/" + name)); 308 | }; 309 | 310 | db.compact = function() { 311 | return responseToObject(server.POST(db.name + "_compact/")); 312 | }; 313 | 314 | db.create = function() { 315 | return server.createDatabase(db.name); 316 | }; 317 | 318 | db.drop = function() { 319 | return server.dropDatabase(db.name) 320 | }; 321 | 322 | db.clear = function() { 323 | return when(server.dropDatabase(db.name), function() { 324 | return server.createDatabase(db.name); 325 | }); 326 | }; 327 | 328 | db.getInfo = function() { 329 | return responseToObject(server.GET(db.name + "/")); 330 | }; 331 | 332 | function bodyToInteger(body) { 333 | return when(bodyToString(body), function(string) { 334 | return parseInt(string, 10); 335 | }); 336 | }; 337 | 338 | if (db.createIfNecessary) { 339 | // FIXME should we check for its existence? handle auth error? 340 | require("promised-io/promise").wait(db.create()); 341 | } 342 | 343 | for (var key in db) this[key] = db[key]; 344 | 345 | Object.defineProperty(this, "revisionLimit", { 346 | get: function() { 347 | return when(server.GET(db.name + "/_revs_limit"), function(response) { 348 | return bodyToInteger(response.body); 349 | }); 350 | }, 351 | set: function(value) { 352 | return when(server.PUT(db.name + "/_revs_limit"), function(response) { 353 | return bodyToInteger(response.body); 354 | }); 355 | }, 356 | enumerable: true 357 | }); 358 | } 359 | 360 | 361 | var defaultDesign; 362 | var Design = exports.Design = function(name, config) { 363 | if (!name) throw new Error("No name defined for design document"); 364 | if (!(this instanceof Design)) return new Design(name, config); 365 | config = config || {} 366 | for (var key in config) { 367 | this[key] = config[key]; 368 | } 369 | } 370 | 371 | 372 | var View = exports.View = function(name, config) { 373 | if (!name) throw new Error("No name defined for view"); 374 | if (!(this instanceof View)) return new View(name, config); 375 | config = config || {} 376 | for (var key in config) { 377 | this[key] = config[key]; 378 | } 379 | var db = config.database || defaultDatabase, 380 | schema; 381 | return { 382 | get: function(id) { 383 | 384 | }, 385 | query: function(query, directives) { 386 | query = parseQuery(query); 387 | function parse(terms) { 388 | 389 | } 390 | 391 | }, 392 | setSchema: function(s) { 393 | return schema = s; 394 | } 395 | } 396 | } 397 | 398 | var Class = exports.Class = function(config) { 399 | /** 400 | ** Extends the read-only View store to add write methods for a class 401 | **/ 402 | if (!(this instanceof Class)) return new Class(name, config); 403 | config = config || {} 404 | for (var key in config) { 405 | this[key] = config[key]; 406 | } 407 | config = config || {}; 408 | var db = config.database || defaultDatabase, 409 | store = new View(config), 410 | schema; 411 | 412 | store.put = function(object, directives) { 413 | 414 | } 415 | store["delete"] = function(id) { 416 | 417 | } 418 | store.setSchema = function(s) { 419 | store.setSchema(s); 420 | return schema = s; 421 | } 422 | 423 | return store; 424 | }; 425 | 426 | 427 | 428 | exports.Managed = function(config) { 429 | config = config || {}; 430 | var db = config.database || defaultDatabase, 431 | store = exports.Document(config), 432 | schema; 433 | 434 | store.query = function(query, directives) { 435 | 436 | 437 | 438 | var q = { 439 | couch: { 440 | include_docs: true, 441 | reduce: false 442 | }, 443 | setDimension: function(dimension) { 444 | if (this.dimension) { 445 | if (dimension !== this.dimension) { 446 | // can't do multidimensional queries in couch 447 | throw new Error("Unsatisfiable query"); 448 | } 449 | // check existing this.couch to make sure it's sane 450 | } 451 | this.dimension = dimension; 452 | } 453 | } 454 | 455 | if (directives.stale) q.couch.stale = "ok"; //FIXME what's the right header? 456 | 457 | var compile = { 458 | eq: function(args) { 459 | q.setDimension(args[0]); 460 | q.couch.key = args[1]; 461 | }, 462 | ge: function(args) { 463 | q.setDimension(args[0]); 464 | q.couch.startkey = args[1]; 465 | }, 466 | gt: function(args) { 467 | q.setDimension(args[0]); 468 | q.couch.startkey = args[1]; 469 | q.excludeStart = args[1]; // TODO 470 | }, 471 | le: function(args) { 472 | q.setDimension(args[0]); 473 | q.couch.startkey = args[1]; 474 | }, 475 | lt: function(args) { 476 | q.setDimension(args[0]); 477 | q.couch.startkey = args[1]; 478 | q.excludeEnd = args[1]; 479 | q.couch.inclusive_end = false; 480 | }, 481 | limit: function(args) { 482 | q.couch.limit = args[0]; 483 | if (args[1]) q.couch.skip = args[1]; 484 | }, 485 | reverse: function(args) { 486 | q.couch.descending = true; 487 | var startkey = q.couch.startkey; 488 | q.couch.startkey = q.couch.endkey; 489 | q.couch.endkey = startkey; 490 | // reset inclusive start/end 491 | var excludeStart = q.excludeStart; 492 | q.excludeStart = q.excludeEnd; 493 | q.excludeEnd = excludeStart; 494 | q.couch.inclusive_end = typeof q.excludeEnd === "undefined"; 495 | }, 496 | select: function(args) { 497 | // postprocess, break 498 | }, 499 | values: function(args) { 500 | // postprocess, break 501 | }, 502 | distinct: function(args) { 503 | // postprocess, break 504 | } 505 | }; 506 | 507 | // add the reduce ops 508 | ["sum", "count", "min", "max", "sumsqr"].forEach(function(op) { 509 | compile[op] = function(args) { 510 | q.couch.include_docs = false; 511 | q.couch.reduce = true; 512 | q.returnProperty = op; 513 | throw new Error("Cannot continue"); 514 | } 515 | }); 516 | 517 | if (typeof query === "string") query = parseQuery(query); 518 | 519 | // if user has privs and query starts with "or" submit q.couch and apply whole query to lazy-array 520 | // if query starts with "and" term loop over 521 | // try { 522 | // compile[term] 523 | // toplevelargs.shift() 524 | // catch (e) { 525 | // // if BadRequestError 526 | // if priveledged, submit q.couch as is and apply the rest of the query to lazy-array 527 | // } else { 528 | // compile[query.name](query.args); 529 | // } 530 | // 531 | 532 | compile[query.name](query.args); 533 | } 534 | 535 | store.setSchema = function(s) { 536 | store.setSchema(s); 537 | var indexedProperties = []; 538 | for (var name in s.properties) { 539 | var prop = s.properties[name]; 540 | if (s.properties[name].index) { 541 | indexedProperties.push(name); 542 | } 543 | } 544 | if (indexedProperties.length) { 545 | var design = db.getDesign(); 546 | indexedProperties.forEach(function(name) { 547 | design.views = design.views || {}; 548 | design.views[schema.id + "." + name] = { 549 | map: "function(doc) { if (doc.perstore_class == '" + schema.id + "') emit(doc." + name + ", 1) }", 550 | reduce: "_stats" 551 | } 552 | }); 553 | db.setDesign(design); 554 | } 555 | return schema = s; 556 | } 557 | return store; 558 | } 559 | 560 | /*exports.errorHandler = function(response) { 561 | // translate couch json error messages into js errors 562 | if (response.status >= 400) { 563 | if (response.body) { 564 | var message = JSON.parse(response.body); 565 | if (message.error === "not_found") throw new error.status[404]; 566 | } 567 | } 568 | }*/ 569 | 570 | // NOTES 571 | /* 572 | _rev: The current revision of this document 573 | _attachments: If the document has attachments, _attachments holds a data structure, which can also be mapped 574 | TODO _attachements: create $refs to attachments? 575 | TODO _deleted: api 576 | 577 | 578 | should be available as methods: 579 | revisions (_revisions, _rev_infos) 580 | conflicts (_conflicts, _deleted_conflicts) 581 | */ 582 | 583 | 584 | 585 | //Couch uses multiversion concurrency control by default. This can be overridden on initialization 586 | //directives.previous can be used to attempt to override mvcc effeciently 587 | 588 | 589 | // couch doc ids cannot begin with an underscore 590 | // top level keys for objects stored in couch cannot begin with an underscore -------------------------------------------------------------------------------- /facet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This provides the facet-based programming model for pintura, allowing for different 3 | * views or forms of accessing the underlying data stores. Different facets can be used 4 | * for different application access points, different security levels, and different locales. 5 | */ 6 | 7 | var DatabaseError = require("./errors").DatabaseError, 8 | AccessError = require("./errors").AccessError, 9 | MethodNotAllowedError = require("./errors").MethodNotAllowedError, 10 | defineProperties = require("./util/es5-helper").defineProperties, 11 | LazyArray = require("promised-io/lazy-array").LazyArray, 12 | promiseModule = require("promised-io/promise"), 13 | when = promiseModule.when, 14 | copy = require("./util/copy").copy, 15 | Query = require("rql/query").Query, 16 | substitute = require("json-schema/lib/validate").substitute, 17 | rpcInvoke = require("./json-rpc").invoke; 18 | require("./coerce");// patches json-schema 19 | 20 | exports.Facet = Facet; 21 | Facet.facetFor = function(store, resolver, mediaType){ 22 | var schema = mediaType.match(/schema=(.*)/)[1]; 23 | if(schema){ 24 | return Facet.instances.filter(function(facet){ 25 | return facet.id == schema; 26 | })[0]; 27 | } 28 | }; 29 | var httpHandlerPrototype = { 30 | options: function(id){ 31 | return Object.keys(this); 32 | }, 33 | trace: function(obj){ 34 | return obj; 35 | }, 36 | wrap: function(instance){ 37 | throw new Error("wrap must be implemented in FacetedStore implementations"); 38 | }, 39 | patch: function(props, id){ 40 | return this.copyProperties(props,{id:id}); 41 | }, 42 | copyProperties: function(props, directives){ 43 | var target = this.get(directives.id); 44 | return when(target, function(target){ 45 | for(var i in props){ 46 | if(props.hasOwnProperty(i) && (target[i] !== props[i])){ 47 | target[i] = props[i]; 48 | } 49 | 50 | } 51 | target.save(directives); 52 | return target; 53 | }); 54 | } 55 | 56 | }; 57 | var NEW = {}; 58 | function FacetedStore(store, facetSchema){ 59 | function constructor(){ 60 | return constructor.construct.apply(constructor, arguments); 61 | } 62 | 63 | var i; 64 | 65 | facetSchema.prototype = facetSchema.prototype || {}; 66 | for(i in facetSchema){ 67 | constructor[i] = facetSchema[i]; 68 | } 69 | constructor.instanceSchema = facetSchema; 70 | var constructOnNewPut = !facetSchema.noConstructPut; 71 | var needsOldVersion = constructOnNewPut; 72 | var properties = constructor.properties; 73 | var indexedProperties = {id: true}; 74 | for(i in properties){ 75 | var propDef = properties[i]; 76 | if(propDef.readonly || propDef.blocked){ 77 | needsOldVersion = true; 78 | } 79 | if(propDef.indexed){ 80 | indexedProperties[i] = true; 81 | } 82 | } 83 | constructor.id = store.id; 84 | constructor.query= function(query, directives){ 85 | if(arguments.length === 0){ 86 | query = Query(); 87 | query.executor = function(query){ 88 | return constructor.query(query.toString()); 89 | }; 90 | return query; 91 | } 92 | if(typeof facetSchema.query !== "function"){ 93 | if(typeof store.query !== "function"){ 94 | throw new MethodNotAllowedError("No query capability provided"); 95 | } 96 | return this.wrap(store.query(query, directives), this.transaction); 97 | } 98 | return this.wrap(facetSchema.query(query, directives), this.transaction); 99 | }; 100 | 101 | var allowedOperators = constructor.allowedOperators || store.allowedOperators 102 | || { 103 | select: true, 104 | limit: true, // required 105 | ne: true, 106 | and: true, 107 | eq: "indexed", 108 | le: "indexed", 109 | lt: "indexed", 110 | ge: "indexed", 111 | gt: "indexed", 112 | sort: "indexed" 113 | }; 114 | var maxLimit = constructor.maxLimit || store.maxLimit || 50; 115 | 116 | constructor.checkQuery = function(query){ 117 | var lastLimit; 118 | var checkOperator = function(operator, checkLimit){ 119 | var name = operator.name; 120 | if(!allowedOperators[name]){ 121 | throw new AccessError("Query operator " + name + " not allowed for this user. You can assign allowed operators in the allowedOperators property of the facet or model."); 122 | } 123 | if(allowedOperators[name] === "indexed" && 124 | !indexedProperties[name === "sort" ? operator.args[0].replace(/^[-\+]/,'') : operator.args[0]]){ 125 | throw new AccessError("Query operator " + name + " not allowed for unindexed property " + operator.args[0] + " for this user. You can assign indexed operators in the indexedProperties property of the facet or model"); 126 | } 127 | operator.args.forEach(function(value){ 128 | if(value && value.name && value.args){ 129 | if(checkLimit && value.name === "limit"){ 130 | lastLimit = value.args[0]; 131 | } 132 | checkOperator(value); 133 | } 134 | }); 135 | }; 136 | checkOperator(Query(query), true); 137 | if(!lastLimit && maxLimit != Infinity){ 138 | throw new RangeError("This user is not allowed to execute a query without a range specified through a Range header or a limit operator in the query like ?limit(10)"); 139 | } 140 | if(lastLimit > maxLimit){ 141 | throw new RangeError("This user is not allowed to execute a query with a limit of " + lastLimit + " the user has maximum range of " + maxLimit); 142 | } 143 | // TODO: should we args[0] = parsedQuery to pass on a the parsed query so it doesn't need to be reparsed? 144 | }; 145 | 146 | constructor.construct = function(instance, directives){ 147 | var result; 148 | 149 | instance = this.wrap({}, this.transaction, instance, NEW); 150 | for(var i in properties){ 151 | var propDef = properties[i]; 152 | if("default" in propDef && !(i in instance)){ 153 | var def = propDef["default"]; 154 | instance[i] = typeof def === "function" ? def() : def; 155 | } 156 | } 157 | directives = directives || {}; 158 | directives.overwrite = false; 159 | if(typeof facetSchema.construct === "function"){ 160 | result = facetSchema.construct(instance, directives); 161 | if(result === undefined){ 162 | result = instance; 163 | } 164 | return result; 165 | } 166 | if(typeof facetSchema.__noSuchMethod__ === "function"){ 167 | result = facetSchema.__noSuchMethod__("construct", [instance, directives], true); 168 | if(result === undefined){ 169 | result = instance; 170 | } 171 | if(result !== null){ 172 | return result; 173 | } 174 | } 175 | // for back-compat: 176 | if(typeof instance.initialize === "function"){ 177 | instance.initialize.apply(instance, arguments); 178 | } 179 | return instance; 180 | }; 181 | constructor.get = function(id, directives){ 182 | if(typeof facetSchema.get === "function"){ 183 | return this.wrap(facetSchema.get(id, directives)); 184 | } 185 | return this.wrap(store.get(id, directives)); 186 | }; 187 | constructor["delete"] = function(id, directives){ 188 | try{ 189 | Object.defineProperty(directives, "previous", { 190 | get: function(){ 191 | return constructor.get(id); 192 | } 193 | }); 194 | }catch(e){ 195 | // silence errors about frozen objects 196 | } 197 | if(typeof facetSchema["delete"] === "function"){ 198 | return this.wrap(facetSchema["delete"](id, directives)); 199 | } 200 | return this.wrap(store["delete"](id, directives)); 201 | }; 202 | constructor.add = function(props, directives){ 203 | return constructor.construct(props).save(directives); 204 | }; 205 | constructor.put = function(props, directives){ 206 | var instance; 207 | 208 | directives = directives || {}; 209 | if (!directives.id) { 210 | directives.id = facetSchema.getId(props); 211 | } 212 | if(typeof props.save !== "function"){ 213 | try{ 214 | if(needsOldVersion){ 215 | instance = this.get(directives.id); 216 | } 217 | } 218 | catch(e){ 219 | } 220 | var self = this; 221 | return when(instance, function(instance){ 222 | if(!instance){ 223 | if(constructOnNewPut){ 224 | // we are doing a PUT for a new object 225 | return self.add(props, directives); 226 | } 227 | // doesn't exist or exists but not loaded, we create a new instance 228 | instance = self.wrap({}, self.transaction, instance, NEW); 229 | return when(instance.save(directives), function(newInstance){ 230 | if(directives.id && (facetSchema.getId(newInstance) != directives.id)){ 231 | throw new Error("Object's id does not match the target URI"); 232 | } 233 | return newInstance; 234 | }); 235 | } 236 | if(props.getMetadata && instance.getMetadata){ 237 | // do conflict detection with the metadata 238 | var incoming = props.getMetadata(); 239 | var current = instance.getMetadata(); 240 | var ifUnmodifiedSince = Date.parse(incoming["if-unmodified-since"]); 241 | var lastModified = Date.parse(current["last-modified"]); 242 | if(ifUnmodifiedSince && lastModified){ 243 | if(lastModified > ifUnmodifiedSince){ 244 | throw new DatabaseError(4, "Object has been modified since " + ifUnmodifiedSince); 245 | } 246 | } 247 | var etag = current.etag; 248 | var ifMatch = incoming["if-match"]; 249 | if(etag && ifMatch){ 250 | if(etag != ifMatch){ 251 | throw new DatabaseError(4, "Object does match " + ifMatch); 252 | } 253 | } 254 | 255 | } 256 | return when(instance.save.call(props, directives), function(){ 257 | instance.load(); 258 | return instance; 259 | }); 260 | }); 261 | } 262 | else{ 263 | return when(props.save(directives), function(){ 264 | props.load(); 265 | return props; 266 | }); 267 | } 268 | 269 | }; 270 | constructor.post = function(props, directives){ 271 | if(typeof facetSchema.post === "function"){ 272 | return this.wrap(facetSchema.post(props, directives)); 273 | } 274 | if(!directives.id){ 275 | // create a new object 276 | return this.add(props, directives); 277 | } 278 | else{ 279 | // check to see if it is an RPC object 280 | // TODO: Do this: if(props instanceof RPC){ // where the media handler creates RPC objects 281 | if(props && "method" in props && "id" in props && "params" in props){ 282 | // looks like JSON-RPC 283 | return rpcInvoke(this.get(directives.id), props, directives); 284 | } 285 | // doing an incremental update 286 | return this.copyProperties(props, directives); 287 | } 288 | }; 289 | 290 | constructor.__proto__ = httpHandlerPrototype; 291 | 292 | // TODO: handle immutable proto 293 | return constructor; 294 | } 295 | var mustBeValid = require("json-schema/lib/validate").mustBeValid; 296 | var validate = require("json-schema/lib/validate").validate; 297 | var writableProto = !!({}.__proto__); 298 | var SchemaControlled = function(facetSchema, sourceClass, permissive){ 299 | var properties = facetSchema.properties; 300 | var schemaLinks = facetSchema.links || sourceClass.links; 301 | var idTemplate; 302 | if(schemaLinks && schemaLinks instanceof Array){ 303 | schemaLinks.forEach(function(link){ 304 | /* // TODO: allow for multiple same-name relations 305 | if(links[link.rel]){ 306 | if(!(links[link.rel] instanceof Array)){ 307 | links[link.rel] = [links[link.rel]]; 308 | } 309 | }*/ 310 | if(link.rel == "self"){ 311 | idTemplate = link.href; 312 | } 313 | }); 314 | } 315 | if(!facetSchema.getId){ 316 | if(idTemplate){ 317 | Object.defineProperty(facetSchema, "getId", { 318 | value: function(object){ 319 | return substitute(idTemplate, object); 320 | } 321 | }); 322 | }else{ 323 | Object.defineProperty(facetSchema, "getId", { 324 | value: function(object){ 325 | return object.id; 326 | } 327 | }); 328 | } 329 | } 330 | var facetPrototype = facetSchema.prototype; 331 | var needSourceParameter = {}; 332 | for(var i in facetPrototype){ 333 | var value = facetPrototype[i]; 334 | if(typeof value == "function"){ 335 | var paramsBeforeSource = value.toString().match(/function \(([\w0-9_$, ]*)source[\),]/); 336 | if(paramsBeforeSource){ 337 | needSourceParameter[i] = paramsBeforeSource[1].split(",").length - 1; 338 | } 339 | } 340 | } 341 | var splice = Array.prototype.splice; 342 | return function createWrap(){ 343 | return function wrap(source, transaction, wrapped, partial){ 344 | return when(source, function(source){ 345 | if(!source || typeof source !== "object"){ 346 | return source; 347 | } 348 | if(source instanceof Array){ 349 | if(source.observe){ 350 | // if event emitter, just return it 351 | return source; 352 | } 353 | // this handles query results, but probably should create a branch for real arrays 354 | var results = LazyArray({ 355 | some: function(callback){ 356 | return source.some(function(item){ 357 | callback((item && typeof item == "object" && wrap(item, transaction, item, true)) || item); 358 | }); 359 | }, 360 | length: source.length 361 | }); 362 | results.totalCount = source.totalCount; 363 | return results; 364 | } 365 | var instancePrototype = Object.create(facetPrototype); 366 | defineProperties(instancePrototype, { 367 | load: { 368 | value: function(){ 369 | var loadingSource; 370 | 371 | if(facetSchema.allowed && !facetSchema.allowed(transaction.request, source)){ 372 | throw new AccessError("Access denied to " + source); 373 | } 374 | if(source.load && this != source){ 375 | loadingSource = source.load(); 376 | } 377 | else{ 378 | loadingSource = sourceClass.get(facetSchema.getId(source)); 379 | } 380 | return when(loadingSource, function(loadingSource){ 381 | source = loadingSource; 382 | copyFromSource(); 383 | loaded(); 384 | return wrapped; 385 | }); 386 | }, 387 | enumerable: false, 388 | writable: true 389 | } 390 | }); 391 | if(partial !== true){ 392 | loaded(); 393 | } 394 | function loaded(){ 395 | defineProperties(instancePrototype,{ 396 | save: { 397 | value: function(directives){ 398 | var i, id; 399 | 400 | directives = directives || {}; 401 | if(this != source){ 402 | directives.previous = copy(source, {}); 403 | } 404 | if(facetPrototype.save){ 405 | facetPrototype.save.call(this, directives); 406 | } 407 | var validation = validate(this, facetSchema); 408 | var instance = this; 409 | for(i in this){ 410 | if(this.hasOwnProperty(i)){ 411 | transfer(this[i]); 412 | } 413 | } 414 | for (i in source){ 415 | if(source.hasOwnProperty(i) && !this.hasOwnProperty(i)){ 416 | transfer(undefined); 417 | } 418 | } 419 | mustBeValid(validation); 420 | var isNew = partial === NEW; 421 | if(isNew && (typeof facetSchema.add === "function")){ // || ) 422 | partial = undefined; 423 | id = facetSchema.add(source, directives); 424 | } 425 | else if(typeof facetSchema.put === "function"){ 426 | if(isNew){ 427 | directives.overwrite = false; 428 | } 429 | id = facetSchema.put(source, directives); 430 | } 431 | else if(permissive && isNew && typeof sourceClass.add === "function"){ 432 | id = sourceClass.add(source, directives); 433 | } 434 | else if(permissive && typeof sourceClass.put === "function"){ 435 | if(isNew){ 436 | directives.overwrite = false; 437 | } 438 | id = sourceClass.put(source, directives); 439 | } 440 | else{ 441 | throw new MethodNotAllowedError("put is not allowed"); 442 | } 443 | var self = this; 444 | /*if(typeof id == "string" || typeof id == "number"){ 445 | source.id = id; 446 | }*/ 447 | return when(id, function(id){ 448 | if(isNew){ 449 | if((typeof id == "string" || typeof id == "number") && promiseModule.currentContext){ 450 | promiseModule.currentContext.generatedId = id; 451 | } 452 | 453 | } 454 | copyFromSource(); 455 | return self; 456 | }); 457 | function transfer(value){ 458 | var propDef = properties && properties[i]; 459 | propDef = propDef || facetSchema.additionalProperties; 460 | var cancelled; 461 | if(propDef){ 462 | if(propDef.blocked){ 463 | addError("can't save a blocked property"); 464 | } 465 | if(propDef["transient"]){ 466 | cancelled = true; 467 | } 468 | if(source[i] !== value){ 469 | if(propDef.set){ 470 | try{ 471 | var newValue = propDef.set.call(instance, value, source, source[i]); 472 | if(newValue !== undefined){ 473 | value = newValue; 474 | } 475 | }catch(e){ 476 | addError(e.message); 477 | } 478 | } 479 | else if(propDef.get){ 480 | cancelled = true; 481 | } 482 | else if(propDef.readonly && source.hasOwnProperty(i)){ 483 | addError("property is read only"); 484 | } 485 | 486 | } 487 | } 488 | if(!cancelled){ 489 | if(value === undefined){ 490 | delete source[i]; 491 | } 492 | else{ 493 | source[i] = value; 494 | } 495 | } 496 | function addError(message){ 497 | validation.valid = false; 498 | validation.errors.push({property: i, message: message}); 499 | cancelled = true; 500 | } 501 | } 502 | }, 503 | enumerable: false, 504 | writable: true 505 | }, 506 | load: { 507 | value: function(){ 508 | if(typeof source.load === "function"){ 509 | source.load(); 510 | } 511 | copyFromSource(); 512 | return wrapped; 513 | }, 514 | enumerable: false, 515 | writable: true 516 | }, 517 | schema: { 518 | get: function(){ 519 | var copyOfSchema = copy(facetSchema, {}); 520 | copyOfSchema.schema = facetSchema.schema; 521 | copyOfSchema.getId = facetSchema.getId; 522 | return copyOfSchema; 523 | }, 524 | enumerable: false 525 | } 526 | }); 527 | } 528 | function copyFromSource(){ 529 | var i, propDef; 530 | 531 | for(i in source){ 532 | if(source.hasOwnProperty(i) && i != "schema"){ 533 | propDef = properties && properties[i]; 534 | if(!(propDef && propDef.blocked)){ 535 | wrapped[i] = source[i]; 536 | } 537 | } 538 | } 539 | for(i in properties){ 540 | propDef = properties[i]; 541 | if(propDef.get){ 542 | wrapped[i] = propDef.get.call(source, i); 543 | } 544 | } 545 | } 546 | for(var i in needSourceParameter){ 547 | // splice in the source argument for each method that needs it 548 | (function(param, protoFunc, i){ 549 | instancePrototype[i] = function(){ 550 | splice.call(arguments, param, 0, source); 551 | return protoFunc.apply(this, arguments); 552 | }; 553 | })(needSourceParameter[i], facetPrototype[i], i); 554 | } 555 | if(writableProto && partial === true){ 556 | source.__proto__ = instancePrototype; 557 | wrapped = source; 558 | } 559 | else{ 560 | if(wrapped){ 561 | wrapped.__proto__ = instancePrototype; 562 | } 563 | else{ 564 | wrapped = Object.create(instancePrototype); 565 | } 566 | if(partial !== NEW){ 567 | copyFromSource(); 568 | } 569 | } 570 | if(facetSchema.onWrap){ 571 | wrapped = facetSchema.onWrap(wrapped) || wrapped; 572 | } 573 | return wrapped; 574 | }); 575 | }; 576 | }; 577 | }; 578 | function canFacetBeAppliedTo(appliesTo, store){ 579 | store = store._baseFacetedStore || store; 580 | if(appliesTo && appliesTo != Object){ 581 | while(store != appliesTo){ 582 | store = store["extends"]; 583 | if(!store){ 584 | return false; 585 | } 586 | } 587 | } 588 | return true; 589 | } 590 | 591 | /** 592 | * Finds the best facet for the given store from the list of provided facets 593 | */ 594 | exports.findBestFacet = function(store, facets){ 595 | var allInstances = Facet.instances; 596 | // TODO: we may need to index of id for base stores since there can be multiple 597 | // instances generated from a database 598 | store = store._baseFacetedStore || store; 599 | var bestFacet, facet, index, allIndex = -1; 600 | while(true){ 601 | while((allIndex = appliesTos.indexOf(store, allIndex + 1)) > -1){ 602 | if((index = facets.indexOf(allInstances[allIndex])) > -1){ 603 | facet = facets[index]; 604 | if(!bestFacet || (facet.quality > (bestFacet.quality || 0.001))){ 605 | bestFacet = facet; 606 | } 607 | } 608 | } 609 | if(store == Object){ 610 | break; 611 | } 612 | store = store["extends"] || Object; 613 | } 614 | return bestFacet; 615 | }; 616 | 617 | 618 | function Facet(appliesTo, schema, permissive){ 619 | var facetedStore, baseFacetedStore = FacetedStore(appliesTo, schema); 620 | var createWrap = SchemaControlled(schema, appliesTo, permissive); 621 | baseFacetedStore.wrap = createWrap(baseFacetedStore); 622 | function FacetForStore(sourceStore, transaction){ 623 | if(!canFacetBeAppliedTo(appliesTo, sourceStore)){ 624 | throw new TypeError("facet can not be applied to " + sourceStore.name); 625 | } 626 | if(appliesTo == sourceStore){ 627 | facetedStore = function(){ 628 | return facetedStore.construct.apply(facetedStore, arguments); 629 | }; 630 | facetedStore.__proto__ = baseFacetedStore; 631 | facetedStore.wrap = createWrap(facetedStore); 632 | } 633 | else{ 634 | facetedStore = FacetedStore(sourceStore, schema, permissive); 635 | facetedStore.wrap = SchemaControlled(schema, sourceStore, permissive)(facetedStore); 636 | } 637 | facetedStore.transaction = transaction; 638 | return facetedStore; 639 | } 640 | baseFacetedStore.forStore = FacetForStore; 641 | baseFacetedStore._baseFacetedStore = baseFacetedStore; 642 | Facet.instances.push(baseFacetedStore); 643 | appliesTos.push(appliesTo || Object); 644 | return baseFacetedStore; 645 | } 646 | var appliesTos = []; 647 | Facet.instances = []; 648 | 649 | exports.Restrictive = function(appliesTo, schema){ 650 | schema = schema || {quality:0.2}; 651 | 652 | var appliesToPrototype = appliesTo.prototype; 653 | if(appliesToPrototype){ 654 | var schemaPrototype = schema.prototype = schema.prototype || {}; 655 | schemaPrototype.__noSuchMethod__ = function(name, source, args, onlyIfAvailable){ 656 | if(name.substring(0,3) === "get"){ 657 | if(appliesToPrototype[name]){ 658 | return facet.wrap(appliesToPrototype[name].apply(source, args)); 659 | } 660 | if(appliesToPrototype.__noSuchMethod__){ 661 | return facet.wrap(source.__noSuchMethod__(name, args, onlyIfAvailable)); 662 | } 663 | } 664 | if(!onlyIfAvailable){ 665 | throw new MethodNotAllowedError(name + " is not allowed"); 666 | } 667 | return null; 668 | }; 669 | if(appliesToPrototype.get){ 670 | schemaPrototype.get = DELEGATE; 671 | } 672 | } 673 | var facet = Facet(appliesTo, schema); 674 | if(!schema.query){ 675 | facet.query = function(query, options){ 676 | facet.checkQuery(query); 677 | return appliesTo.query(query, options); 678 | }; 679 | } 680 | for(var i in appliesTo){ 681 | if(!facet[i] && i.substring(0,3) == "get"){ 682 | (function(i){ 683 | facet[i] = function(){ 684 | return facet.wrap(appliesTo[i].apply(appliesTo, arguments)); 685 | }; 686 | })(i); 687 | } 688 | } 689 | return facet; 690 | 691 | }; 692 | var DELEGATE = function(){}; 693 | exports.Permissive = function(appliesTo, schema){ 694 | schema = schema || {quality:0.5}; 695 | var appliesToPrototype = appliesTo.prototype; 696 | if(appliesToPrototype){ 697 | var schemaPrototype = schema.prototype = schema.prototype || {}; 698 | schemaPrototype.__noSuchMethod__ = function(name, source, args, onlyIfAvailable){ 699 | if(appliesToPrototype[name]){ 700 | return facet.wrap(appliesToPrototype[name].apply(source, args)); 701 | } 702 | if(appliesToPrototype.__noSuchMethod__){ 703 | return facet.wrap(source.__noSuchMethod__(name, args, onlyIfAvailable)); 704 | } 705 | if(!onlyIfAvailable){ 706 | throw new MethodNotAllowedError(name + " is not allowed"); 707 | } 708 | return null; 709 | }; 710 | if(appliesToPrototype.get){ 711 | schemaPrototype.get = DELEGATE; 712 | } 713 | } 714 | var facet = Facet(appliesTo, schema, true); 715 | for(var i in appliesTo){ 716 | if(!facet[i]){ 717 | (function(i){ 718 | facet[i] = function(){ 719 | return facet.wrap(appliesTo[i].apply(appliesTo, arguments)); 720 | }; 721 | })(i); 722 | } 723 | } 724 | return facet; 725 | }; 726 | 727 | exports.callMethod = function(object, name, args, onlyIfAvailable){ 728 | if(object[name]){ 729 | return object[name].apply(object, args); 730 | } 731 | if(object.__noSuchMethod__){ 732 | return object.__noSuchMethod__(name, args, onlyIfAvailable); 733 | } 734 | if(!onlyIfAvailable){ 735 | throw new MethodNotAllowedError(name + " is not allowed"); 736 | } 737 | 738 | }; 739 | --------------------------------------------------------------------------------