This commit is contained in:
Matthias Janetschek
2015-06-05 15:58:53 +02:00
parent 8fc57cc92e
commit f925e43d1f
85 changed files with 11613 additions and 394 deletions
@@ -85,52 +85,12 @@ DeviceRepoHandlerFactory::DeviceRepoHandlerFactory(devicerepository::DeviceRepos
ss << "<???>";
return ss.str();
} });
io_service_thread.reset(new std::thread([this] {
try {
std::cout << "Boost IO service starting" << std::endl;
boost::asio::io_service::work work(io_service);
this->io_service.run();
std::cout << "Boost IO service stopped" << std::endl;
} catch (std::exception& e) {
ROS_ERROR_STREAM("IO-Service thread caught exception: " << e.what());
} catch(...) {
ROS_ERROR("IO-Service thread caught unknown exception");
}
}));
}
DeviceRepoHandlerFactory::~DeviceRepoHandlerFactory() {
if (io_service_thread) {
this->io_service.stop();
io_service_thread->join();
}
}
Net::HTTPRequestHandler* DeviceRepoHandlerFactory::createRequestHandler( Poco::URI matchUri, const Net::HTTPServerRequest& request, bool ssl ) noexcept {
return new DeviceRepoHandler( this, matchUri, repoService, ssl);
}
std::shared_ptr<DeviceEventQueue> DeviceRepoHandlerFactory::getDeviceEventQueue( const std::string& clientID ) {
if (clientID.empty()) {
return std::make_shared<DeviceEventQueue>(clientID, repoService, this);
} else {
auto i = deviceEventQueues.find(clientID);
if (i == deviceEventQueues.end()) {
auto n = std::make_shared<DeviceEventQueue>(clientID, repoService, this);
deviceEventQueues.insert({clientID, n});
return n;
} else {
return i->second;
}
}
}
void DeviceRepoHandlerFactory::deleteDeviceEventQueue( const std::string& clientID ) {
deviceEventQueues.erase(clientID);
}
void DeviceRepoHandler::_handleRequest(Poco::Net::HTTPServerRequest &req, Poco::Net::HTTPServerResponse &resp) {
if(req.find("Upgrade") != req.end() && Poco::icompare(req["Upgrade"], "websocket") == 0) {
@@ -359,7 +319,8 @@ void DeviceRepoHandler::__handleEventLongPolling(Poco::Net::HTTPServerRequest &r
connCheckTimer.async_wait(connCheckFun);
try {
msg = eventQueue->popEvent();
} catch (DeviceEventQueue::DisconnectedException& e) {
} catch (DeviceEventQueue::TimeoutException& e) {
msg["eventtype"] = "timeoutevent";
}
{
std::lock_guard<std::mutex> _l(_m);
@@ -503,7 +464,7 @@ void DeviceRepoHandler::__handleEvent(Poco::Net::HTTPServerRequest &req, Poco::N
path << tmp;
}
// Get event queue
auto eventQueue = parent->getDeviceEventQueue(clientID);
auto eventQueue = parent->getEventQueue(clientID, repoService);
if (!eventQueue->setHandler(this, &req, &resp)) {
throw webserver::webresource::WebResourceHandlerException((boost::format("Client id \"%1%\" already in use.") % devid).str(),
Poco::Net::HTTPResponse::HTTPStatus::HTTP_BAD_REQUEST);
@@ -530,22 +491,18 @@ void DeviceRepoHandler::__handleEvent(Poco::Net::HTTPServerRequest &req, Poco::N
DeviceEventQueue::DeviceEventQueue( const std::string& clientID, DeviceRepositoryService* repoService, DeviceRepoHandlerFactory* handlerFactory )
: clientID(clientID), repoService(repoService), handlerFactory(handlerFactory), timer(handlerFactory->getIOService()) {
std::cout << "DeviceEventQueue \"" << clientID << "\" created" << std::endl;
}
void DeviceEventQueue::checkRegistration(DeviceEventType eventType, Device* device, const std::string& pointer) {
if (!registeredToEvent || eventType != this->eventType || device != this->device) {
unregisterEvent();
registerEvent(eventType, device);
unregisterEvents();
registerEvents(eventType, device);
}
if (this->pointer != pointer) {
this->pointer = pointer;
}
}
void DeviceEventQueue::registerEvent(DeviceEventType eventType, Device* device) {
void DeviceEventQueue::registerEvents(DeviceEventType eventType, Device* device) {
std::lock_guard<std::mutex> lock(_mutex);
if (!registeredToEvent) {
if (device) {
@@ -559,8 +516,9 @@ void DeviceEventQueue::registerEvent(DeviceEventType eventType, Device* device)
}
}
void DeviceEventQueue::unregisterEvent() {
void DeviceEventQueue::unregisterEvents() {
std::lock_guard<std::mutex> lock(_mutex);
std::cout << "DeviceEventQueue::unregisterEvents" << std::endl;
if (registeredToEvent) {
if (device) {
device->deleteObserver(this);
@@ -571,73 +529,6 @@ void DeviceEventQueue::unregisterEvent() {
}
}
void DeviceEventQueue::pushEvent(const libvariant::Variant& event) {
{
std::lock_guard<std::mutex> lock(_mutex);
_eventQueue.push(event);
}
_cv.notify_one();
}
libvariant::Variant DeviceEventQueue::popEvent() {
std::unique_lock<std::mutex> lock(_mutex);
libvariant::Variant retval;
breakPopLoop = false;
Net::HTTPServerRequestImpl* reqImpl = static_cast<Net::HTTPServerRequestImpl*>(req);
// I don't like the content of this loop at all.
while (_eventQueue.empty() && !breakPopLoop) {
_cv.wait_for(lock, std::chrono::seconds(1));
// Monitor the socket for a very small time with Posix "select".
// If select says that there is data, available() returns 0 then apparently the client has disconnected.
// WTF, is this really the only way to notice that a client properly disconnected? Internet says so.
/*fd_set fds;
auto sockfs = reqImpl->socket().impl()->sockfd();
FD_ZERO(&fds);
FD_SET(sockfs, &fds);
struct timeval timeout = {0, 1};
int sel = select(sockfs + 1, &fds, 0, 0, &timeout);
std::cout << "Debug01: " << sel << " - " << reqImpl->socket().available() << " - " << sockfs << std::endl;
if (sel > 0 && reqImpl->socket().available() == 0) {
ROS_ERROR_STREAM("Apparently we have a disconnect in device event queue \"" << clientID << "\"");
throw DisconnectedException();
}*/
}
if (!_eventQueue.empty() && !breakPopLoop) {
retval = _eventQueue.front();
_eventQueue.pop();
}
return retval;
}
bool DeviceEventQueue::setHandler(DeviceRepoHandler* handler, Poco::Net::HTTPServerRequest* req, Poco::Net::HTTPServerResponse* resp) {
if (openConnectionCount <= 0) {
this->handler = handler;
this->req = req;
this->resp =resp;
openConnectionCount++;
timer.cancel();
return true;
}
return false;
}
void DeviceEventQueue::unsetHandler() {
this->handler = nullptr;
this->req = nullptr;
this->resp =nullptr;
openConnectionCount--;
if (openConnectionCount <= 0) {
std::cout << "DeviceEventQueue \"" << clientID << "\" has no open connections." << std::endl;
timer.expires_from_now(boost::posix_time::seconds(10));
timer.async_wait([this](const boost::system::error_code& error) {
if (error != boost::asio::error::operation_aborted) {
handlerFactory->deleteDeviceEventQueue(clientID);
}
});
}
}
void DeviceEventQueue::notificationReceived( interface::Observer<devicerepository::DeviceEventType, devicerepository::DeviceEvent>::observable_type* o,
const devicerepository::DeviceEventType& key,
const devicerepository::DeviceEvent* e) noexcept {
@@ -787,141 +678,6 @@ DeviceAPIMethod DeviceRepoHandler::getDeviceAPIMethod(Net::HTTPServerRequest &re
/*
void DeviceEventQueue::notificationReceived( interface::Observer<devicerepository::DeviceEventType,
devicerepository::DeviceEvent>::observable_type* o,
const devicerepository::DeviceEventType& key,
const devicerepository::DeviceEvent* e) noexcept {
if (eventType == DeviceEventType::ALL_EVENTS || eventType == key) {
switch (key) {
case devicerepository::DeviceEventType::COMMAND_RECEIVED:
break;
case devicerepository::DeviceEventType::STATUS_UPDATE_RECEIVED:
break;
case devicerepository::DeviceEventType::STATE_CHANGED:
break;
case devicerepository::DeviceEventType::DEVICE_ADDED:
break;
case devicerepository::DeviceEventType::DEVICE_REMOVED:
break;
default:
break;
}
}
}
Net::HTTPRequestHandler* DeviceRepoHandlerFactory::createRequestHandler( Poco::URI matchUri, const Net::HTTPServerRequest& request, bool ssl ) noexcept {
if(request.find("Upgrade") != request.end() && Poco::icompare(request["Upgrade"], "websocket") == 0) {
return new DeviceRepoEventHandler( matchUri, DeviceAPIMethod::EVENT, repoService, ssl, true);
} else {
if (request.getMethod() == Net::HTTPRequest::HTTP_PUT) {
return new DeviceRepoCommandHandler(matchUri, DeviceAPIMethod::UNKNOWN, repoService, ssl);
} else if (request.getMethod() == Net::HTTPRequest::HTTP_GET || request.getMethod() == Net::HTTPRequest::HTTP_POST) {
DeviceAPIMethod method = getDeviceAPIMethod(request);
if (method == DeviceAPIMethod::GET) {
return new DeviceRepoInfoHandler( matchUri, method, repoService, infoCategoryFuncs, ssl );
} else if (method == DeviceAPIMethod::COMMAND || method == DeviceAPIMethod::COMMAND) {
return new DeviceRepoCommandHandler(matchUri, method, repoService, infoCategoryFuncs, ssl);
} else if (method == DeviceAPIMethod::EVENT) {
return new DeviceRepoEventHandler( matchUri, method, repoService, infoCategoryFuncs, ssl, false);
}
}
}
return service->createErrorHandler(request, ssl, "", Net::HTTPResponse::HTTP_BAD_REQUEST);
}
DeviceRepoHandlerFactory::DeviceRepoHandlerFactory(devicerepository::DeviceRepositoryService* repoService)
: repoService(repoService) {
infoCategoryFuncs.insert({"id", [](Poco::URI& matchUri, devicerepository::Device* device, Poco::Net::HTTPServerRequest &req, const std::string& category, bool ssl)->Variant {
return device->getDeviceID();
} });
infoCategoryFuncs.insert({"type", [](Poco::URI& matchUri, devicerepository::Device* device, Poco::Net::HTTPServerRequest &req, const std::string& category, bool ssl)->Variant {
return device->getDeviceType();
} });
infoCategoryFuncs.insert({"status_topic", [](Poco::URI& matchUri, devicerepository::Device* device, Poco::Net::HTTPServerRequest &req, const std::string& category, bool ssl)->Variant {
return device->getStatusTopic();
} });
infoCategoryFuncs.insert({"command_topic", [](Poco::URI& matchUri, devicerepository::Device* device, Poco::Net::HTTPServerRequest &req, const std::string& category, bool ssl)->Variant {
return device->getCmdTopic();
} });
infoCategoryFuncs.insert({"current_state", [](Poco::URI& matchUri, devicerepository::Device* device, Poco::Net::HTTPServerRequest &req, const std::string& category, bool ssl)->Variant {
auto retval = device->getCurrentState();
if (retval.IsMap()) {
retval["timestamp"] = utils::timestamp_to_ISO8601(device->getCurrentStateTimestamp());
}
return retval;
} });
infoCategoryFuncs.insert({"last_state", [](Poco::URI& matchUri, devicerepository::Device* device, Poco::Net::HTTPServerRequest &req, const std::string& category, bool ssl)->Variant {
auto retval = device->getLastState();
if (retval.IsMap()) {
retval["timestamp"] = utils::timestamp_to_ISO8601(device->getLastStateTimestamp());
}
return retval;
} });
infoCategoryFuncs.insert({"uri", [=](Poco::URI& matchUri, devicerepository::Device* device, Poco::Net::HTTPServerRequest &req, const std::string& category, bool ssl)->Variant {
std::stringstream ss;
if (ssl) {
ss << "https://";
} else {
ss << "http://";
}
ss << req.getHost() << matchUri.toString() << device->getDeviceID();
return ss.str();
} });
}
DeviceAPIMethod DeviceRepoHandlerFactory::getDeviceAPIMethod(Net::HTTPServerRequest &req) noexcept {
Net::HTMLForm form;
DeviceAPIMethod retval = DeviceAPIMethod::GET; // default
if (req.getMethod() == Net::HTTPRequest::HTTP_POST && req.has("Content-type")
&& req.get("Content-type") == Net::HTMLForm::ENCODING_URL) {
form = Net::HTMLForm(req, req.stream());
} else {
form = Net::HTMLForm(req);
}
std::string methodRaw;
if (form.has("method")) {
methodRaw = form.has("method");
} else if (req.has("API-Method")) {
methodRaw = req.has("API-Method");
} else if (req.has("X-API-Method")) {
methodRaw = req.has("X-API-Method");
}
if (!methodRaw.empty()) {
if (methodRaw == "command") {
retval = DeviceAPIMethod::COMMAND;
} else if (methodRaw == "statusupdate") {
retval = DeviceAPIMethod::STATUSUPDATE;
} else if (methodRaw == "event") {
retval = DeviceAPIMethod::EVENT;
} else {
throw webserver::webresource::WebResourceHandlerException(
(boost::format("Unknown device api method \"%1%\"") % methodRaw).str(),
Net::HTTPResponse::HTTPStatus::HTTP_BAD_REQUEST );
}
} else if (form.has("event")) {
retval = DeviceAPIMethod::EVENT;
}
return retval;
}
*/
} // namespace resources
} // namespace webserver
} // namespace service
@@ -9,14 +9,6 @@
#include <Poco/Net/HTTPRequestHandler.h>
#include <Variant/Variant.h>
#include <ros/ros.h>
#include <hos/cosgi/service/Implementation.h>
#include <hos/cosgi/service/webserver/WebServerService.h>
#include <hos/cosgi/service/webserver/webresource/WebResourceHandlerImpl.h>
#include <hos/cosgi/service/devicerepository/DeviceRepositoryService.h>
#include <Poco/Net/HTTPRequestHandler.h>
#include <Variant/Variant.h>
#include <vector>
#include <string>
#include <queue>
@@ -50,55 +42,31 @@ enum class DeviceAPIMethod {
};
typedef webserver::webresource::WebResourceEventQueue<
interface::Observer<devicerepository::DeviceEventType, devicerepository::DeviceEvent>,
DeviceRepoHandlerFactory,
DeviceRepoHandler
> DeviceEventQueueBase;
class DeviceEventQueue
: public virtual interface::Observer<devicerepository::DeviceEventType, devicerepository::DeviceEvent> {
: public DeviceEventQueueBase {
public:
class DisconnectedException : public std::exception {};
const std::string clientID;
DeviceEventType eventType = DeviceEventType::ALL_EVENTS;
DeviceRepositoryService* const repoService;
Device* device = nullptr;
std::string pointer;
bool registeredToEvent = false;
size_t openConnectionCount = 0;
bool breakPopLoop = false;
DeviceRepoHandler* handler = nullptr;
DeviceRepoHandlerFactory* handlerFactory;
Poco::Net::HTTPServerRequest* req = nullptr;
Poco::Net::HTTPServerResponse* resp = nullptr;
std::queue<libvariant::Variant> _eventQueue;
std::mutex _mutex;
std::condition_variable _cv;
boost::asio::deadline_timer timer;
DeviceEventQueue( const std::string& clientID,
DeviceRepositoryService* repoService,
DeviceRepoHandlerFactory* handlerFactory );
void pushEvent(const libvariant::Variant& event);
libvariant::Variant popEvent();
void cancelPopEvent() {
breakPopLoop = true;
_cv.notify_all();
DeviceRepoHandlerFactory* handlerFactory,
DeviceRepositoryService* repoService )
: WebResourceEventQueue(clientID, handlerFactory), repoService(repoService) {}
virtual ~DeviceEventQueue() {
unregisterEvents();
}
bool setHandler(DeviceRepoHandler* handler, Poco::Net::HTTPServerRequest* req, Poco::Net::HTTPServerResponse* resp);
void unsetHandler();
void checkRegistration(DeviceEventType eventType, Device* device, const std::string& pointer);
void registerEvent(DeviceEventType eventType, Device* device = nullptr);
void unregisterEvent();
~DeviceEventQueue() {
unregisterEvent();
std::cout << "DeviceEventQueue \"" << clientID << "\" destroyed" << std::endl;
}
void registerEvents(DeviceEventType eventType, Device* device = nullptr);
void unregisterEvents();
virtual void notificationReceived( interface::Observer<devicerepository::DeviceEventType, devicerepository::DeviceEvent>::observable_type* o,
const devicerepository::DeviceEventType& key,
@@ -107,42 +75,19 @@ public:
class DeviceRepoHandlerFactory
: public virtual webserver::WebResourceHandlerFactory,
public implementation::Service<DeviceRepoHandlerFactory> {
: public webserver::webresource::WebResourceHandlerFactoryImpl<DeviceRepoHandlerFactory, false, DeviceEventQueue> {
public:
typedef std::function<libvariant::Variant( devicerepository::Device* device, const std::string& category )> infoCategoryFunc_t;
private:
webserver::WebServerService* service = nullptr;
DeviceRepositoryService* repoService;
std::map<std::string, infoCategoryFunc_t> infoCategoryFuncs;
std::map<std::string, std::shared_ptr<DeviceEventQueue>> deviceEventQueues;
boost::asio::io_service io_service;
std::unique_ptr<std::thread> io_service_thread;
public:
DeviceRepoHandlerFactory(devicerepository::DeviceRepositoryService* repoService);
~DeviceRepoHandlerFactory();
virtual Poco::Net::HTTPRequestHandler* createRequestHandler(Poco::URI matchUri, const Poco::Net::HTTPServerRequest& request, bool ssl ) noexcept override;
virtual void setWebServerService(webserver::WebServerService* service) noexcept override {
this->service = service;
}
virtual bool excludeFromAuthentication() noexcept override {
return false;
}
boost::asio::io_service& getIOService() {
return io_service;
}
std::shared_ptr<DeviceEventQueue> getDeviceEventQueue( const std::string& clientID );
void deleteDeviceEventQueue( const std::string& clientID );
libvariant::Variant getDeviceInfo( devicerepository::Device* device,
const std::string& category = std::string(),
const std::string& path = std::string() );
@@ -189,64 +134,6 @@ public:
};
/*
class DeviceRepoInfoHandler
: public DeviceRepoHandlerBase {
protected:
const std::map<std::string, DeviceRepoHandlerFactory::infoCategoryFunc_t>& infoCategoryFuncs;
public:
DeviceRepoInfoHandler( Poco::URI matchUri,
DeviceAPIMethod method,
devicerepository::DeviceRepositoryService* repoService,
const std::map<std::string, DeviceRepoHandlerFactory::infoCategoryFunc_t>& infoCategoryFuncs,
bool ssl = false )
: DeviceRepoHandlerBase(matchUri, method, repoService, ssl), infoCategoryFuncs(infoCategoryFuncs) {}
virtual void _handleRequest(Poco::Net::HTTPServerRequest &req, Poco::Net::HTTPServerResponse &resp) override;
libvariant::Variant getDeviceInfo( devicerepository::Device* device,
Poco::Net::HTTPServerRequest &req,
const std::string& category = std::string(),
const std::vector<std::string>& path = std::vector<std::string>() );
};
class DeviceRepoCommandHandler
: public DeviceRepoHandlerBase {
public:
DeviceRepoCommandHandler( Poco::URI matchUri,
DeviceAPIMethod method,
devicerepository::DeviceRepositoryService* repoService,
bool ssl = false )
: DeviceRepoHandlerBase(matchUri, method, repoService, ssl) {}
virtual void _handleRequest(Poco::Net::HTTPServerRequest &req, Poco::Net::HTTPServerResponse &resp) override;
void sendDeviceCommand( devicerepository::Device* device,
Poco::Net::HTTPServerRequest &req,
const std::string& cmd,
const std::vector<std::string>& path = std::vector<std::string>() );
};
class DeviceRepoEventHandler
: public DeviceRepoHandlerBase {
protected:
bool webSocketConnection;
public:
DeviceRepoEventHandler( Poco::URI matchUri,
DeviceAPIMethod method,
devicerepository::DeviceRepositoryService* repoService,
bool ssl = false,
bool webSocketConnection = false)
: DeviceRepoHandlerBase(matchUri, method, repoService, ssl), webSocketConnection(webSocketConnection) {}
virtual void _handleRequest(Poco::Net::HTTPServerRequest &req, Poco::Net::HTTPServerResponse &resp) override;
};
*/
} // namespace webresource
} // namespace devicerepository
+1
View File
@@ -8,6 +8,7 @@ find_package(catkin REQUIRED COMPONENTS
hos_cosgi_core
hos_cosgi_scripting
hos_cpp_utils
hos_cosgi_webserver
hos_deps
roscpp
hos_cpp_cron
@@ -6,6 +6,7 @@
#include <hos/cosgi/service/Interface.h>
#include <string>
#include <vector>
#include <chrono>
#include <memory>
@@ -57,11 +58,39 @@ public:
enum class RuleEngineEventType : int {
RULEPKG_ADDED,
RULEPKG_REMOVED,
RULEPKG_UPDATED,
RULEPKG_STATE_CHANGED,
RULE_ADDED,
RULE_REMOVED,
RULE_UPDATED,
RULE_STATE_CHANGED,
ALL_EVENTS,
CHANGE_EVENTS
};
class RuleEngineEvent {
public:
typedef std::chrono::system_clock::time_point timestamp_t;
RuleEngineEventType eventType;
const timestamp_t timestamp;
RuleEngineEvent(const RuleEngineEventType& type, const timestamp_t& timestamp)
: eventType(type), timestamp(timestamp) {}
};
/**
* Public COSGI interface of the rule engine service.
*/
class RuleEngineService
: public virtual interface::Service {
: public virtual interface::ObservableService<RuleEngineEventType, RuleEngineEvent> {
public:
+1
View File
@@ -48,6 +48,7 @@
<build_depend>hos_deps</build_depend>
<build_depend>roscpp</build_depend>
<build_depend>hos_cpp_cron</build_depend>
<build_depend>hos_cosgi_webserver</build_depend>
<run_depend>hos_celix</run_depend>
<run_depend>hos_cosgi_devicerepository</run_depend>
<run_depend>hos_cosgi_core</run_depend>
@@ -22,6 +22,21 @@
#include <string>
namespace std {
/**
* Specialization of std::hash for RuleEngineEventType.
*
* Required by std::map.
*/
template <>
class hash <hos::cosgi::service::ruleengine::RuleEngineEventType>{
public :
size_t operator()(const hos::cosgi::service::ruleengine::RuleEngineEventType& x) const {
return static_cast<size_t>(x);
}
};
} // namespace std
// Service Namespace
namespace hos {
@@ -40,9 +55,11 @@ class RulePackageImpl;
/**
* Implementation of RuleEngineService.
**/
class RuleEngineServiceImpl :
public virtual RuleEngineService,
public implementation::Service<RuleEngineServiceImpl> {
class RuleEngineServiceImpl
: public virtual RuleEngineService,
public implementation::Service<RuleEngineServiceImpl>,
public implementation::Observable<RuleEngineEventType, RuleEngineEvent> {
//public implementation::ObservableService<DeviceRepositoryServiceImpl, > { /* Why does this one not work? */
private:
typedef std::map<std::string, std::shared_ptr<rules::RulePackageImpl>> rulePkgMap_t;
@@ -0,0 +1,10 @@
/*
* RuleEngineHandler.cpp
*
* Created on: 05.06.2015
* Author: matthias
*/
@@ -0,0 +1,106 @@
#pragma once
#include <hos/cosgi/service/Implementation.h>
#include <hos/cosgi/service/webserver/WebServerService.h>
#include <hos/cosgi/service/webserver/webresource/WebResourceHandlerImpl.h>
#include <hos/cosgi/service/ruleengine/RuleEngineService.h>
#include <Poco/Net/HTTPRequestHandler.h>
#include <Variant/Variant.h>
#include <ros/ros.h>
#include <vector>
#include <string>
#include <queue>
#include <condition_variable>
#include <memory>
#include <thread>
#include <functional>
#include <boost/asio.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
// Service Namespace
namespace hos {
namespace cosgi {
namespace service {
namespace ruleengine {
namespace webresource {
class RuleEngineHandler;
class RuleEngineHandlerFactory;
enum class RuleEngineAPIMethod {
GET,
SET,
EVENT,
UNKNOWN
};
class RuleEngineHandlerFactory
: public virtual webserver::WebResourceHandlerFactory,
public implementation::Service<RuleEngineHandlerFactory> {
private:
webserver::WebServerService* service = nullptr;
RuleEngineService* ruleService;
boost::asio::io_service io_service;
std::unique_ptr<std::thread> io_service_thread;
public:
RuleEngineHandlerFactory(RuleEngineService* ruleService);
~RuleEngineHandlerFactory();
virtual Poco::Net::HTTPRequestHandler* createRequestHandler(Poco::URI matchUri, const Poco::Net::HTTPServerRequest& request, bool ssl ) noexcept override;
virtual void setWebServerService(webserver::WebServerService* service) noexcept override {
this->service = service;
}
virtual bool excludeFromAuthentication() noexcept override {
return false;
}
boost::asio::io_service& getIOService() {
return io_service;
}
};
class RuleEngineHandler
: public webserver::webresource::WebResourceHandlerImpl {
protected:
RuleEngineHandlerFactory* parent;
Poco::URI matchUri;
RuleEngineService* ruleService;
bool ssl;
public:
RuleEngineHandler( RuleEngineHandlerFactory* parent,
Poco::URI matchUri,
RuleEngineService* ruleService,
bool ssl = false )
: parent(parent), matchUri(matchUri), ruleService(ruleService), ssl(ssl) {}
virtual void _handleRequest(Poco::Net::HTTPServerRequest &req, Poco::Net::HTTPServerResponse &resp) override;
static RuleEngineAPIMethod getRuleEngineAPIMethod(Poco::Net::HTTPServerRequest &req) noexcept;
};
} // namespace webresource
} // namespace ruleengine
} // namespace service
} // namespace cosgi
} // namespace hos
@@ -0,0 +1 @@
node_modules/
@@ -0,0 +1,72 @@
{
"asi": false,
"bitwise": false,
"boss": false,
"browser": true,
"camelcase": true,
"couch": false,
"curly": true,
"debug": false,
"devel": true,
"dojo": false,
"eqeqeq": false,
"eqnull": true,
"es3": true,
"esnext": false,
"evil": false,
"expr": true,
"forin": false,
"funcscope": true,
"gcl": false,
"globalstrict": false,
"immed": true,
"iterator": false,
"jquery": false,
"lastsemic": false,
"latedef": false,
"laxbreak": true,
"laxcomma": false,
"loopfunc": true,
"mootools": false,
"moz": false,
"multistr": false,
"newcap": true,
"noarg": true,
"node": false,
"noempty": false,
"nonew": true,
"nonstandard": false,
"nomen": false,
"onecase": false,
"onevar": false,
"passfail": false,
"phantom": false,
"plusplus": false,
"proto": true,
"prototypejs": false,
"regexdash": true,
"regexp": false,
"rhino": false,
"scripturl": true,
"shadow": true,
"shelljs": false,
"smarttabs": true,
"strict": false,
"sub": false,
"supernew": false,
"trailing": true,
"undef": true,
"unused": true,
"validthis": true,
"withstmt": false,
"worker": false,
"wsh": false,
"yui": false,
"maxlen": 140,
"indent": 4,
"maxerr": 250,
"predef": [ "require", "define" ],
"quotmark": "single",
"maxcomplexity": 20
}
@@ -0,0 +1,196 @@
define([
'dojo/_base/array',
'dojo/when',
'dojo/_base/declare',
'dojo/_base/lang',
'./Store',
'./Memory',
'./QueryResults'
], function (arrayUtil, when, declare, lang, Store, Memory, QueryResults) {
// module:
// dstore/Cache
function cachingQuery(type) {
// ensure querying creates a parallel caching store query
return function () {
var subCollection = this.inherited(arguments);
var cachingCollection = this.cachingCollection || this.cachingStore;
subCollection.cachingCollection = cachingCollection[type].apply(cachingCollection, arguments);
subCollection.isValidFetchCache = this.canCacheQuery === true || this.canCacheQuery(type, arguments);
return subCollection;
};
}
function init (store) {
if (!store.cachingStore) {
store.cachingStore = new Memory();
}
store.cachingStore.Model = store.Model;
store.cachingStore.idProperty = store.idProperty;
}
var CachePrototype = {
cachingStore: null,
constructor: function () {
init(this);
},
canCacheQuery: function (method, args) {
// summary:
// Indicates if a queried (filter, sort, etc.) collection should using caching
return false;
},
isAvailableInCache: function () {
// summary:
// Indicates if the collection's cachingCollection is a viable source
// for a fetch
return (this.isValidFetchCache && (this.allLoaded || this.fetchRequest)) ||
this._parent && this._parent.isAvailableInCache();
},
fetch: function () {
return this._fetch(arguments);
},
fetchRange: function () {
return this._fetch(arguments, true);
},
_fetch: function (args, isRange) {
// if the data is available in the cache (via any parent), we use fetch from the caching store
var cachingStore = this.cachingStore;
var cachingCollection = this.cachingCollection || cachingStore;
var store = this;
var available = this.isAvailableInCache();
if (available) {
return new QueryResults(when(available, function () {
// need to double check to make sure the flag hasn't been cleared
// and we really have all data loaded
if (store.isAvailableInCache()) {
return isRange ?
cachingCollection.fetchRange(args[0]) :
cachingCollection.fetch();
} else {
return store.inherited(args);
}
}));
}
var results = this.fetchRequest = this.inherited(args);
when(results, function (results) {
var allLoaded = !isRange;
store.fetchRequest = null;
// store each object before calling the callback
arrayUtil.forEach(results, function (object) {
// store each object before calling the callback
if (!store.isLoaded || store.isLoaded(object)) {
cachingStore.put(object);
} else {
// if anything is not loaded, we can't consider them all loaded
allLoaded = false;
}
});
if (allLoaded) {
store.allLoaded = true;
}
return results;
});
return results;
},
// TODO: for now, all forEach() calls delegate to fetch(), but that may be different
// with IndexedDB, so we may need to intercept forEach as well (and hopefully not
// double load elements.
// isValidFetchCache: boolean
// This flag indicates if a previous fetch can be used as a cache for subsequent
// fetches (in this collection, or downstream).
isValidFetchCache: false,
get: function (id, directives) {
var cachingStore = this.cachingStore;
var masterGet = this.getInherited(arguments);
var masterStore = this;
// if everything is being loaded, we always wait for that to finish
return when(this.fetchRequest, function () {
return when(cachingStore.get(id), function (result) {
if (result !== undefined) {
return result;
} else if (masterGet) {
return when(masterGet.call(masterStore, id, directives), function (result) {
if (result) {
cachingStore.put(result, {id: id});
}
return result;
});
}
});
});
},
add: function (object, directives) {
var cachingStore = this.cachingStore;
return when(this.inherited(arguments), function (result) {
// now put result in cache (note we don't do add, because add may have
// called put() and already added it)
var cachedPutResult =
cachingStore.put(object && typeof result === 'object' ? result : object, directives);
// the result from the add should be dictated by the master store and be unaffected by the cachingStore,
// unless the master store doesn't implement add
return result || cachedPutResult;
});
},
put: function (object, directives) {
// first remove from the cache, so it is empty until we get a response from the master store
var cachingStore = this.cachingStore;
cachingStore.remove((directives && directives.id) || this.getIdentity(object));
return when(this.inherited(arguments), function (result) {
// now put result in cache
var cachedPutResult =
cachingStore.put(object && typeof result === 'object' ? result : object, directives);
// the result from the put should be dictated by the master store and be unaffected by the cachingStore,
// unless the master store doesn't implement put
return result || cachedPutResult;
});
},
remove: function (id, directives) {
var cachingStore = this.cachingStore;
return when(this.inherited(arguments), function (result) {
return when(cachingStore.remove(id, directives), function () {
return result;
});
});
},
evict: function (id) {
// summary:
// Evicts an object from the cache
// any eviction means that we don't have everything loaded anymore
this.allLoaded = false;
return this.cachingStore.remove(id);
},
invalidate: function () {
// summary:
// Invalidates this collection's cache as being a valid source of
// future fetches
this.allLoaded = false;
},
_createSubCollection: function () {
var subCollection = this.inherited(arguments);
subCollection._parent = this;
return subCollection;
},
sort: cachingQuery('sort'),
filter: cachingQuery('filter'),
_getQuerierFactory: function (type) {
var cachingStore = this.cachingStore;
return this.inherited(arguments) || lang.hitch(cachingStore, cachingStore._getQuerierFactory(type));
}
};
var Cache = declare(null, CachePrototype);
Cache.create = function (target, properties) {
// create a delegate of an existing store with caching
// functionality mixed in
target = declare.safeMixin(lang.delegate(target), CachePrototype);
declare.safeMixin(target, properties);
// we need to initialize it since the constructor won't have been called
init(target);
return target;
};
return Cache;
});
@@ -0,0 +1,172 @@
define([
'dojo/_base/lang',
'dojo/_base/declare'
], function (lang, declare) {
// originally from https://github.com/kfranqueiro/dojo-smore/blob/master/Csv.js
var quoteRx = /^\s*"([\S\s]*)"\s*$/,
doubleQuoteRx = /""/g,
singleQuoteRx = /"/g;
function arrays2hash(keys, values) {
// Takes 2 arrays and builds a hash where the keys are from the first array,
// and the values are from the second.
var obj = {},
len = keys.length,
i;
for (i = 0; i < len; i++) {
obj[keys[i]] = values[i];
}
return obj;
}
return declare(null, {
// summary:
// A store mixin for supporting CSV format.
// fieldNames: Array?
// If specified, indicates names of fields in the order they appear in
// CSV records. If unspecified, the first line of the CSV will be treated
// as a header row, and field names will be populated from there.
fieldNames: null,
// delimiter: String
// Delimiter between fields; default is a comma.
delimiter: ',',
// newline: String
// Character sequence to consider a newline.
// Defaults to '\r\n' (CRLF) as per RFC 4180.
newline: '\r\n',
// trim: Boolean
// If true, leading/trailing space will be trimmed from any unquoted values.
trim: false,
parse: function (str) {
// handles the parsing of the incoming data as CSV.
var data = [],
lines = str.split(this.newline),
fieldNames = this.fieldNames,
numquotes = 0, // tracks number of " characters encountered
values = [], // records values in the current record
value = '',
prefix = '', // used to re-add delimiters and newlines to a spanning value
parts, part, numlines, numparts, match,
i, j, k;
// Outer loop iterates over lines. It's labeled so that inner loop
// can jump out if an invalid value is encountered.
lineloop:
for (i = 0, numlines = lines.length; i < numlines; i++) {
if (!lang.trim(lines[i])) { continue; } // ignore blank lines
parts = lines[i].split(this.delimiter);
// Inner loop iterates over "parts" (pieces of the line, split by
// the configured delimiter).
for (j = 0, numparts = parts.length; j < numparts; j++) {
part = parts[j];
k = -1;
// Apply any leftovers in prefix before the next part, then clear it.
value += prefix + part;
prefix = '';
// Count number of quotes in part to see whether we have a matching set.
while ((k = part.indexOf('"', k + 1)) >= 0) { numquotes++; }
if (numquotes % 2 === 0) {
// Even number of quotes: we're done with this value.
if (numquotes > 0) {
match = quoteRx.exec(value);
if (match) {
// Good quoted string; unescape any quotes within.
values.push(match[1].replace(doubleQuoteRx, '"'));
} else {
// If the completed value didn't match the RegExp, it's invalid
// (e.g. quotes were inside the value but not surrounding it).
// Jump out of the outer loop and start fresh on the next line.
console.warn('Csv: discarding row with invalid value: ' + value);
values = [];
value = '';
numquotes = 0;
continue lineloop;
}
} else {
// No quotes; push value as-is or trimmed.
// (If this is the header row, trim regardless of setting.)
values.push(this.trim || !fieldNames ? lang.trim(value) : value);
}
value = '';
numquotes = 0;
} else {
// Open quoted value: add delimiter to current value on next run.
// (i.e., we split on an instance of the delimiter character that is
// actually *inside* a quoted value.)
prefix = this.delimiter;
}
} // End of inner loop (delimited parts)
if (numquotes === 0) {
// Line ended cleanly, push values and reset.
if (!fieldNames) {
// We don't know any field names yet, so pick them up from the
// first row of data.
fieldNames = this.fieldNames = values;
} else {
data.push(arrays2hash(fieldNames, values));
}
values = [];
} else {
// We're in the middle of a quoted value with a newline in it,
// so add a newline to it on the next iteration.
prefix = this.newline;
}
} // End of outer loop (lines)
// The data is assembled; return
return data;
},
toCsv: function (options) {
// summary:
// Returns data from Memory store, re-exported to CSV format.
return this.stringify(this.data, options);
},
stringify: function (data, options) {
// summary:
// Serializes data as CSV
// options: Object?
// Optional object specifying options affecting the CSV output.
// * alwaysQuote: if true (default), all values will be quoted;
// if false, values will be quoted only if they need to be.
// * trailingNewline: if true, a newline will be included at the end
// of the string (after the last record). Default is false.
options = options || {};
var alwaysQuote = options.alwaysQuote,
fieldNames = this.fieldNames,
delimiter = this.delimiter,
newline = this.newline,
output = '',
i, j, value, needsQuotes;
// Process header row first (-1 case), then all data rows.
for (i = -1; i < data.length; i++) {
if (i > -1) { output += newline; }
for (j = 0; j < fieldNames.length; j++) {
value = i < 0 ? fieldNames[j] : data[i][fieldNames[j]];
needsQuotes = alwaysQuote ||
value.indexOf('"') >= 0 || value.indexOf(delimiter) >= 0;
output += (j > 0 ? delimiter : '') +
(needsQuotes ? '"' + value.replace(singleQuoteRx, '""') + '"' : value);
}
}
if (options.trailingNewline) { output += newline; }
return output;
}
});
});
@@ -0,0 +1,91 @@
define(['dojo/_base/declare'], function (declare) {
// a Filter builder
function filterCreator(type) {
// constructs a new filter based on type, used to create each comparison method
return function newFilter() {
var Filter = this.constructor;
var filter = new Filter();
filter.type = type;
// ensure args is array so we can concat, slice, unshift
filter.args = Array.prototype.slice.call(arguments);
if (this.type) {
// we are chaining, so combine with an and operator
return filterCreator('and').call(Filter.prototype, this, filter);
}
return filter;
};
}
function logicalOperatorCreator(type) {
// constructs a new logical operator 'filter', used to create each logical operation method
return function newLogicalOperator() {
var Filter = this.constructor;
var argsArray = [];
for (var i = 0; i < arguments.length; i++) {
var arg = arguments[i];
argsArray.push(arg instanceof Filter ? arg : new Filter(arg));
}
var filter = new Filter();
filter.type = type;
filter.args = argsArray;
if (this.type === type) {
// chaining, same type
// combine arguments
filter.args = this.args.concat(argsArray);
} else if (this.type) {
// chaining, different type
// add this filter to start of arguments
argsArray.unshift(this);
} else if (argsArray.length === 1) {
// not chaining and only one argument
// returned filter is the same as the single argument
filter.type = argsArray[0].type;
filter.args = argsArray[0].args.slice();
}
return filter;
};
}
var Filter = declare(null, {
constructor: function (filterArg) {
var argType = typeof filterArg;
switch (argType) {
case 'object':
var filter = this;
// construct a filter based on the query object
for (var key in filterArg){
var value = filterArg[key];
if (value instanceof this.constructor) {
// fully construct the filter from the single arg
filter = filter[value.type](key, value.args[0]);
} else if (value && value.test) {
// support regex
filter = filter.match(key, value);
} else {
filter = filter.eq(key, value);
}
}
this.type = filter.type;
this.args = filter.args;
break;
case 'function': case 'string':
// allow string and function args as well
this.type = argType;
this.args = [filterArg];
}
},
// define our operators
and: logicalOperatorCreator('and'),
or: logicalOperatorCreator('or'),
eq: filterCreator('eq'),
ne: filterCreator('ne'),
lt: filterCreator('lt'),
lte: filterCreator('lte'),
gt: filterCreator('gt'),
gte: filterCreator('gte'),
contains: filterCreator('contains'),
'in': filterCreator('in'),
match: filterCreator('match')
});
Filter.filterCreator = filterCreator;
Filter.logicalOperatorCreator = logicalOperatorCreator;
return Filter;
});
@@ -0,0 +1,190 @@
dstore is available under *either* the terms of the modified BSD license *or* the
Academic Free License version 2.1. As a recipient of dstore, you may choose which
license to receive this code under.
The text of the AFL and BSD licenses is reproduced below.
-------------------------------------------------------------------------------
The "New" BSD License:
**********************
Copyright (c) 2010-2015, The Dojo Foundation
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the Dojo Foundation nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-------------------------------------------------------------------------------
The Academic Free License, v. 2.1:
**********************************
This Academic Free License (the "License") applies to any original work of
authorship (the "Original Work") whose owner (the "Licensor") has placed the
following notice immediately following the copyright notice for the Original
Work:
Licensed under the Academic Free License version 2.1
1) Grant of Copyright License. Licensor hereby grants You a world-wide,
royalty-free, non-exclusive, perpetual, sublicenseable license to do the
following:
a) to reproduce the Original Work in copies;
b) to prepare derivative works ("Derivative Works") based upon the Original
Work;
c) to distribute copies of the Original Work and Derivative Works to the
public;
d) to perform the Original Work publicly; and
e) to display the Original Work publicly.
2) Grant of Patent License. Licensor hereby grants You a world-wide,
royalty-free, non-exclusive, perpetual, sublicenseable license, under patent
claims owned or controlled by the Licensor that are embodied in the Original
Work as furnished by the Licensor, to make, use, sell and offer for sale the
Original Work and Derivative Works.
3) Grant of Source Code License. The term "Source Code" means the preferred
form of the Original Work for making modifications to it and all available
documentation describing how to modify the Original Work. Licensor hereby
agrees to provide a machine-readable copy of the Source Code of the Original
Work along with each copy of the Original Work that Licensor distributes.
Licensor reserves the right to satisfy this obligation by placing a
machine-readable copy of the Source Code in an information repository
reasonably calculated to permit inexpensive and convenient access by You for as
long as Licensor continues to distribute the Original Work, and by publishing
the address of that information repository in a notice immediately following
the copyright notice that applies to the Original Work.
4) Exclusions From License Grant. Neither the names of Licensor, nor the names
of any contributors to the Original Work, nor any of their trademarks or
service marks, may be used to endorse or promote products derived from this
Original Work without express prior written permission of the Licensor. Nothing
in this License shall be deemed to grant any rights to trademarks, copyrights,
patents, trade secrets or any other intellectual property of Licensor except as
expressly stated herein. No patent license is granted to make, use, sell or
offer to sell embodiments of any patent claims other than the licensed claims
defined in Section 2. No right is granted to the trademarks of Licensor even if
such marks are included in the Original Work. Nothing in this License shall be
interpreted to prohibit Licensor from licensing under different terms from this
License any Original Work that Licensor otherwise would have a right to
license.
5) This section intentionally omitted.
6) Attribution Rights. You must retain, in the Source Code of any Derivative
Works that You create, all copyright, patent or trademark notices from the
Source Code of the Original Work, as well as any notices of licensing and any
descriptive text identified therein as an "Attribution Notice." You must cause
the Source Code for any Derivative Works that You create to carry a prominent
Attribution Notice reasonably calculated to inform recipients that You have
modified the Original Work.
7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that
the copyright in and to the Original Work and the patent rights granted herein
by Licensor are owned by the Licensor or are sublicensed to You under the terms
of this License with the permission of the contributor(s) of those copyrights
and patent rights. Except as expressly stated in the immediately proceeding
sentence, the Original Work is provided under this License on an "AS IS" BASIS
and WITHOUT WARRANTY, either express or implied, including, without limitation,
the warranties of NON-INFRINGEMENT, MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU.
This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No
license to Original Work is granted hereunder except under this disclaimer.
8) Limitation of Liability. Under no circumstances and under no legal theory,
whether in tort (including negligence), contract, or otherwise, shall the
Licensor be liable to any person for any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License
or the use of the Original Work including, without limitation, damages for loss
of goodwill, work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses. This limitation of liability shall not
apply to liability for death or personal injury resulting from Licensor's
negligence to the extent applicable law prohibits such limitation. Some
jurisdictions do not allow the exclusion or limitation of incidental or
consequential damages, so this exclusion and limitation may not apply to You.
9) Acceptance and Termination. If You distribute copies of the Original Work or
a Derivative Work, You must make a reasonable effort under the circumstances to
obtain the express assent of recipients to the terms of this License. Nothing
else but this License (or another written agreement between Licensor and You)
grants You permission to create Derivative Works based upon the Original Work
or to exercise any of the rights granted in Section 1 herein, and any attempt
to do so except under the terms of this License (or another written agreement
between Licensor and You) is expressly prohibited by U.S. copyright law, the
equivalent laws of other countries, and by international treaty. Therefore, by
exercising any of the rights granted to You in Section 1 herein, You indicate
Your acceptance of this License and all of its terms and conditions.
10) Termination for Patent Action. This License shall terminate automatically
and You may no longer exercise any of the rights granted to You by this License
as of the date You commence an action, including a cross-claim or counterclaim,
against Licensor or any licensee alleging that the Original Work infringes a
patent. This termination provision shall not apply for an action alleging
patent infringement by combinations of the Original Work with other software or
hardware.
11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this
License may be brought only in the courts of a jurisdiction wherein the
Licensor resides or in which Licensor conducts its primary business, and under
the laws of that jurisdiction excluding its conflict-of-law provisions. The
application of the United Nations Convention on Contracts for the International
Sale of Goods is expressly excluded. Any use of the Original Work outside the
scope of this License or after its termination shall be subject to the
requirements and penalties of the U.S. Copyright Act, 17 U.S.C. § 101 et
seq., the equivalent laws of other countries, and international treaty. This
section shall survive the termination of this License.
12) Attorneys Fees. In any action to enforce the terms of this License or
seeking damages relating thereto, the prevailing party shall be entitled to
recover its costs and expenses, including, without limitation, reasonable
attorneys' fees and costs incurred in connection with such action, including
any appeal of such action. This section shall survive the termination of this
License.
13) Miscellaneous. This License represents the complete agreement concerning
the subject matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent necessary to
make it enforceable.
14) Definition of "You" in This License. "You" throughout this License, whether
in upper or lower case, means an individual or a legal entity exercising rights
under, and complying with all of the terms of, this License. For legal
entities, "You" includes any entity that controls, is controlled by, or is
under common control with you. For purposes of this definition, "control" means
(i) the power, direct or indirect, to cause the direction or management of such
entity, whether by contract or otherwise, or (ii) ownership of fifty percent
(50%) or more of the outstanding shares, or (iii) beneficial ownership of such
entity.
15) Right to Use. You may use the Original Work in all ways not otherwise
restricted or conditioned by this License or by law, and Licensor promises not
to interfere with or be responsible for such uses by You.
This license is Copyright (C) 2003-2004 Lawrence E. Rosen. All rights reserved.
Permission is hereby granted to copy and distribute this license without
modification. This license may not be modified without the express written
permission of its copyright owner.
@@ -0,0 +1,8 @@
define(['./db/has!indexeddb?./db/IndexedDB:sql?./db/SQL:./db/LocalStorage'],
function(LocalDB){
// module:
// ./store/LocalDB
// summary:
// The module defines an object store based on local database access
return LocalDB;
});
@@ -0,0 +1,238 @@
define([
'dojo/_base/declare',
'dojo/_base/lang',
'dojo/_base/array',
'./Store',
'./Promised',
'./SimpleQuery',
'./QueryResults'
], function (declare, lang, arrayUtil, Store, Promised, SimpleQuery, QueryResults) {
// module:
// dstore/Memory
return declare([Store, Promised, SimpleQuery ], {
constructor: function () {
// summary:
// Creates a memory object store.
// options: dstore/Memory
// This provides any configuration information that will be mixed into the store.
// This should generally include the data property to provide the starting set of data.
// Add a version property so subcollections can detect when they're using stale data
this.storage.version = 0;
},
postscript: function () {
this.inherited(arguments);
// Set the data in `postscript` so subclasses can override `data` in their constructors
// (e.g., a LocalStorage store that retrieves its data from localStorage)
this.setData(this.data || []);
},
// data: Array
// The array of all the objects in the memory store
data: null,
autoEmitEvents: false, // this is handled by the methods themselves
getSync: function (id) {
// summary:
// Retrieves an object by its identity
// id: Number
// The identity to use to lookup the object
// returns: Object
// The object in the store that matches the given id.
return this.storage.fullData[this.storage.index[id]];
},
putSync: function (object, options) {
// summary:
// Stores an object
// object: Object
// The object to store.
// options: dstore/Store.PutDirectives?
// Additional metadata for storing the data. Includes an 'id'
// property if a specific id is to be used.
// returns: Number
options = options || {};
var storage = this.storage,
index = storage.index,
data = storage.fullData;
var Model = this.Model;
if (Model && !(object instanceof Model)) {
// if it is not the correct type, restore a
// properly typed version of the object. Note that we do not allow
// mutation here
object = this._restore(object);
}
var id = this.getIdentity(object);
if (id == null) {
this._setIdentity(object, ('id' in options) ? options.id : Math.random());
id = this.getIdentity(object);
}
storage.version++;
var eventType = id in index ? 'update' : 'add',
event = { target: object },
previousIndex,
defaultDestination;
if (eventType === 'update') {
if (options.overwrite === false) {
throw new Error('Object already exists');
} else {
data.splice(previousIndex = index[id], 1);
defaultDestination = previousIndex;
}
} else {
defaultDestination = this.defaultNewToStart ? 0 : data.length;
}
var destination;
if ('beforeId' in options) {
var beforeId = options.beforeId;
if (beforeId === null) {
destination = data.length;
} else {
destination = index[beforeId];
// Account for the removed item
if (previousIndex < destination) {
--destination;
}
}
if (destination !== undefined) {
event.beforeId = beforeId;
} else {
console.error('options.beforeId was specified but no corresponding index was found');
destination = defaultDestination;
}
} else {
destination = defaultDestination;
}
data.splice(destination, 0, object);
// the fullData has been changed, so the index needs updated
var i = isFinite(previousIndex) ? Math.min(previousIndex, destination) : destination;
for (var l = data.length; i < l; ++i) {
index[this.getIdentity(data[i])] = i;
}
this.emit(eventType, event);
return object;
},
addSync: function (object, options) {
// summary:
// Creates an object, throws an error if the object already exists
// object: Object
// The object to store.
// options: dstore/Store.PutDirectives?
// Additional metadata for storing the data. Includes an 'id'
// property if a specific id is to be used.
// returns: Number
(options = options || {}).overwrite = false;
// call put with overwrite being false
return this.putSync(object, options);
},
removeSync: function (id) {
// summary:
// Deletes an object by its identity
// id: Number
// The identity to use to delete the object
// returns: Boolean
// Returns true if an object was removed, falsy (undefined) if no object matched the id
var storage = this.storage;
var index = storage.index;
var data = storage.fullData;
if (id in index) {
var removed = data.splice(index[id], 1)[0];
// now we have to reindex
this._reindex();
this.emit('delete', {id: id, target: removed});
return true;
}
},
setData: function (data) {
// summary:
// Sets the given data as the source for this store, and indexes it
// data: Object[]
// An array of objects to use as the source of data. Note that this
// array will not be copied, it is used directly and mutated as
// data changes.
if (this.parse) {
data = this.parse(data);
}
if (data.items) {
// just for convenience with the data format ItemFileReadStore expects
this.idProperty = data.identifier || this.idProperty;
data = data.items;
}
var storage = this.storage;
storage.fullData = this.data = data;
this._reindex();
},
_reindex: function () {
var storage = this.storage;
var index = storage.index = {};
var data = storage.fullData;
var Model = this.Model;
var ObjectPrototype = Object.prototype;
for (var i = 0, l = data.length; i < l; i++) {
var object = data[i];
if (Model && !(object instanceof Model)) {
var restoredObject = this._restore(object,
// only allow mutation if it is a plain object
// (which is generally the expected input),
// if "typed" objects are actually passed in, we will
// respect that, and leave the original alone
object.__proto__ === ObjectPrototype);
if (object !== restoredObject) {
// a new object was generated in the restoration process,
// so we have to update the item in the data array.
data[i] = object = restoredObject;
}
}
index[this.getIdentity(object)] = i;
}
storage.version++;
},
fetchSync: function () {
var data = this.data;
if (!data || data._version !== this.storage.version) {
// our data is absent or out-of-date, so we requery from the root
// start with the root data
data = this.storage.fullData;
var queryLog = this.queryLog;
// iterate through the query log, applying each querier
for (var i = 0, l = queryLog.length; i < l; i++) {
data = queryLog[i].querier(data);
}
// store it, with the storage version stamp
data._version = this.storage.version;
this.data = data;
}
return new QueryResults(data);
},
fetchRangeSync: function (kwArgs) {
var data = this.fetchSync(),
start = kwArgs.start,
end = kwArgs.end;
return new QueryResults(data.slice(start, end), {
totalLength: data.length
});
},
_includePropertyInSubCollection: function (name) {
return name !== 'data' && this.inherited(arguments);
}
});
});
@@ -0,0 +1,36 @@
define([
'dojo/_base/declare',
'dojo/Deferred',
'./QueryResults',
'dojo/when'
], function (declare, Deferred, QueryResults, when) {
// module:
// this is a mixin that can be used to provide async methods,
// by implementing their sync counterparts
function promised(method, query) {
return function() {
var deferred = new Deferred();
try {
deferred.resolve(this[method].apply(this, arguments));
} catch (error) {
deferred.reject(error);
}
if (query) {
// need to create a QueryResults and ensure the totalLength is
// a promise.
var queryResults = new QueryResults(deferred.promise);
queryResults.totalLength = when(queryResults.totalLength);
return queryResults;
}
return deferred.promise;
};
}
return declare(null, {
get: promised('getSync'),
put: promised('putSync'),
add: promised('addSync'),
remove: promised('removeSync'),
fetch: promised('fetchSync', true),
fetchRange: promised('fetchRangeSync', true)
});
});
@@ -0,0 +1,68 @@
define([], function () {
/*=====
var __QueryMethodArgs = {
// type: String
// The type of the query. This identifies the query's type in the query log
// and the name of the corresponding query engine method.
// normalizeArguments: Function?
// A function that normalizes arguments for consumption by a query engine
// applyQuery: Function?
// A function that takes the query's new subcollection and the query's log entry
// and applies it to the new subcollection. This is useful for collections that need
// to both declare and implement new query methods.
// querierFactory: Function?
// A factory function that provides a default querier implementation to use when
// a collection does not define its own querier factory method for this query type.
};
=====*/
return function QueryMethod(/*__QueryMethodArgs*/ kwArgs) {
// summary:
// The constructor for a dstore collection query method
// description:
// This is the constructor for a collection query method. It encapsulates the following:
// * Creating a new subcollection for the query results
// * Logging the query in the collection's `queryLog`
// * Normalizing query arguments
// * Applying the query engine
// kwArgs:
// The properties that define the query method
// returns: Function
// Returns a function that takes query arguments and returns a new collection with
// the query associated with it.
var type = kwArgs.type,
normalizeArguments = kwArgs.normalizeArguments,
applyQuery = kwArgs.applyQuery,
defaultQuerierFactory = kwArgs.querierFactory;
return function () {
// summary:
// A query method whose arguments are determined by the query type
// returns: dstore/Collection
// A collection representing the query results
var originalArguments = Array.prototype.slice.call(arguments),
normalizedArguments = normalizeArguments
? normalizeArguments.apply(this, originalArguments)
: originalArguments,
logEntry = {
type: type,
arguments: originalArguments,
normalizedArguments: normalizedArguments
},
querierFactory = this._getQuerierFactory(type) || defaultQuerierFactory;
if (querierFactory) {
// Call the query factory in store context to support things like
// mapping a filter query's string argument to a custom filter method on the collection
logEntry.querier = querierFactory.apply(this, normalizedArguments);
}
var newCollection = this._createSubCollection({
queryLog: this.queryLog.concat(logEntry)
});
return applyQuery ? applyQuery.call(this, newCollection, logEntry) : newCollection;
};
};
});
@@ -0,0 +1,38 @@
define(['dojo/_base/lang', 'dojo/when'], function (lang, when) {
function forEach(callback, instance) {
return when(this, function(data) {
for (var i = 0, l = data.length; i < l; i++){
callback.call(instance, data[i], i, data);
}
});
}
return function (data, options) {
var hasTotalLength = options && 'totalLength' in options;
if(data.then) {
data = lang.delegate(data);
// a promise for the eventual realization of the totalLength, in
// case it comes from the resolved data
var totalLengthPromise = data.then(function (data) {
// calculate total length, now that we have access to the resolved data
var totalLength = hasTotalLength ? options.totalLength :
data.totalLength || data.length;
// make it available on the resolved data
data.totalLength = totalLength;
// don't return the totalLength promise unless we need to, to avoid
// triggering a lazy promise
return !hasTotalLength && totalLength;
});
// make the totalLength available on the promise (whether through the options or the enventual
// access to the resolved data)
data.totalLength = hasTotalLength ? options.totalLength : totalLengthPromise;
// make the response available as well
data.response = options && options.response;
} else {
data.totalLength = hasTotalLength ? options.totalLength : data.length;
}
data.forEach = forEach;
return data;
};
});
@@ -0,0 +1,67 @@
dstore
======
The dstore package is a data infrastructure framework, providing the tools for modelling and interacting with data collections and objects. dstore is designed to work with a variety of data storage mediums, and provide a consistent interface for accessing data across different user interface components. There are several key entities within dstore:
* [Collection](./docs/Collection.md) - This is a list of objects, which can be iterated over, sorted, filtered, and monitored for changes.
* [Store](./docs/Store.md) - A Store is a Collection that may also include the ability to identify, to add, remove, and update objects.
# [Included Stores](./docs/Stores.md)
The dstore package includes several store implementations that can be used for the needs of different applications. These include:
* `Memory` - This is a simple memory-based store that takes an array and provides access to the objects in the array through the store interface.
* `Request` - This is a simple server-based collection that sends HTTP requests following REST conventions to access and modify data requested through the store interface.
* `Rest` - This is a store built on `Request` that implements add, remove, and update operations using HTTP requests following REST conventions
* `RequestMemory` - This is a Memory based store that will retrieve its contents from a server/URL.
* `LocalDB` - This a store based on the browser's local database/storage capabilities. Data stored in this store will be persisted in the local browser.
* `Cache` - This is a store mixin that combines a master and caching store to provide caching functionality.
* `Tree` - This is a store mixin that provides hierarchical querying functionality, defining parent/child relationships for the display of data in a tree.
* `Trackable` - This a store mixin that adds index information to `add`, `update`, and `delete` events of tracked store instances. This adds a track() method for tracking stores.
* `SimpleQuery` - This is a mixin with basic querying functionality, which is extended by the Memory store, and can be used to add client side querying functionality to the Request/Rest store.
See the [Stores section](./docs/Stores.md) for more information these stores.
## [Collections](./docs/Collection.md)
A Collection is the interface for a collection of items, which can be filtered and sorted to create new collections. When implementing this interface, every method and property is optional, and is only needed if the functionality it provides is required, however all the included stores implement every method. A collection's list of objects may not be immediately retrieved from the underlying data storage until the it is accessed through `forEach()`, `fetch()`, or `fetchRange()`.
For more details on the Collection API and how to query, see the [Collection section](./docs/Collection.md)
## [Store](./docs/Store.md)
A store is an extension of a collection and is an entity that not only contains a set of objects, but also provides an interface for identifying, adding, modifying, removing, and querying data. See the [Store section](./docs/Store.md) for the details on the Store interface.
## Promise-based API and Synchronous Operations
All CRUD methods, such as `get`, `put`, `remove`, and `fetch`, return promises. However, stores and collections may provide synchronous versions of those methods with a "Sync" suffix (e.g., `Memory#fetchSync` to fetch synchronously from a `Memory` store).
# [Data Modelling](https://github.com/SitePen/dmodel)
In addition to handling collections of items, dstore works with the dmodel package to provides robust data modeling capabilities for managing individual objects. dmodel provides a data model class that includes multiple methods on data objects, for saving, validating, and monitoring objects for changes. By setting a model on stores, all objects returned from a store, whether a single object returned from a `get()` or an array of objects returned from a `fetch()`, will be an instance of the store's data model.
For more information, please see the [dmodel project](https://github.com/SitePen/dmodel).
# [Adapters](./docs/Adapters.md)
Adapters make it possible work with legacy Dojo object stores and widgets that expect Dojo object stores. dstore also includes an adapter for using a store with charts. See the [Adapters section](./docs/Adapters.md) for more information.
# [Testing](./docs/Testing.md)
dstore uses [Intern](http://theintern.io/) as its test runner. A full description
of how to setup testing is [available here](./docs/Testing.md). Tests can
either be run using the browser, or using [Sauce Labs](https://saucelabs.com/).
More information on writing your own tests with Intern can be found in the
[Intern wiki](https://github.com/theintern/intern/wiki).
# Dependencies
dstore's only required dependency is Dojo version 1.8 or higher. Running the unit tests requires the [intern-geezer](https://github.com/theintern/intern/tree/geezer) package (see the testing docs for [more information](./docs/Testing.md)). The extensions/RqlQuery module can leverage the [rql](https://github.com/persvr/rql) package, but the rql package is only needed if you use this extension.
# Contributing
We welcome contributions, but please read the [contributing documentation](./docs/CONTRIBUTING.md) to help us be able to effectively receive your contributions and pull requests.
# License
The dstore project is available under the same dual BSD/AFLv2 license as the Dojo Toolkit.
@@ -0,0 +1,296 @@
define([
'dojo/request',
'dojo/_base/lang',
'dojo/_base/array',
'dojo/json',
'dojo/_base/declare',
'./Store',
'./QueryResults'
], function (request, lang, arrayUtil, JSON, declare, Store, QueryResults) {
var push = [].push;
return declare(Store, {
// summary:
// This is a basic store for RESTful communicating with a server through JSON
// formatted data. It extends dstore/Store.
constructor: function () {
// summary:
// This is a basic store for RESTful communicating with a server through JSON
// formatted data.
// options: dstore/JsonRest
// This provides any configuration information that will be mixed into the store
this.headers || (this.headers = {});
this._targetContainsQueryString = this.target.lastIndexOf('?') >= 0;
},
// headers: Object
// Additional headers to pass in all requests to the server. These can be overridden
// by passing additional headers to calls to the store.
headers: {},
// parse: Function
// This function performs the parsing of the response text from the server. This
// defaults to JSON, but other formats can be parsed by providing an alternate
// parsing function. If you do want to use an alternate format, you will probably
// want to use an alternate stringify function for the serialization of data as well.
// Also, if you want to support parsing a larger set of JavaScript objects
// outside of strict JSON parsing, you can provide dojo/_base/json.fromJson as the parse function
parse: JSON.parse,
// target: String
// The target base URL to use for all requests to the server. This string will be
// prepended to the id to generate the URL (relative or absolute) for requests
// sent to the server
target: '',
// _targetContainsQueryString: Boolean
// A flag indicating whether the target contains a query string
// sortParam: String
// The query parameter to used for holding sort information. If this is omitted, than
// the sort information is included in a functional query token to avoid colliding
// with the set of name/value pairs.
// ascendingPrefix: String
// The prefix to apply to sort property names that are ascending
ascendingPrefix: '+',
// descendingPrefix: String
// The prefix to apply to sort property names that are ascending
descendingPrefix: '-',
// accepts: String
// Defines the Accept header to use on HTTP requests
accepts: 'application/json',
// useRangeHeaders: Boolean
// The indicates if range limits (start and end) should be specified
// a Range header, using items units. If this is set to true, a header
// be included of the form:
// Range: items=start-end
// rangeStartParam: String
// The indicates if range limits (start and end) should be specified
// in a query parameter, and what the start parameter should be.
// This must be used in conjunction with the rangeCountParam
// If this is not specified, the range will
// included with a RQL style limit() parameter
// rangeCountParam: String
// The indicates if range limits (start and end) should be specified
// in a query parameter, and what the count parameter should be.
// This must be used in conjunction with the rangeStartParam
// If this is not specified, the range will
// included with a RQL style limit() parameter
fetch: function (kwArgs) {
var results = this._request(kwArgs);
return new QueryResults(results.data, {
response: results.response
});
},
fetchRange: function (kwArgs) {
var start = kwArgs.start,
end = kwArgs.end,
requestArgs = {};
if (this.useRangeHeaders) {
requestArgs.headers = lang.mixin(this._renderRangeHeaders(start, end), kwArgs.headers);
} else {
requestArgs.queryParams = this._renderRangeParams(start, end);
if (kwArgs.headers) {
requestArgs.headers = kwArgs.headers;
}
}
var results = this._request(requestArgs);
return new QueryResults(results.data, {
totalLength: results.total,
response: results.response
});
},
_request: function (kwArgs) {
kwArgs = kwArgs || {};
// perform the actual query
var headers = lang.delegate(this.headers, { Accept: this.accepts });
if ('headers' in kwArgs) {
lang.mixin(headers, kwArgs.headers);
}
var requestUrl = this._renderUrl(kwArgs.queryParams);
var response = request(requestUrl, {
method: 'GET',
headers: headers
});
var collection = this;
var parsedResponse = response.then(function (response) {
return collection.parse(response);
});
return {
data: parsedResponse.then(function (data) {
// support items in the results
var results = data.items || data;
for (var i = 0, l = results.length; i < l; i++) {
results[i] = collection._restore(results[i], true);
}
return results;
}),
total: parsedResponse.then(function (data) {
// check for a total property
var total = data.total;
if (total > -1) {
// if we have a valid positive number from the data,
// we can use that
return total;
}
// else use headers
return response.response.then(function (response) {
var range = response.getHeader('Content-Range');
return range && (range = range.match(/\/(.*)/)) && +range[1];
});
}),
response: response.response
};
},
_renderFilterParams: function (filter) {
// summary:
// Constructs filter-related params to be inserted into the query string
// returns: String
// Filter-related params to be inserted in the query string
var type = filter.type;
var args = filter.args;
if (!type) {
return [''];
}
if (type === 'string') {
return [args[0]];
}
if (type === 'and' || type === 'or') {
return [arrayUtil.map(filter.args, function (arg) {
// render each of the arguments to and or or, then combine by the right operator
var renderedArg = this._renderFilterParams(arg);
return ((arg.type === 'and' || arg.type === 'or') && arg.type !== type) ?
// need to observe precedence in the case of changing combination operators
'(' + renderedArg + ')' : renderedArg;
}, this).join(type === 'and' ? '&' : '|')];
}
var target = args[1];
if (target) {
if(target._renderUrl) {
// detected nested query, and render the url inside as an argument
target = '(' + target._renderUrl() + ')';
} else if (target instanceof Array) {
target = '(' + target + ')';
}
}
return [encodeURIComponent(args[0]) + '=' + (type === 'eq' ? '' : type + '=') + encodeURIComponent(target)];
},
_renderSortParams: function (sort) {
// summary:
// Constructs sort-related params to be inserted in the query string
// returns: String
// Sort-related params to be inserted in the query string
var sortString = arrayUtil.map(sort, function (sortOption) {
var prefix = sortOption.descending ? this.descendingPrefix : this.ascendingPrefix;
return prefix + encodeURIComponent(sortOption.property);
}, this);
var params = [];
if (sortString) {
params.push(this.sortParam
? encodeURIComponent(this.sortParam) + '=' + sortString
: 'sort(' + sortString + ')'
);
}
return params;
},
_renderRangeParams: function (start, end) {
// summary:
// Constructs range-related params to be inserted in the query string
// returns: String
// Range-related params to be inserted in the query string
var params = [];
if (this.rangeStartParam) {
params.push(
this.rangeStartParam + '=' + start,
this.rangeCountParam + '=' + (end - start)
);
} else {
params.push('limit(' + (end - start) + (start ? (',' + start) : '') + ')');
}
return params;
},
_renderSelectParams: function (properties) {
// summary:
// Constructs range-related params to be inserted in the query string
// returns: String
// Range-related params to be inserted in the query string
var params = [];
if (this.selectParam) {
params.push(this.selectParam + '=' + properties);
} else {
params.push('select(' + properties + ')');
}
return params;
},
_renderQueryParams: function () {
var queryParams = [];
arrayUtil.forEach(this.queryLog, function (entry) {
var type = entry.type,
renderMethod = '_render' + type[0].toUpperCase() + type.substr(1) + 'Params';
if (this[renderMethod]) {
push.apply(queryParams, this[renderMethod].apply(this, entry.normalizedArguments));
} else {
console.warn('Unable to render query params for "' + type + '" query', entry);
}
}, this);
return queryParams;
},
_renderUrl: function (requestParams) {
// summary:
// Constructs the URL used to fetch the data.
// returns: String
// The URL of the data
var queryParams = this._renderQueryParams(),
requestUrl = this.target;
if (requestParams) {
push.apply(queryParams, requestParams);
}
if (queryParams.length > 0) {
requestUrl += (this._targetContainsQueryString ? '&' : '?') + queryParams.join('&');
}
return requestUrl;
},
_renderRangeHeaders: function (start, end) {
// summary:
// Applies a Range header if this collection incorporates a range query
// headers: Object
// The headers to which a Range property is added
var value = 'items=' + start + '-' + (end - 1);
return {
'Range': value,
'X-Range': value //set X-Range for Opera since it blocks "Range" header
};
}
});
});
@@ -0,0 +1,13 @@
define([
'dojo/_base/declare',
'./Request',
'./Cache'
], function(declare, Request, Cache) {
return declare([ Request, Cache ], {
postscript: function () {
this.inherited(arguments);
this.fetch();
},
isValidFetchCache: true
});
});
@@ -0,0 +1,148 @@
define([
'dojo/request',
'dojo/when',
'dojo/_base/lang',
'dojo/json',
'dojo/io-query',
'dojo/_base/declare',
'./Request' /*=====, './Store' =====*/
], function (request, when, lang, JSON, ioQuery, declare, Request /*=====, Store =====*/) {
/*=====
var __HeaderOptions = {
// headers: Object?
// Additional headers to send along with the request.
},
__PutDirectives = declare(Store.PutDirectives, __HeaderOptions),
=====*/
return declare(Request, {
// stringify: Function
// This function performs the serialization of the data for requests to the server. This
// defaults to JSON, but other formats can be serialized by providing an alternate
// stringify function. If you do want to use an alternate format, you will probably
// want to use an alternate parse function for the parsing of data as well.
stringify: JSON.stringify,
_getTarget: function(id){
// summary:
// If the target has no trailing '/', then append it.
// id:
// The identity of the requested target
var target = this.target;
if(target.slice(-1) == '/'){
return target + id;
}else{
return target + '/' + id;
}
},
get: function (id, options) {
// summary:
// Retrieves an object by its identity. This will trigger a GET request to the server using
// the url `this.target + id`.
// id: Number
// The identity to use to lookup the object
// options: Object?
// HTTP headers. For consistency with other methods, if a `headers` key exists on this
// object, it will be used to provide HTTP headers instead.
// returns: Object
// The object in the store that matches the given id.
options = options || {};
var headers = lang.mixin({ Accept: this.accepts }, this.headers, options.headers || options);
var store = this;
return request(this._getTarget(id), {
headers: headers
}).then(function (response) {
return store._restore(store.parse(response), true);
});
},
autoEmitEvents: false, // this is handled by the methods themselves
put: function (object, options) {
// summary:
// Stores an object. This will trigger a PUT request to the server
// if the object has an id, otherwise it will trigger a POST request.
// object: Object
// The object to store.
// options: __PutDirectives?
// Additional metadata for storing the data. Includes an 'id'
// property if a specific id is to be used.
// returns: dojo/_base/Deferred
options = options || {};
var id = ('id' in options) ? options.id : this.getIdentity(object);
var hasId = typeof id !== 'undefined';
var store = this;
var positionHeaders = 'beforeId' in options
? (options.beforeId === null
? { 'Put-Default-Position': 'end' }
: { 'Put-Before': options.beforeId })
: (!hasId || options.overwrite === false
? { 'Put-Default-Position': (this.defaultNewToStart ? 'start' : 'end') }
: null);
var initialResponse = request(hasId ? this._getTarget(id) : this.target, {
method: hasId && !options.incremental ? 'PUT' : 'POST',
data: this.stringify(object),
headers: lang.mixin({
'Content-Type': 'application/json',
Accept: this.accepts,
'If-Match': options.overwrite === true ? '*' : null,
'If-None-Match': options.overwrite === false ? '*' : null
}, positionHeaders, this.headers, options.headers)
});
return initialResponse.then(function (response) {
var event = {};
if ('beforeId' in options) {
event.beforeId = options.beforeId;
}
var result = event.target = response && store._restore(store.parse(response), true) || object;
when(initialResponse.response, function (httpResponse) {
store.emit(httpResponse.status === 201 ? 'add' : 'update', event);
});
return result;
});
},
add: function (object, options) {
// summary:
// Adds an object. This will trigger a PUT request to the server
// if the object has an id, otherwise it will trigger a POST request.
// object: Object
// The object to store.
// options: __PutDirectives?
// Additional metadata for storing the data. Includes an 'id'
// property if a specific id is to be used.
options = options || {};
options.overwrite = false;
return this.put(object, options);
},
remove: function (id, options) {
// summary:
// Deletes an object by its identity. This will trigger a DELETE request to the server.
// id: Number
// The identity to use to delete the object
// options: __HeaderOptions?
// HTTP headers.
options = options || {};
var store = this;
return request(this._getTarget(id), {
method: 'DELETE',
headers: lang.mixin({}, this.headers, options.headers)
}).then(function (response) {
var target = response && store.parse(response);
store.emit('delete', {id: id, target: target});
return response ? target : true;
});
}
});
});
@@ -0,0 +1,204 @@
define([
'dojo/_base/declare',
'dojo/_base/array'
], function (declare, arrayUtil) {
// module:
// dstore/SimpleQuery
function makeGetter(property, queryAccessors) {
if (property.indexOf('.') > -1) {
var propertyPath = property.split('.');
var pathLength = propertyPath.length;
return function (object) {
for (var i = 0; i < pathLength; i++) {
object = object && (queryAccessors && object.get ? object.get(propertyPath[i]) : object[propertyPath[i]]);
}
return object;
};
}
// else
return function (object) {
return object.get ? object.get(property) : object[property];
};
}
var comparators = {
eq: function (value, required) {
return value === required;
},
'in': function(value, required) {
// allow for a collection of data
return arrayUtil.indexOf(required.data || required, value) > -1;
},
ne: function (value, required) {
return value !== required;
},
lt: function (value, required) {
return value < required;
},
lte: function (value, required) {
return value <= required;
},
gt: function (value, required) {
return value > required;
},
gte: function (value, required) {
return value >= required;
},
match: function (value, required, object) {
return required.test(value, object);
},
contains: function (value, required, object, key) {
var collection = this;
return arrayUtil.every(required.data || required, function (requiredValue) {
if (typeof requiredValue === 'object' && requiredValue.type) {
var comparator = collection._getFilterComparator(requiredValue.type);
return arrayUtil.some(value, function (item) {
return comparator.call(collection, item, requiredValue.args[1], object, key);
});
}
return arrayUtil.indexOf(value, requiredValue) > -1;
});
}
};
return declare(null, {
// summary:
// Mixin providing querier factories for core query types
_createFilterQuerier: function (filter) {
// create our matching filter function
var queryAccessors = this.queryAccessors;
var collection = this;
var querier = getQuerier(filter);
function getQuerier(filter) {
var querier;
var type = filter.type;
var args = filter.args;
var comparator = collection._getFilterComparator(type);
if (comparator) {
// it is a comparator
var firstArg = args[0];
var getProperty = makeGetter(firstArg, queryAccessors);
var secondArg = args[1];
if (secondArg && secondArg.fetchSync) {
// if it is a collection, fetch the contents (for `in` and `contains` operators)
secondArg = secondArg.fetchSync();
}
return function (object) {
// get the value for the property and compare to expected value
return comparator.call(collection, getProperty(object), secondArg, object, firstArg);
};
}
switch (type) {
case 'and': case 'or':
for (var i = 0, l = args.length; i < l; i++) {
// combine filters, using and or or
var nextQuerier = getQuerier(args[i]);
if (querier) {
// combine the last querier with a new one
querier = (function(a, b) {
return type === 'and' ?
function(object) {
return a(object) && b(object);
} :
function(object) {
return a(object) || b(object);
};
})(querier, nextQuerier);
} else {
querier = nextQuerier;
}
}
return querier;
case 'function':
return args[0];
case 'string':
// named filter
var filterFunction = collection[args[0]];
if (!filterFunction) {
throw new Error('No filter function ' + args[0] + ' was found in the collection');
}
return filterFunction;
case undefined:
return function () {
return true;
};
default:
throw new Error('Unknown filter operation "' + type + '"');
}
}
return function (data) {
return arrayUtil.filter(data, querier);
};
},
_getFilterComparator: function (type) {
// summary:
// Get the comparator for the specified type
// returns: Function?
return comparators[type] || this.inherited(arguments);
},
_createSelectQuerier: function (properties) {
return function (data) {
var l = properties.length;
return arrayUtil.map(data, properties instanceof Array ?
// array of properties
function (object) {
var selectedObject = {};
for (var i = 0; i < l; i++) {
var property = properties[i];
selectedObject[property] = object[property];
}
return selectedObject;
} :
// single property
function (object) {
return object[properties];
});
};
}
/* jshint ignore:start */
,
_createSortQuerier: function (sorted) {
var queryAccessors = this.queryAccessors;
return function (data) {
data = data.slice();
data.sort(typeof sorted == 'function' ? sorted : function (a, b) {
for (var i = 0; i < sorted.length; i++) {
var comparison;
var sorter = sorted[i];
if (typeof sorter == 'function') {
comparison = sorter(a, b);
} else {
var getProperty = sorter.get || (sorter.get = makeGetter(sorter.property, queryAccessors));
var descending = sorter.descending;
var aValue = getProperty(a);
var bValue = getProperty(b);
aValue != null && (aValue = aValue.valueOf());
bValue != null && (bValue = bValue.valueOf());
comparison = aValue === bValue
? 0
: (!!descending === (aValue === null || aValue > bValue && bValue !== null) ? -1 : 1);
}
if (comparison !== 0) {
return comparison;
}
}
return 0;
});
return data;
};
}
/* jshint ignore:end */
});
});
@@ -0,0 +1,497 @@
define([
'dojo/_base/lang',
'dojo/_base/array',
'dojo/aspect',
'dojo/has',
'dojo/when',
'dojo/Deferred',
'dojo/_base/declare',
'./QueryMethod',
'./Filter',
'dojo/Evented'
], function (lang, arrayUtil, aspect, has, when, Deferred, declare, QueryMethod, Filter, Evented) {
// module:
// dstore/Store
/* jshint proto: true */
// detect __proto__, and avoid using it on Firefox, as they warn about
// deoptimizations. The watch method is a clear indicator of the Firefox
// JS engine.
has.add('object-proto', !!{}.__proto__ && !({}).watch);
var hasProto = has('object-proto');
function emitUpdateEvent(type) {
return function (result, args) {
var self = this;
when(result, function (result) {
var event = { target: result },
options = args[1] || {};
if ('beforeId' in options) {
event.beforeId = options.beforeId;
}
self.emit(type, event);
});
return result;
};
}
var base = Evented;
/*=====
base = [ Evented, Collection ];
=====*/
return /*==== Store= ====*/declare(base, {
constructor: function (options) {
// perform the mixin
options && declare.safeMixin(this, options);
if (this.Model && this.Model.createSubclass) {
// we need a distinct model for each store, so we can
// save the reference back to this store on it.
// we always create a new model to be safe.
this.Model = this.Model.createSubclass([]).extend({
// give a reference back to the store for saving, etc.
_store: this
});
}
// the object the store can use for holding any local data or events
this.storage = new Evented();
var store = this;
if (this.autoEmitEvents) {
// emit events when modification operations are called
aspect.after(this, 'add', emitUpdateEvent('add'));
aspect.after(this, 'put', emitUpdateEvent('update'));
aspect.after(this, 'remove', function (result, args) {
when(result, function () {
store.emit('delete', {id: args[0]});
});
return result;
});
}
},
// autoEmitEvents: Boolean
// Indicates if the events should automatically be fired for put, add, remove
// method calls. Stores may wish to explicitly fire events, to control when
// and which event is fired.
autoEmitEvents: true,
// idProperty: String
// Indicates the property to use as the identity property. The values of this
// property should be unique.
idProperty: 'id',
// queryAccessors: Boolean
// Indicates if client-side query engine filtering should (if the store property is true)
// access object properties through the get() function (enabling querying by
// computed properties), or if it should (by setting this to false) use direct/raw
// property access (which may more closely follow database querying style).
queryAccessors: true,
getIdentity: function (object) {
// summary:
// Returns an object's identity
// object: Object
// The object to get the identity from
// returns: String|Number
return object.get ? object.get(this.idProperty) : object[this.idProperty];
},
_setIdentity: function (object, identityArg) {
// summary:
// Sets an object's identity
// description:
// This method sets an object's identity and is useful to override to support
// multi-key identities and object's whose properties are not stored directly on the object.
// object: Object
// The target object
// identityArg:
// The argument used to set the identity
if (object.set) {
object.set(this.idProperty, identityArg);
} else {
object[this.idProperty] = identityArg;
}
},
forEach: function (callback, thisObject) {
var collection = this;
return when(this.fetch(), function (data) {
for (var i = 0, item; (item = data[i]) !== undefined; i++) {
callback.call(thisObject, item, i, collection);
}
return data;
});
},
on: function (type, listener) {
return this.storage.on(type, listener);
},
emit: function (type, event) {
event = event || {};
event.type = type;
try {
return this.storage.emit(type, event);
} finally {
// Return the initial value of event.cancelable because a listener error makes it impossible
// to know whether the event was actually canceled
return event.cancelable;
}
},
// parse: Function
// One can provide a parsing function that will permit the parsing of the data. By
// default we assume the provide data is a simple JavaScript array that requires
// no parsing (subclass stores may provide their own default parse function)
parse: null,
// stringify: Function
// For stores that serialize data (to send to a server, for example) the stringify
// function can be specified to control how objects are serialized to strings
stringify: null,
// Model: Function
// This should be a entity (like a class/constructor) with a 'prototype' property that will be
// used as the prototype for all objects returned from this store. One can set
// this to the Model from dmodel/Model to return Model objects, or leave this
// to null if you don't want any methods to decorate the returned
// objects (this can improve performance by avoiding prototype setting),
Model: null,
_restore: function (object, mutateAllowed) {
// summary:
// Restores a plain raw object, making an instance of the store's model.
// This is called when an object had been persisted into the underlying
// medium, and is now being restored. Typically restored objects will come
// through a phase of deserialization (through JSON.parse, DB retrieval, etc.)
// in which their __proto__ will be set to Object.prototype. To provide
// data model support, the returned object needs to be an instance of the model.
// This can be accomplished by setting __proto__ to the model's prototype
// or by creating a new instance of the model, and copying the properties to it.
// Also, model's can provide their own restore method that will allow for
// custom model-defined behavior. However, one should be aware that copying
// properties is a slower operation than prototype assignment.
// The restore process is designed to be distinct from the create process
// so their is a clear delineation between new objects and restored objects.
// object: Object
// The raw object with the properties that need to be defined on the new
// model instance
// mutateAllowed: boolean
// This indicates if restore is allowed to mutate the original object
// (by setting its __proto__). If this isn't true, than the restore should
// copy the object to a new object with the correct type.
// returns: Object
// An instance of the store model, with all the properties that were defined
// on object. This may or may not be the same object that was passed in.
var Model = this.Model;
if (Model && object) {
var prototype = Model.prototype;
var restore = prototype._restore;
if (restore) {
// the prototype provides its own restore method
object = restore.call(object, Model, mutateAllowed);
} else if (hasProto && mutateAllowed) {
// the fast easy way
// http://jsperf.com/setting-the-prototype
object.__proto__ = prototype;
} else {
// create a new object with the correct prototype
object = lang.delegate(prototype, object);
}
}
return object;
},
create: function (properties) {
// summary:
// This creates a new instance from the store's model.
// properties:
// The properties that are passed to the model constructor to
// be copied onto the new instance. Note, that should only be called
// when new objects are being created, not when existing objects
// are being restored from storage.
return new this.Model(properties);
},
_createSubCollection: function (kwArgs) {
var newCollection = lang.delegate(this.constructor.prototype);
for (var i in this) {
if (this._includePropertyInSubCollection(i, newCollection)) {
newCollection[i] = this[i];
}
}
return declare.safeMixin(newCollection, kwArgs);
},
_includePropertyInSubCollection: function (name, subCollection) {
return !(name in subCollection) || subCollection[name] !== this[name];
},
// queryLog: __QueryLogEntry[]
// The query operations represented by this collection
queryLog: [], // NOTE: It's ok to define this on the prototype because the array instance is never modified
filter: new QueryMethod({
type: 'filter',
normalizeArguments: function (filter) {
var Filter = this.Filter;
if (filter instanceof Filter) {
return [filter];
}
return [new Filter(filter)];
}
}),
Filter: Filter,
sort: new QueryMethod({
type: 'sort',
normalizeArguments: function (property, descending) {
var sorted;
if (typeof property === 'function') {
sorted = [ property ];
} else {
if (property instanceof Array) {
sorted = property.slice();
} else if (typeof property === 'object') {
sorted = [].slice.call(arguments);
} else {
sorted = [{ property: property, descending: descending }];
}
sorted = arrayUtil.map(sorted, function (sort) {
// copy the sort object to avoid mutating the original arguments
sort = lang.mixin({}, sort);
sort.descending = !!sort.descending;
return sort;
});
// wrap in array because sort objects are a single array argument
sorted = [ sorted ];
}
return sorted;
}
}),
select: new QueryMethod({
type: 'select'
}),
_getQuerierFactory: function (type) {
var uppercaseType = type[0].toUpperCase() + type.substr(1);
return this['_create' + uppercaseType + 'Querier'];
}
/*====,
get: function (id) {
// summary:
// Retrieves an object by its identity
// id: Number
// The identity to use to lookup the object
// returns: Object
// The object in the store that matches the given id.
},
put: function (object, directives) {
// summary:
// Stores an object
// object: Object
// The object to store.
// directives: dstore/Store.PutDirectives?
// Additional directives for storing objects.
// returns: Object
// The object that was stored, with any changes that were made by
// the storage system (like generated id)
},
add: function (object, directives) {
// summary:
// Creates an object, throws an error if the object already exists
// object: Object
// The object to store.
// directives: dstore/Store.PutDirectives?
// Additional directives for creating objects.
// returns: Object
// The object that was stored, with any changes that were made by
// the storage system (like generated id)
},
remove: function (id) {
// summary:
// Deletes an object by its identity
// id: Number
// The identity to use to delete the object
},
transaction: function () {
// summary:
// Starts a new transaction.
// Note that a store user might not call transaction() prior to using put,
// delete, etc. in which case these operations effectively could be thought of
// as "auto-commit" style actions.
// returns: dstore/Store.Transaction
// This represents the new current transaction.
},
getChildren: function (parent) {
// summary:
// Retrieves the children of an object.
// parent: Object
// The object to find the children of.
// returns: dstore/Store.Collection
// A result set of the children of the parent object.
}
====*/
});
});
/*====
var Collection = declare(null, {
// summary:
// This is an abstract API for a collection of objects, which can be filtered,
// sorted, and sliced to create new collections. This is considered to be base
// interface for all stores and query results in dstore. Note that the objects in the
// collection may not be immediately retrieved from the underlying data
// storage until they are actually accessed through forEach() or fetch().
filter: function (query) {
// summary:
// Filters the collection, returning a new subset collection
// query: String|Object|Function
// The query to use for retrieving objects from the store.
// returns: Collection
},
sort: function (property, descending) {
// summary:
// Sorts the current collection into a new collection, reordering the objects by the provided sort order.
// property: String|Function
// The property to sort on. Alternately a function can be provided to sort with
// descending?: Boolean
// Indicate if the sort order should be descending (defaults to ascending)
// returns: Collection
},
fetchRange: function (kwArgs) {
// summary:
// Retrieves a range of objects from the collection, returning a promise to an array.
// kwArgs.start: Number
// The starting index of objects to return (0-indexed)
// kwArgs.end: Number
// The exclusive end of objects to return
// returns: Collection
},
forEach: function (callback, thisObject) {
// summary:
// Iterates over the query results, based on
// https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/forEach.
// Note that this may executed asynchronously (in which case it will return a promise),
// and the callback may be called after this function returns.
// callback:
// Function that is called for each object in the query results
// thisObject:
// The object to use as |this| in the callback.
// returns:
// undefined|Promise
},
fetch: function () {
// summary:
// This can be called to materialize and request the data behind this collection.
// Often collections may be lazy, and won't retrieve their underlying data until
// forEach or fetch is called. This returns an array, or for asynchronous stores,
// this will return a promise, resolving to an array of objects, once the
// operation is complete.
// returns Array|Promise
},
on: function (type, listener) {
// summary:
// This registers a callback for notification of when data is modified in the query results.
// type: String
// There are four types of events defined in this API:
// - add - A new object was added
// - update - An object was updated
// - delete - An object was deleted
// listener: Function
// The listener function is called when objects in the query results are modified
// to affect the query result. The listener function is called with a single event object argument:
// | listener(event);
//
// - The event object as the following properties:
// - type - The event type (of the four above)
// - target - This indicates the object that was create or modified.
// - id - If an object was removed, this indicates the object that was removed.
// The next two properties will only be available if array tracking is employed,
// which is usually provided by dstore/Trackable
// - previousIndex - The previousIndex parameter indicates the index in the result array where
// the object used to be. If the value is -1, then the object is an addition to
// this result set (due to a new object being created, or changed such that it
// is a part of the result set).
// - index - The inex parameter indicates the index in the result array where
// the object should be now. If the value is -1, then the object is a removal
// from this result set (due to an object being deleted, or changed such that it
// is not a part of the result set).
}
});
Collection.SortInformation = declare(null, {
// summary:
// An object describing what property to sort on, and the direction of the sort.
// property: String
// The name of the property to sort on.
// descending: Boolean
// The direction of the sort. Default is false.
});
Store.Collection = Collection;
Store.PutDirectives = declare(null, {
// summary:
// Directives passed to put() and add() handlers for guiding the update and
// creation of stored objects.
// id: String|Number?
// Indicates the identity of the object if a new object is created
// beforeId: String?
// If the collection of objects in the store has a natural ordering,
// this indicates that the created or updated object should be placed before the
// object whose identity is specified as the value of this property. A value of null indicates that the
// object should be last.
// parent: Object?,
// If the store is hierarchical (with single parenting) this property indicates the
// new parent of the created or updated object.
// overwrite: Boolean?
// If this is provided as a boolean it indicates that the object should or should not
// overwrite an existing object. A value of true indicates that a new object
// should not be created, the operation should update an existing object. A
// value of false indicates that an existing object should not be updated, a new
// object should be created (which is the same as an add() operation). When
// this property is not provided, either an update or creation is acceptable.
});
Store.Transaction = declare(null, {
// summary:
// This is an object returned from transaction() calls that represents the current
// transaction.
commit: function () {
// summary:
// Commits the transaction. This may throw an error if it fails. Of if the operation
// is asynchronous, it may return a promise that represents the eventual success
// or failure of the commit.
},
abort: function (callback, thisObject) {
// summary:
// Aborts the transaction. This may throw an error if it fails. Of if the operation
// is asynchronous, it may return a promise that represents the eventual success
// or failure of the abort.
}
});
var __QueryLogEntry = {
type: String
The query type
arguments: Array
The original query arguments
normalizedArguments: Array
The normalized query arguments
querier: Function?
A client-side implementation of the query that takes an item array and returns an item array
};
====*/
@@ -0,0 +1,426 @@
define([
'dojo/_base/lang',
'dojo/_base/declare',
'dojo/aspect',
'dojo/when',
'dojo/promise/all',
'dojo/_base/array',
'dojo/on'
/*=====, './api/Store' =====*/
], function (lang, declare, aspect, when, whenAll, arrayUtil, on /*=====, Store =====*/) {
// module:
// dstore/Trackable
var revision = 0;
function createRange(newStart, newEnd) {
return {
start: newStart,
count: newEnd - newStart
};
}
function registerRange(ranges, newStart, newEnd) {
for (var i = ranges.length - 1; i >= 0; --i) {
var existingRange = ranges[i],
existingStart = existingRange.start,
existingEnd = existingStart + existingRange.count;
if (newStart > existingEnd) {
// existing range completely precedes new range. we are done.
ranges.splice(i + 1, 0, createRange(newStart, newEnd));
return;
} else if (newEnd >= existingStart) {
// the ranges overlap and must be merged into a single range
newStart = Math.min(newStart, existingStart);
newEnd = Math.max(newEnd, existingEnd);
ranges.splice(i, 1);
}
}
ranges.unshift(createRange(newStart, newEnd));
}
function unregisterRange(ranges, start, end) {
for (var i = 0, range; (range = ranges[i]); ++i) {
var existingStart = range.start,
existingEnd = existingStart + range.count;
if (start <= existingStart) {
if (end >= existingEnd) {
// The existing range is within the forgotten range
ranges.splice(i, 1);
} else {
// The forgotten range overlaps the beginning of the existing range
range.start = end;
range.count = existingEnd - range.start;
// Since the forgotten range ends before the existing range,
// there are no more ranges to update, and we are done
return;
}
} else if (start < existingEnd) {
if (end > existingStart) {
// The forgotten range is within the existing range
ranges.splice(i, 1, createRange(existingStart, start), createRange(end, existingEnd));
// We are done because the existing range bounded the forgotten range
return;
} else {
// The forgotten range overlaps the end of the existing range
range.count = start - range.start;
}
}
}
}
var trackablePrototype = {
track: function () {
var store = this.store || this;
// monitor for updates by listening to these methods
var handles = [];
var eventTypes = {add: 1, update: 1, 'delete': 1};
// register to listen for updates
for (var type in eventTypes) {
handles.push(
this.on(type, (function (type) {
return function (event) {
notify(type, event);
};
})(type))
);
}
function makeFetch() {
return function () {
var self = this;
var fetchResults = this.inherited(arguments);
when(fetchResults, function (results) {
results = self._results = results.slice();
if (self._partialResults) {
// clean this up, as we don't need this anymore
self._partialResults = null;
}
self._ranges = [];
registerRange(self._ranges, 0, results.length);
});
return fetchResults;
};
}
function makeFetchRange() {
return function (kwArgs) {
var self = this,
start = kwArgs.start,
end = kwArgs.end,
fetchResults = this.inherited(arguments);
// only use this if we don't have all the data
if (!this._results) {
when(fetchResults, function (results) {
return when(results.totalLength, function (totalLength) {
var partialResults = self._partialResults || (self._partialResults = []);
end = Math.min(end, start + results.length);
partialResults.length = totalLength;
// copy the new ranged data into the parent partial data set
var spliceArgs = [ start, end - start ].concat(results);
partialResults.splice.apply(partialResults, spliceArgs);
registerRange(self._ranges, start, end);
return results;
});
});
}
return fetchResults;
};
}
// delegate rather than call _createSubCollection because we are not ultimately creating
// a new collection, just decorating an existing collection with item index tracking.
// If we use _createSubCollection, it will return a new collection that may exclude
// important, defining properties from the tracked collection.
var observed = declare.safeMixin(lang.delegate(this), {
_ranges: [],
fetch: makeFetch(),
fetchRange: makeFetchRange(),
releaseRange: function (start, end) {
if (this._partialResults) {
unregisterRange(this._ranges, start, end);
for (var i = start; i < end; ++i) {
delete this._partialResults[i];
}
}
},
on: function (type, listener) {
var self = this,
inheritedOn = this.getInherited(arguments);
return on.parse(observed, type, listener, function (target, type) {
return type in eventTypes ?
aspect.after(observed, 'on_tracked' + type, listener, true) :
inheritedOn.call(self, type, listener);
});
},
tracking: {
remove: function () {
while (handles.length > 0) {
handles.pop().remove();
}
this.remove = function () {};
}
},
// make sure track isn't called twice
track: null
});
if (this.fetchSync) {
// only add these if we extending a sync-capable store
declare.safeMixin(observed, {
fetchSync: makeFetch(),
fetchRangeSync: makeFetchRange()
});
// we take the presence of fetchSync to indicate that the results can be
// retrieved cheaply, and then we can just automatically fetch and start
// tracking results
observed.fetchSync();
}
// Create a function that applies all queriers in the query log
// in order to determine whether a new or updated item belongs
// in the results and at what position.
var queryExecutor;
arrayUtil.forEach(this.queryLog, function (entry) {
var existingQuerier = queryExecutor,
querier = entry.querier;
if (querier) {
queryExecutor = existingQuerier
? function (data) { return querier(existingQuerier(data)); }
: querier;
}
});
var defaultEventProps = {
'add': { index: undefined },
'update': { previousIndex: undefined, index: undefined },
'delete': { previousIndex: undefined }
},
findObject = function (data, id, start, end) {
start = start !== undefined ? start : 0;
end = end !== undefined ? end : data.length;
for (var i = start; i < end; ++i) {
if (store.getIdentity(data[i]) === id) {
return i;
}
}
return -1;
};
function notify(type, event) {
revision++;
var target = event.target;
event = lang.delegate(event, defaultEventProps[type]);
when(observed._results || observed._partialResults, function (resultsArray) {
/* jshint maxcomplexity: 32 */
function emitEvent() {
// TODO: Eventually we will want to aggregate all the listener events
// in an event turn, but we will wait until we have a reliable, performant queueing
// mechanism for this (besides setTimeout)
var method = observed['on_tracked' + type];
method && method.call(observed, event);
}
if (!resultsArray) {
// without data, we have no way to determine the indices effected by the change,
// so just pass along the event and return.
emitEvent();
return;
}
var i, j, l, ranges = observed._ranges, range;
/*if(++queryRevision != revision){
throw new Error('Query is out of date, you must observe() the' +
' query prior to any data modifications');
}*/
var targetId = 'id' in event ? event.id : store.getIdentity(target);
var removedFrom = -1,
removalRangeIndex = -1,
insertedInto = -1,
insertionRangeIndex = -1;
if (type === 'delete' || type === 'update') {
// remove the old one
for (i = 0; removedFrom === -1 && i < ranges.length; ++i) {
range = ranges[i];
for (j = range.start, l = j + range.count; j < l; ++j) {
var object = resultsArray[j];
// often ids can be converted strings (if they are used as keys in objects),
// so we do a coercive equality check
/* jshint eqeqeq: false */
if (store.getIdentity(object) == targetId) {
removedFrom = event.previousIndex = j;
removalRangeIndex = i;
resultsArray.splice(removedFrom, 1);
range.count--;
for (j = i + 1; j < ranges.length; ++j) {
ranges[j].start--;
}
break;
}
}
}
}
if (type === 'add' || type === 'update') {
if (queryExecutor) {
// with a queryExecutor, we can determine the correct sorted index for the change
if (queryExecutor([target]).length) {
var begin = 0,
end = ranges.length - 1,
sampleArray,
candidateIndex = -1,
sortedIndex,
adjustedIndex;
while (begin <= end && insertedInto === -1) {
// doing a binary search for the containing range
i = begin + Math.round((end - begin) / 2);
range = ranges[i];
sampleArray = resultsArray.slice(range.start, range.start + range.count);
if ('beforeId' in event) {
candidateIndex = event.beforeId === null
? sampleArray.length
: findObject(sampleArray, event.beforeId);
}
if (candidateIndex === -1) {
// If the original index came from this range, put back in the original slot
// so it doesn't move unless it needs to (relying on a stable sort below)
if (removedFrom >= Math.max(0, range.start - 1)
&& removedFrom <= (range.start + range.count)) {
candidateIndex = removedFrom;
} else {
candidateIndex = store.defaultNewToStart ? 0 : sampleArray.length;
}
}
sampleArray.splice(candidateIndex, 0, target);
sortedIndex = arrayUtil.indexOf(queryExecutor(sampleArray), target);
adjustedIndex = range.start + sortedIndex;
if (sortedIndex === 0 && range.start !== 0) {
end = i - 1;
} else if (sortedIndex >= (sampleArray.length - 1) &&
adjustedIndex < resultsArray.length) {
begin = i + 1;
} else {
insertedInto = adjustedIndex;
insertionRangeIndex = i;
}
}
if (insertedInto === -1 && begin > 0 && begin < ranges.length) {
var betweenRanges = true;
}
}
} else {
// we don't have a queryExecutor, so we can't provide any information
// about where it was inserted or moved to. If it is an update, we leave
// its position alone. otherwise, we at least indicate a new object
var range,
possibleRangeIndex = -1;
if ('beforeId' in event) {
if (event.beforeId === null) {
insertedInto = resultsArray.length;
possibleRangeIndex = ranges.length - 1;
} else {
for (i = 0, l = ranges.length; insertionRangeIndex === -1 && i < l; ++i) {
range = ranges[i];
insertedInto = findObject(
resultsArray,
event.beforeId,
range.start,
range.start + range.count
);
if (insertedInto !== -1) {
insertionRangeIndex = i;
}
}
}
} else {
if (type === 'update') {
insertedInto = removedFrom;
insertionRangeIndex = removalRangeIndex;
} else {
if (store.defaultNewToStart) {
insertedInto = 0;
possibleRangeIndex = 0;
} else {
// default to the bottom
insertedInto = resultsArray.length;
possibleRangeIndex = ranges.length - 1;
}
}
}
if (possibleRangeIndex !== -1 && insertionRangeIndex === -1) {
range = ranges[possibleRangeIndex];
if (range && range.start <= insertedInto
&& insertedInto <= (range.start + range.count)) {
insertionRangeIndex = possibleRangeIndex;
}
}
}
// an item only truly has a known index if it is in a known range
if (insertedInto > -1 && insertionRangeIndex > -1) {
event.index = insertedInto;
resultsArray.splice(insertedInto, 0, target);
// update the count and start of the appropriate ranges
ranges[insertionRangeIndex].count++;
for (i = insertionRangeIndex + 1; i < ranges.length; ++i) {
ranges[i].start++;
}
} else if (betweenRanges) {
// the begin index will be after the inserted item, and is
// where we can begin incrementing start values
event.beforeIndex = ranges[begin].start;
for (i = begin; i < ranges.length; ++i) {
ranges[i].start++;
}
}
}
// update the total
event.totalLength = resultsArray.length;
emitEvent();
});
}
return observed;
}
};
var Trackable = declare(null, trackablePrototype);
Trackable.create = function (target, properties) {
// create a delegate of an existing store with trackability functionality mixed in
target = declare.safeMixin(lang.delegate(target), trackablePrototype);
declare.safeMixin(target, properties);
return target;
};
return Trackable;
});
@@ -0,0 +1,42 @@
define([
'dojo/_base/declare'
/*=====, 'dstore/Store'=====*/
], function (declare /*=====, Store=====*/) {
return declare(null, {
constructor: function () {
this.root = this;
},
mayHaveChildren: function (object) {
// summary:
// Check if an object may have children
// description:
// This method is useful for eliminating the possibility that an object may have children,
// allowing collection consumers to determine things like whether to render UI for child-expansion
// and whether a query is necessary to retrieve an object's children.
// object:
// The potential parent
// returns: boolean
return 'hasChildren' in object ? object.hasChildren : true;
},
getRootCollection: function () {
// summary:
// Get the collection of objects with no parents
// returns: dstore/Store.Collection
return this.root.filter({ parent: null });
},
getChildren: function (object) {
// summary:
// Get a collection of the children of the provided parent object
// object:
// The parent object
// returns: dstore/Store.Collection
return this.root.filter({ parent: this.getIdentity(object) });
}
});
});
@@ -0,0 +1,21 @@
{
"name": "dstore",
"description": "A data collection infrastructure",
"keywords": [
"dojo",
"store"
],
"repository": {
"type": "git",
"url": "git://github.com/SitePen/dstore"
},
"dependencies": {
"dojo": ">=1.8.1"
},
"ignore": [
".*",
"tests"
],
"license": [ "AFL-2.1", "BSD-3-Clause" ],
"moduleType": [ "amd" ]
}
@@ -0,0 +1,93 @@
define([
'dojo/_base/lang',
'dojo/_base/declare',
'dojo/_base/array'
], function (lang, declare, array) {
return declare(null, {
constructor: function (collection, value) {
// summary:
// Series adapter for dstore object stores.
// collection: dstore/api/Store.Collection
// A dstore object store.
// value: Function|Object|String
// Function, which takes an object handle, and
// produces an output possibly inspecting the store's item. Or
// a dictionary object, which tells what names to extract from
// an object and how to map them to an output. Or a string, which
// is a numeric field name to use for plotting. If undefined, null
// or empty string (the default), "value" field is extracted.
this.collection = collection.track ? collection.track() : collection;
if (value) {
if (typeof value === 'function') {
this.value = value;
} else if (typeof value === 'object') {
this.value = function (object) {
var o = {};
for (var key in value) {
o[key] = object[value[key]];
}
return o;
};
} else {
this.value = function (object) {
return object[value];
};
}
} else {
this.value = function (object) {
return object.value;
};
}
this.data = [];
this._initialRendering = false;
this.fetch();
},
destroy: function () {
// summary:
// Clean up before GC.
var tracking = this.collection.tracking;
tracking && tracking.remove();
},
setSeriesObject: function (series) {
// summary:
// Sets a dojox.charting.Series object we will be working with.
// series: dojox/charting/Series
// Our interface to the chart.
this.series = series;
},
fetch: function () {
// summary:
// Fetches data from the store and updates a chart.
var collection = this.collection,
update = lang.hitch(this, this._update);
collection.fetch().then(lang.hitch(this, function (results) {
this.objects = results;
update();
if (collection.tracking) {
collection.on('add, update, delete', update);
}
}));
},
_update: function () {
var self = this;
this.data = array.map(this.objects, function (object) {
return self.value(object, self.collection);
});
if (this.series) {
this.series.chart.updateSeries(this.series.name, this, this._initialRendering);
this._initialRendering = false;
this.series.chart.delayedRender();
}
}
});
});
@@ -0,0 +1,814 @@
define([
'dojo/_base/declare',
'dojo/_base/lang',
'dojo/Deferred',
'dojo/when',
'dojo/promise/all',
'dstore/Store',
'dstore/SimpleQuery',
'dstore/QueryResults'
], function (declare, lang, Deferred, when, all, Store, SimpleQuery, QueryResults) {
function makePromise(request) {
var deferred = new Deferred();
request.onsuccess = function (event) {
deferred.resolve(event.target.result);
};
request.onerror = function () {
request.error.message = request.webkitErrorMessage;
deferred.reject(request.error);
};
return deferred.promise;
}
// we keep a queue of cursors, so we can prioritize the traversal of result sets
var cursorQueue = [];
var maxConcurrent = 1;
var cursorsRunning = 0;
function queueCursor(cursor, priority, retry) {
// process the cursor queue, possibly submitting a cursor for continuation
if (cursorsRunning || cursorQueue.length) {
// actively processing
if (cursor) {
// add to queue
cursorQueue.push({cursor: cursor, priority: priority, retry: retry});
// keep the queue in priority order
cursorQueue.sort(function(a, b) {
return a.priority > b.priority ? 1 : -1;
});
}
if (cursorsRunning >= maxConcurrent) {
return;
}
var cursorObject = cursorQueue.pop();
cursor = cursorObject && cursorObject.cursor;
}//else nothing in the queue, just shortcut directly to continuing the cursor
if (cursor) {
try {
// submit the continuation of the highest priority cursor
cursor['continue']();
cursorsRunning++;
} catch(e) {
if ((e.name === 'TransactionInactiveError' || e.name === 0) && cursorObject) { // == 0 is IndexedDBShim
// if the cursor has been interrupted we usually need to create a new transaction,
// handing control back to the query/filter function to open the cursor again
cursorObject.retry();
} else {
throw e;
}
}
}
}
function yes() {
return true;
}
var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;
return declare([Store, SimpleQuery], {
// summary:
// This is a basic store for IndexedDB. It implements dojo/store/api/Store.
constructor: function (options) {
// summary:
// This is a basic store for IndexedDB.
// options:
// This provides any configuration information that will be mixed into the store
declare.safeMixin(this, options);
var store = this;
var dbConfig = this.dbConfig;
this.indices = dbConfig.stores[this.storeName];
this.cachedCount = {};
for (var index in store.indices) {
var value = store.indices[index];
if (typeof value === 'number') {
store.indices[index] = {
preference: value
};
}
}
this.db = this.db || dbConfig.db;
if (!this.db) {
var openRequest = dbConfig.openRequest;
if (!openRequest) {
openRequest = dbConfig.openRequest = window.indexedDB.open(dbConfig.name || 'dojo-db',
parseInt(dbConfig.version, 10));
openRequest.onupgradeneeded = function () {
var db = store.db = openRequest.result;
for (var storeName in dbConfig.stores) {
var storeConfig = dbConfig.stores[storeName];
if (!db.objectStoreNames.contains(storeName)) {
var idProperty = storeConfig.idProperty || 'id';
var idbStore = db.createObjectStore(storeName, {
keyPath: idProperty,
autoIncrement: storeConfig[idProperty] &&
storeConfig[idProperty].autoIncrement || false
});
} else {
idbStore = openRequest.transaction.objectStore(storeName);
}
for (var index in storeConfig) {
if (!idbStore.indexNames.contains(index) && index !== 'autoIncrement' &&
storeConfig[index].indexed !== false) {
idbStore.createIndex(index, index, storeConfig[index]);
}
}
}
};
dbConfig.db = makePromise(openRequest);
}
this.db = dbConfig.db.then(function(db) {
return (store.db = db);
});
}
},
// idProperty: String
// Indicates the property to use as the identity property. The values of this
// property should be unique.
idProperty: 'id',
storeName: '',
// indices:
// a hash of the preference of indices, indices that are likely to have very
// unique values should have the highest numbers
// as a reference, sorting is always set at 1, so properties that are higher than
// one will trigger filtering with index and then sort the whole set.
// we recommend setting boolean values at 0.1.
indices: {
/*
property: {
preference: 1,
multiEntry: true
}
*/
},
transaction: function () {
var store = this;
this._currentTransaction = null;// get rid of the last transaction
return {
abort: function () {
store._currentTransaction.abort();
},
commit: function () {
// noop, idb does auto-commits
store._currentTransaction = null;// get rid of the last transaction
}
};
},
_getTransaction: function () {
if (!this._currentTransaction) {
if (!this.db) {
console.error('The database has not been initialized yet');
}
this._currentTransaction = this.db.transaction([this.storeName], 'readwrite');
var store = this;
this._currentTransaction.oncomplete = function () {
// null it out so we will use a new one next time
store._currentTransaction = null;
};
this._currentTransaction.onerror = function (error) {
console.error(error);
};
}
return this._currentTransaction;
},
_callOnStore: function (method, args, index, returnRequest) {
// calls a method on the IndexedDB store
var store = this;
return when(this.db, function callOnStore(db) {
if (store.db.then) {
// replace the promise with the database itself
store.db = db;
}
var currentTransaction = store._currentTransaction;
if (currentTransaction) {
var allowRetry = true;
} else {
currentTransaction = store._getTransaction();
}
var request, idbStore;
if (allowRetry) {
try {
idbStore = currentTransaction.objectStore(store.storeName);
if (index) {
idbStore = idbStore.index(index);
}
request = idbStore[method].apply(idbStore, args);
} catch(e) {
if (e.name === 'TransactionInactiveError' || e.name === 'InvalidStateError') {
store._currentTransaction = null;
//retry
return callOnStore();
} else {
throw e;
}
}
} else {
idbStore = currentTransaction.objectStore(store.storeName);
if (index) {
idbStore = idbStore.index(index);
}
request = idbStore[method].apply(idbStore, args);
}
return returnRequest ? request : makePromise(request);
});
},
get: function (id) {
// summary:
// Retrieves an object by its identity.
// id: Number
// The identity to use to lookup the object
// options: Object?
// returns: dojo//Deferred
var store = this;
return this._callOnStore('get',[id]).then(function (object) {
return store._restore(object);
});
},
getIdentity: function (object) {
// summary:
// Returns an object's identity
// object: Object
// The object to get the identity from
// returns: Number
return object[this.idProperty];
},
put: function (object, options) {
// summary:
// Stores an object.
// object: Object
// The object to store.
// options: __PutDirectives?
// Additional metadata for storing the data. Includes an "id"
// property if a specific id is to be used.
// returns: dojo/Deferred
options = options || {};
this.cachedCount = {}; // clear the count cache
var store = this;
return this._callOnStore(options.overwrite === false ? 'add' : 'put',[object])
.then(function (object) {
return store._restore(object);
});
},
add: function (object, options) {
// summary:
// Adds an object.
// object: Object
// The object to store.
// options: __PutDirectives?
// Additional metadata for storing the data. Includes an "id"
// property if a specific id is to be used.
// returns: dojo/Deferred
options = options || {};
options.overwrite = false;
return this.put(object, options);
},
remove: function (id) {
// summary:
// Deletes an object by its identity.
// id: Number
// The identity to use to delete the object
// returns: dojo/Deferred
this.cachedCount = {}; // clear the count cache
return this._callOnStore('delete', [id]);
},
fetch: function () {
return this._fetch(yes);
},
fetchRange: function (range) {
return this._fetch(yes, range);
},
forEach: function (callback, thisObject) {
return this._fetch(function (object, index) {
callback.call(thisObject, object, index);
});
},
_union: function (query, callback, fetchOptions) {
// perform a union query
fetchOptions = fetchOptions || {};
var start = fetchOptions.start || 0;
var end = fetchOptions.end || Infinity;
var sortOption = query.sort;
var select = query.select;
var parts = query.filter;
var sorter, sortOptions;
if (sortOption) {
sortOptions = {sort: sortOption};
sorter = this._createSortQuerier(sortOptions);
} else {
sorter = function (data) {
// no sorting
return data;
};
}
var totals = [];
var collectedCount = 0;
var inCount = 0;
var index = 0;
var queues = [];
var done;
var collected = {};
var results = [];
var store = this;
// wait for all the union segments to complete
return new QueryResults(when(parts).then(
function(parts){
return all(parts.map(function(part, i) {
var queue = queues[i] = [];
function addToQueue(object) {
// to the queue that is kept for each individual query for merge sorting
queue.push(object);
var nextInQueues = []; // so we can index of the selected choice
var toMerge = [];
while (queues.every(function(queue) {
if (queue.length > 0) {
var next = queue[0];
if (next) {
toMerge.push(next);
}
return nextInQueues.push(next);
}
})) {
if (index >= end || toMerge.length === 0) {
done = true;
return; // exit filter loop
}
var nextSelected = sorter(toMerge)[0];
// shift it off the selected queue
queues[nextInQueues.indexOf(nextSelected)].shift();
if (index++ >= start) {
results.push(nextSelected);
if (!callback(nextSelected)) {
done = true;
return;
}
}
nextInQueues = [];// reset
toMerge = [];
}
return true;
}
var queryResults = store._query({
filterFunction: query.filterFunction,
filter: part,
sort: sortOption
}, function (object) {
if (done) {
return;
}
var id = store.getIdentity(object);
inCount++;
if (id in collected) {
return true;
}
collectedCount++;
collected[id] = true;
return addToQueue(object);
});
totals[i] = queryResults.totalLength;
return queryResults.then(function(results) {
// null signifies the end of this particular query result
addToQueue(null);
return results;
});
}));
}
).then(function() {
if (select) {
return select.querier(results);
}
return results;
}), {
totalLength: {
then: function () {
// do it lazily again
return all(totals).then(function(totals) {
return totals.reduce(function(a, b) {
return a + b;
}) * collectedCount / (inCount || 1);
}).then.apply(this, arguments);
}
}
});
},
_normalizeQuery: function () {
// normalize the operators to a single query object
var filter;
var union;
var filterQuery = {};
var filterOperator;
var sortOption;
var select;
var filterPromises = [];
// iterate through the query log, applying each querier
this.queryLog.forEach(function (entry) {
var type = entry.type;
var args = entry.normalizedArguments;
if (type === 'filter') {
filterOperator = args;
var oldFilter = filter;
filter = oldFilter ? function (data) {
return entry.querier(oldFilter(data));
} : entry.querier;
processFilter(args, true);
} else if (type === 'sort') {
sortOption = args[0];
sortOption.querier = entry.querier;
} else if (type === 'select') {
select = entry;
}
});
function processFilter(filterArgs) {
// iterate through the query log, applying each querier
filterArgs.forEach(function processFilterArg(entry) {
if (union) {
throw new Error('Can not process conditions after a union, rearrange the query with the union last');
}
var type = entry.type;
var args = [].slice.call(entry.args, 0);
var name = args[0];
var value = args[1];
if (value && value.fetch && !value.data) {
// the value is a collection, we will fetch it
filterPromises.push(value.fetch().then(function (fetched) {
value.data = fetched;
processFilterArg(entry);
}, function (error) {
console.error('Failed to retrieved nested collection', error);
}));
return;
}
if (type === 'or') {
or(args);
} else if (type === 'and') {
processFilter(args);
} else if (type === 'eq') {
addCondition(name, value);
} else if (type === 'gt' || type === 'gte') {
var filterProperty = filterQuery[name] || (filterQuery[name] = {});
filterProperty.from = value;
filterProperty.excludeFrom = type === 'gt';
addCondition(name, filterProperty);
} else if (type === 'lt' || type === 'lte') {
var filterProperty = filterQuery[name] || (filterQuery[name] = {});
filterProperty.to = value;
filterProperty.excludeTo = type === 'lt';
addCondition(name, filterProperty);
} else if (type === 'in') {
// split this into a union of equals
or((value.data || value).map(function (item) {
return {
type: 'eq',
args: [name, item]
};
}));
} else if (type === 'contains') {
var filterProperty = filterQuery[name] || (filterQuery[name] = {});
filterProperty.contains = value.data ? value.data : value instanceof Array ? value : [value];
addCondition(name, filterProperty);
} else if (type === 'match') {
value = value.source;
if (value[0] === '^' && !value.match(/[\{\}\(\)\[\]\.\,\$\*]/)) {
var filterProperty = filterQuery[name] || (filterQuery[name] = {});
value = value.slice(1);
filterProperty.from = value;
filterProperty.to = value + '~';
addCondition(name, filterProperty);
} else {
throw new Error('The match filter only supports simple prefix matching like /^starts with/');
}
} else {
throw new Error('Unsupported filter type "' + type + '"');
}
});
}
filter = filter || function (data) {
return data;
};
// an array, do a union (note that indexeddb only runs on modern browsers)
function or(args) {
union = args.map(function (arg) {
filterQuery = {};
processFilter([arg]);
return filterQuery;
});
}
function addCondition(key, filterValue) {
// test all the filters as possible indices to drive the query
var range = false;
if (typeof filterValue === 'boolean') {
// can't use booleans as filter keys
return;
}
if (filterValue) {
if (filterValue.from || filterValue.to) {
range = true;
(function(from, to) {
// convert a to/from object to a testable object with a keyrange
filterValue.test = function (value) {
return !from || from <= value &&
(!to || to >= value);
};
filterValue.keyRange = from ?
to ?
IDBKeyRange.bound(from, to, filterValue.excludeFrom, filterValue.excludeTo) :
IDBKeyRange.lowerBound(from, filterValue.excludeFrom) :
IDBKeyRange.upperBound(to, filterValue.excludeTo);
})(filterValue.from, filterValue.to);
} else if (typeof filterValue === 'object' && filterValue.contains) {
// contains is for matching any value in a given array to any value in the target indices array
// this expects a multiEntry: true index
(function(contains) {
var keyRange, first = contains[0];
if (typeof first === 'object' && first.type === 'match') {
if (first.args[1].source[0] === '^') {
var value = first.args[1].source.slice(1);
keyRange = IDBKeyRange.bound(value, value + '~');
}
} else {
keyRange = IDBKeyRange.only(first);
}
filterValue.test = function (value) {
return contains.every(function(item) {
if (typeof item === 'object' && item.type === 'match') {
// regular expression
var regex = item.args[1];
return value.some(function (value) {
return !!regex.test(value);
});
}
return value && value.indexOf(item) > -1;
} );
};
filterValue.keyRange = keyRange;
})(filterValue.contains);
}
}
filterQuery[key] = filterValue;
}
return {
filter: filterPromises.length > 0 ? all(filterPromises).then(function () {
return union;
}) : (union || filterQuery),
filterFunction: filter,
filterOperator: filterOperator || null,
sort: sortOption,
select: select
};
},
_fetch: function (callback, fetchOptions) {
var query = this._normalizeQuery();
var filterQuery = query.filter;
var store = this;
if (filterQuery instanceof Array || filterQuery.then) {
return this._union(query, function (object) {
callback(store._restore(object));
// keep going
return true;
}, fetchOptions);
}
return this._query(query, function (object) {
callback(store._restore(object));
// keep going
return true;
}, fetchOptions);
},
_query: function (query, callback, fetchOptions) {
// summary:
// Queries the store for objects.
// query: Object
// The query to use for retrieving objects from the store.
fetchOptions = fetchOptions || {};
var store = this;
var start = fetchOptions.start || 0;
var end = fetchOptions.end || Infinity;
var keyRange;
var alreadySearchedProperty;
var advance;
var bestIndex, bestIndexQuality = 0;
var indexTries = 0;
var filterValue;
var deferred = new Deferred();
var resultsPromise = deferred.promise;
var filter = query.filterFunction;
var filterQuery = query.filter;
var sortOption = query.sort;
var select = query.select;
function tryIndex(indexName, quality, factor) {
indexTries++;
var indexDefinition = store.indices[indexName];
if (indexDefinition && indexDefinition.indexed !== false) {
quality = quality || indexDefinition.preference * (factor || 1) || 0.001;
if (quality > bestIndexQuality) {
bestIndexQuality = quality;
bestIndex = indexName;
return true;
}
}
indexTries++;
}
for (var key in filterQuery) {
var value = filterQuery[key];
tryIndex(key, null, value && (value.from || value.to) ? 0.1 : 1);
}
var queryId = JSON.stringify(query.filterOperator) + '-' + JSON.stringify(sortOption);
var descending;
if (sortOption) {
// this isn't necessarily the best heuristic to determine the best index
var mainSort = sortOption[0];
if (mainSort.property === bestIndex || tryIndex(mainSort.property, 1)) {
descending = mainSort.descending;
} else {
// we need to sort afterwards now
var postSorting = true;
// we have to retrieve everything in this case
start = 0;
end = Infinity;
}
}
var cursorRequestArgs;
if (bestIndex) {
if (bestIndex in filterQuery) {
// we are filtering
filterValue = filterQuery[bestIndex];
if (filterValue && (filterValue.keyRange)) {
keyRange = filterValue.keyRange;
} else {
keyRange = IDBKeyRange.only(filterValue);
}
alreadySearchedProperty = bestIndex;
} else {
keyRange = null;
}
cursorRequestArgs = [keyRange, descending ? 'prev' : 'next'];
} else {
// no index, no arguments required
cursorRequestArgs = [];
}
var cachedPosition = store.cachedPosition;
if (cachedPosition && cachedPosition.queryId === queryId &&
cachedPosition.offset < start && indexTries > 1) {
advance = cachedPosition.preFilterOffset + 1;
// make a new copy, so we don't have concurrency issues
store.cachedPosition = cachedPosition = lang.mixin({}, cachedPosition);
} else {
// cache of the position, tracking our traversal progress
cachedPosition = store.cachedPosition = {
offset: -1,
preFilterOffset: -1,
queryId: queryId
};
if (indexTries < 2) {
// can skip to advance
cachedPosition.offset = cachedPosition.preFilterOffset = (advance = start) - 1;
}
}
// this is adjusted so we can compute the total more accurately
var totalLength = {
then: function (callback, errback) {
// make this a lazy promise, only executing if we need to
var cachedCount = store.cachedCount[queryId];
if (cachedCount) {
return callback(adjustTotal(cachedCount));
} else {
if (cachedPosition.finished) {
// we have aleady finished traversing the results, can provide the exact count immediately
var deferred = new Deferred();
deferred.resolve(cachedPosition.offset + 1);
return deferred.then(callback);
}
var countPromise = (keyRange ? store._callOnStore('count', [keyRange], bestIndex) : store._callOnStore('count'));
return countPromise.then(adjustTotal).then(callback, errback);
}
function adjustTotal(total) {
// we estimate the total count base on the matching rate
store.cachedCount[queryId] = total;
return Math.round((cachedPosition.offset + 1.01) / (cachedPosition.preFilterOffset + 1.01) * total);
}
}
};
// this is main implementation of the the query results traversal, forEach and map use this method
var all = [];
function openCursor() {
// get the cursor
when(store._callOnStore('openCursor', cursorRequestArgs, bestIndex, true), function (cursorRequest) {
// this will be called for each iteration in the traversal
cursorsRunning++;
cursorRequest.onsuccess = function (event) {
cursorsRunning--;
var cursor = event.target.result;
if (cursor) {
if (advance) {
// we can advance through and wait for the completion
cursor.advance(advance);
cursorsRunning++;
advance = false;
return;
}
cachedPosition.preFilterOffset++;
try {
var item = cursor.value;
if (fetchOptions.join) {
item = fetchOptions.join(item);
}
return when(item, function (item) {
if (filter([item]).length > 0) {
cachedPosition.offset++;
if (cachedPosition.offset >= start) { // make sure we are after the start
all.push(item);
if (!callback(item, cachedPosition.offset - start) ||
cachedPosition.offset >= end - 1) {
// finished
cursorRequest.lastCursor = cursor;
deferred.resolve(all);
queueCursor();
return;
}
}
}
// submit our cursor to the priority queue for continuation, now or when our turn comes up
return queueCursor(cursor, fetchOptions.priority, function () {
// retry function, that we provide to the queue to use
// if the cursor can't be continued due to interruption
// if called, open the cursor again, and continue from our current position
advance = cachedPosition.preFilterOffset;
openCursor();
});
});
} catch(e) {
deferred.reject(e);
}
} else {
if (!start || cachedPosition.offset >= start) {
cachedPosition.finished = true;
}
deferred.resolve(all);
}
// let any other cursors start executing now
queueCursor();
};
cursorRequest.onerror = onCursorError;
}, onCursorError);
function onCursorError(error) {
cursorsRunning--;
deferred.reject(error);
queueCursor();
}
}
openCursor();
if (postSorting) {
// we are using the index to do filtering, so we are going to have to sort the entire list
// we have to redirect the callback
var sortedCallback = callback;
callback = yes;
var resultsPromise = resultsPromise.then(function (filteredResults) {
// now apply the sort and reapply the range
var start = fetchOptions.start || 0;
var end = fetchOptions.end || Infinity;
var sorted = sortOption.querier(filteredResults).slice(start, end);
sorted.forEach(sortedCallback);
return sorted;
});
}
if (select) {
resultsPromise = resultsPromise.then(function(results) {
return select.querier(results);
});
}
return new QueryResults(resultsPromise, {totalLength: totalLength});
}
});
});
@@ -0,0 +1,37 @@
define([
'dojo/_base/declare',
'../Memory'
],
function(declare, Memory) {
// a very simple localStorage-based store, that basically loads everything
// in memory and executes on it there. localStorage doesn't really provide
// any real querying functionality, so might just as well do it all in memory
return declare([Memory], {
dbPrefix: 'dojo-db',
storeName: 'default',
constructor: function () {
// load all the data from the local storage
var data = [];
var prefix = this.prefix = this.dbPrefix + '-' + this.storeName + '-';
var store = this;
for (var i = 0, l = localStorage.length; i < l; i++) {
var key = localStorage.key(i);
if (key.slice(0, prefix.length) === prefix) {
data.push(store._restore(JSON.parse(localStorage.getItem(key))));
}
}
this.setData(data);
},
putSync: function (object) {
// addSync and all the async update methods eventually go through this
var result = this.inherited(arguments);
// prefix and store
localStorage.setItem(this.prefix + this.getIdentity(object), JSON.stringify(object));
return result;
},
removeSync: function (id) {
localStorage.removeItem(this.prefix + id);
return this.inherited(arguments);
}
});
});
@@ -0,0 +1,439 @@
define([
'dojo/_base/declare',
'dojo/Deferred',
'dojo/when',
'dstore/QueryResults',
'dstore/Store',
'dstore/SimpleQuery',
'dojo/_base/lang',
'dojo/promise/all'
], function (declare, Deferred, when, QueryResults, Store, SimpleQuery, lang, all) {
// summary:
// This module implements the dstore API using the WebSQL database
function safeSqlName(name) {
if (name.match(/[^\w_\.]/)) {
throw new URIError('Illegal column name ' + name);
}
return name;
}
function getProperty(object, property) {
var propertyPath = property.split('.');
var pathLength = propertyPath.length;
for (var i = 0; i < pathLength; i++) {
object = object && object[propertyPath[i]];
}
return object;
}
function escapeDot(column) {
return column.replace(/\./g, '_dot_');
}
var sqlOperators = {
'and' : ' AND ',
'or' : ' OR ',
'eq' : '=',
'ne' : '!=',
'lte' : '<=',
'gte' : '>=',
'lt' : '<',
'gt' : '>'
};
function convertExtra(object) {
// converts the 'extra' data on sql rows that can contain expando properties outside of the defined column
return object && object.__extra ? lang.mixin(object, JSON.parse(object.__extra)) : object;
}
return declare([Store, SimpleQuery], {
constructor: function (config) {
var dbConfig = config.dbConfig;
// open the database and get it configured
// args are short_name, version, display_name, and size
this.database = openDatabase(config.dbName || 'dojo-db', '1.0', 'dojo-db', 4*1024*1024);
var indexPrefix = this.indexPrefix = config.indexPrefix || 'idx_';
var storeName = config.table || config.storeName;
this.table = (config.table || config.storeName).replace(/[^\w]/g, '_');
var promises = []; // for all the structural queries
// the indices for this table
this.indices = dbConfig.stores[storeName];
this.repeatingIndices = {};
for (var index in this.indices) {
// we support multiEntry property to simulate the similar behavior in IndexedDB, we track these because we use the
if (this.indices[index].multiEntry) {
this.repeatingIndices[index] = true;
}
}
if (!dbConfig.available) {
// the configuration where we create any necessary tables and indices
for (var storeName in dbConfig.stores) {
var storeConfig = dbConfig.stores[storeName];
var table = storeName.replace(/[^\w]/g, '_');
// the __extra property contains any expando properties in JSON form
var idConfig = storeConfig[this.idProperty];
var indices = ['__extra', this.idProperty + ' ' + ((idConfig && idConfig.autoIncrement) ?
'INTEGER PRIMARY KEY AUTOINCREMENT' : 'PRIMARY KEY')];
var repeatingIndices = [this.idProperty];
for (var index in storeConfig) {
if (index != this.idProperty) {
indices.push(escapeDot(index));
}
}
promises.push(this.executeSql('CREATE TABLE IF NOT EXISTS ' + table+ ' ('
+ indices.join(',') +
')'));
for (var index in storeConfig) {
if (index != this.idProperty) {
if (storeConfig[index].multiEntry) {
// it is 'repeating' property, meaning that we expect it to have an array,
// and we want to index each item in the array
// we will search on it using a nested select
repeatingIndices.push(index);
var repeatingTable = table+ '_repeating_' + escapeDot(index);
promises.push(this.executeSql('CREATE TABLE IF NOT EXISTS ' +
repeatingTable + ' (id,value)'));
promises.push(this.executeSql('CREATE INDEX IF NOT EXISTS idx_' +
repeatingTable + '_id ON ' + repeatingTable + '(id)'));
promises.push(this.executeSql('CREATE INDEX IF NOT EXISTS idx_' +
repeatingTable + '_value ON ' + repeatingTable + '(value)'));
}else{
promises.push(this.executeSql('ALTER TABLE ' + table + ' ADD ' + escapeDot(index))
.then(null, function () {
/* suppress failed alter table statements*/
}));
// otherwise, a basic index will do
if (storeConfig[index].indexed !== false) {
promises.push(this.executeSql('CREATE INDEX IF NOT EXISTS ' + indexPrefix +
table + '_' + escapeDot(index) + ' ON ' + table + '(' + escapeDot(index) + ')'));
}
}
}
}
}
dbConfig.available = all(promises);
}
this.available = dbConfig.available;
},
idProperty: 'id',
selectColumns: ['*'],
get: function (id) {
// basic get() operation, query by id property
var store = this;
return when(this.executeSql('SELECT ' + this.selectColumns.join(',') + ' FROM ' +
this.table + ' WHERE ' + this.idProperty + '=?', [id]), function (result) {
return result.rows.length > 0 ? store._restore(convertExtra(result.rows.item(0))) : undefined;
});
},
getIdentity: function (object) {
return object[this.idProperty];
},
remove: function (id) {
return this.executeSql('DELETE FROM ' + this.table + ' WHERE ' + this.idProperty + '=?', [id]); // Promise
// TODO: remove from repeating rows too
},
identifyGeneratedKey: true,
add: function (object) {
// An add() wiill translate to an INSERT INTO in SQL
var params = [], vals = [], cols = [];
var extra = {};
var actionsWithId = [];
var store = this;
function addColumn(column, value, originalColumn) {
if (store.repeatingIndices[originalColumn || column]) {
// we will need to add to the repeating table for the given field/column,
// but it must take place after the insert, so we know the id
actionsWithId.push(function(id) {
return all(value.map(function(value) {
return store.executeSql('INSERT INTO ' + store.table + '_repeating_' +
column + ' (value, id) VALUES (?, ?)', [value, id]);
}));
});
}else{
// add to the columns and values for SQL statement
cols.push(column);
vals.push('?');
params.push(value);
}
}
for (var i in object) {
if (object.hasOwnProperty(i)) {
if (i in this.indices || i == this.idProperty) {
addColumn(i, object[i]);
}else{
extra[i] = object[i];
}
}
}
// now handle nested property values, so they can be indexed
for (var i in this.indices) {
if (i.indexOf('.') > -1) {
addColumn(escapeDot(i), getProperty(object, i), i);
}
}
// add the 'extra' expando data as well
cols.push('__extra');
vals.push('?');
params.push(JSON.stringify(extra));
var idColumn = this.idProperty;
if (this.identifyGeneratedKey) {
params.idColumn = idColumn;
}
var sql = 'INSERT INTO ' + this.table + ' (' + cols.join(',') + ') VALUES (' + vals.join(',') + ')';
return when(this.executeSql(sql, params), function (results) {
var idDefinition = store.indices[idColumn];
if (idDefinition && idDefinition.autoIncrement) {
var id = results.insertId;
object[idColumn] = id;
} else {
id = object[idColumn];
}
// got the id now, perform the insertions for the repeating data
return all(actionsWithId.map(function(func) {
return func(id);
})).then(function() {
return id;
});
});
},
put: function (object, directives) {
// put, if overwrite is not specified, we have to do a get() to determine
// if we need to do an INSERT INTO (via add), or an UPDATE
directives = directives || {};
var id = directives.id || object[this.idProperty];
var overwrite = directives.overwrite;
if (overwrite === undefined) {
// can't tell if we need to do an INSERT or UPDATE, do a get() to find out
var store = this;
return this.get(id).then(function(previous) {
if ((directives.overwrite = !!previous)) {
directives.overwrite = true;
return store.put(object, directives);
}else{
return store.add(object, directives);
}
});
}
if (!overwrite) {
return store.add(object, directives);
}
var sql = 'UPDATE ' + this.table + ' SET ';
var params = [];
var cols = [];
var extra = {};
var store = this;
function addColumn(column, value, originalColumn) {
if (store.repeatingIndices[originalColumn || column]) {
// update the repeating value tables
store.executeSql('DELETE FROM ' + store.table + '_repeating_' + column + ' WHERE id=?', [id]);
for (var j = 0; j < value.length; j++) {
store.executeSql('INSERT INTO ' + store.table + '_repeating_' + column + ' (value, id) VALUES (?, ?)',
[value[j], id]);
}
}else{
cols.push(column + '=?');
params.push(value);
}
}
for (var i in object) {
if (object.hasOwnProperty(i)) {
if (i in this.indices || i == this.idProperty) {
addColumn(i, object[i]);
}else{
extra[i] = object[i];
}
}
}
// now handle nested property values, so they can be indexed
for (var i in this.indices) {
if (i.indexOf('.') > -1) {
addColumn(escapeDot(i), getProperty(object, i), i);
}
}
cols.push('__extra=?');
params.push(JSON.stringify(extra));
// create the SETs for the SQL
sql += cols.join(',') + ' WHERE ' + this.idProperty + '=?';
params.push(object[this.idProperty]);
return when(this.executeSql(sql, params), function () {
return id;
});
},
fetchRange: function (options) {
return this.fetch(options);
},
generateSql: function () {
var from = 'FROM ' + this.table;
var condition = '';
var order = '';
var select = '*';
var selectEntry;
var store = this;
var table = this.table;
var params = [];
this.queryLog.forEach(function (query) {
if (query.type === 'filter') {
condition = (condition ? ' AND ' : '') + convertFilter(query.normalizedArguments[0]).join('');
} else if (query.type === 'sort') {
order = ' ORDER BY ' + query.normalizedArguments[0].map(function(sort) {
return sqlColumn(sort.property) + ' ' + (sort.descending ? 'desc' : 'asc');
}).join(',');
} else if (query.type === 'select') {
var selectArgument = query.normalizedArguments[0];
if (selectArgument instanceof Array) {
for (var i = 0; i < selectArgument.length; i++){
if (!(selectArgument[i] in store.indices)) {
// can't use SQL SELECT to do the select since extra properties may be involved
selectEntry = query;
return;
}
}
select = selectArgument.map(sqlColumn).join(', ');
} else {
if (selectArgument in store.indices) {
select = sqlColumn(selectArgument);
}
// must always apply the select function to map to a single value per item
selectEntry = query;
}
}
});
function sqlColumn(column) {
safeSqlName(column);
return table + '.' + escapeDot(column);
}
function convertFilter(filter) {
var args = filter.args;
var column = args[0];
var value = args[1];
switch(filter.type) {
case 'eq': case 'ne': case 'lt': case 'lte': case 'gt': case 'gte':
params.push(value);
return [sqlColumn(column), sqlOperators[filter.type] + '?'];
case 'and': case 'or':
var parts = [];
for (var index = 0; index < args.length; index++) {
parts.push(convertFilter(args[index]).join(''));
}
return ['(', parts.join(sqlOperators[filter.type]), ')'];
case 'in':
if (value.length === 0) {
// an empty IN clause is considered invalid SQL
return '0=1';
} else if (value.generateSql) {
var query = value.generateSql();
params.push.apply(params, query.params);
return [sqlColumn(column), ' IN (' + query.select + query.from + ')'];
} else {
return [sqlColumn(column), ' IN (' + args[1].map(function(value) {
params.push(value);
return '?';
}).join(',') + ')'];
}
break;
case 'contains':
var repeatingTable = table + '_repeating_' + column;
return ['(', value.map(function(value) {
var condition;
if (value && value.type) {
condition = 'value ' + convertFilter(value).slice(1).join('');
}else{
params.push(value);
condition = 'value=?';
}
return sqlColumn(store.idProperty) + ' IN (SELECT id FROM ' + repeatingTable + ' WHERE ' + condition + ')';
}).join(' AND '), ')'];
case 'match':
value = value.source;
if (value[0] === '^' && !value.match(/[\{\}\(\)\[\]\.\,\$\*]/)) {
/* jshint quotmark: false */
return [table + '.' + column, ' LIKE \'' + value.slice(1).replace(/'/g,"''") + '%\''];
} else {
throw new Error('The match filter only supports simple prefix matching like /^starts with/');
}
// js hint can't seem to tell that both branches break
/* fall through */
default:
throw new URIError('Invalid query syntax, ' + filter.type + ' not implemented');
}
}
if (condition) {
condition = ' WHERE ' + condition;
}
return {
select: 'SELECT ' + select + ' ',
from: from + condition + order,
params: params,
querier: selectEntry && selectEntry.querier
};
},
fetch: function (options) {
options = options || {};
var query = this.generateSql();
var select = query.select;
var from = query.from;
var params = query.params;
var querier = query.querier;
var limitedCondition = select + ' ' + from;
if (options.end) {
limitedCondition += ' LIMIT ' + (options.end - (options.start || 0));
}
if (options.start) {
limitedCondition += ' OFFSET ' + options.start;
}
var store = this;
var results = lang.delegate(this.executeSql(limitedCondition, params).then(function(sqlResults) {
// get the results back and do any conversions on it
var results = [];
for (var i = 0; i < sqlResults.rows.length; i++) {
results.push(store._restore(convertExtra(sqlResults.rows.item(i))));
}
if (querier) {
results = querier(results);
}
return results;
}));
var store = this;
return new QueryResults(results, {
totalLength: {
then: function (callback,errback) {
// lazily do a total, using the same query except with a COUNT(*) and without the limits
return store.executeSql('SELECT COUNT(*) ' + from, params).then(function(sqlResults) {
return sqlResults.rows.item(0)['COUNT(*)'];
}).then(callback,errback);
}
}
});
},
executeSql: function (sql, parameters) {
// send it off to the DB
var deferred = new Deferred();
var result, error;
this.database.transaction(function(transaction) {
transaction.executeSql(sql, parameters, function (transaction, value) {
deferred.resolve(result = value);
}, function (transaction, e) {
deferred.reject(error = e);
});
});
// return synchronously if the data is already available.
if (result) {
return result;
}
if (error) {
throw error;
}
return deferred.promise;
}
});
});
@@ -0,0 +1,7 @@
define(['dojo/has', 'dojo/sniff'], function (has) {
// summary:
// has() test for indexeddb.
has.add('indexeddb', !!(window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB));
has.add('sql', !!window.openDatabase);
return has;
});
@@ -0,0 +1,60 @@
# Adapters
## StoreAdapter
The `dstore/legacy/StoreAdapter` module allows a Dojo object store to be used as a dstore store. Create an adapted version of an existing `dstore` object store by calling `new StoreAdapter({objectStore: objectStore})`.
```js
require([
'dstore/legacy/StoreAdapter',
'dojo/store/Memory'
], function(StoreAdapter) {
var objectStore = new Memory({...});
var adaptedStore = new StoreAdapter({objectStore: objectStore});
});
```
This provides all the methods and properties inherited from `dstore/api/Store`.
## DstoreAdapter
The `dstore/legacy/DstoreAdapter` module allows a dstore store to be used as a legacy Dojo object store. Create an adapted version of an existing dstore store by creating a `DstoreAdapter` instance with the `store` property referencing the dstore:
```js
require([
'dstore/legacy/DstoreAdapter',
'dstore/Memory'
], function(DstoreAdapter, Memory) {
var store = new Memory({...});
var adaptedStore = new DstoreAdapter(store);
});
```
If the dstore store is trackable (mixed in `dstore/Trackable`), the result sets returned from `query()` calls will be observable (have an `observe()` method).
## StoreSeries
The `dstore/charting/StoreSeries` module allows a dstore object to be used as a `Series` in a Dojox chart.
```js
require([
'dstore/charting/StoreSeries'
], function (StoreSeries) {
//... create a store and a chart ...
// Adds a StoreSeries to the y axis.
chart.addSeries('y', new StoreSeries(store));
});
```
### Constructor
The `StoreSeries` constructor expects 2 parameters.
Property | Description
-------- | -----------
`store` | A dstore object store.
`value` | An optional string, object or function that describes which property or properties to extract from each store item to include in the series. If this parameter is omitted, then "value" is used by default.
### Method Summary
Method | Description
------ | -------------
`setSeriesObject(series)` | Sets the `dojox\charting\Series` object that will render the data.
`fetch()` | Retrieves all of the data from the store. This method is initially called when the adapter is constructed. If the store is observable, the adapter will register an observer to listen for updates from the store.
`destroy()` | Causes the adapter to release all resources.
@@ -0,0 +1,240 @@
_Do you have a contribution? We welcome contributions, but please ensure that you read the following information
before issuing a pull request. Also refer back to this document as a checklist before issuing your pull request.
This will save time for everyone._
# Before You Start
## Understanding the Basics
If you don't understand what a *pull request* is, or how to submit one, please refer to the
[documentation][github-docs] provided by GitHub.
## Search GitHub issues first; create an issue if necessary
Is there already an issue that addresses your concern? Do a bit of searching
in our [issue tracker][] to see if you can find something similar. If not,
please create a new issue before submitting a pull request unless the change is
truly trivial, e.g. typo fixes.
Please note that support questions should not be reported as issues; they should be asked on the
[dojo-interest mailing list][] or #dojo on irc.freenode.net where they may catch more of the community's attention.
Web interfaces are available from the [Dojo Toolkit Community page][].
## Discuss Non-Trivial Contributions with the Committers
If your desired contribution is more than a non-trivial fix, you should discuss it on the mailing list or IRC first
to ensure you are on the right track. In the case of changesets for new features, this will also provide an
opportunity for early feedback to gauge the committers' interest in incorporating your changes.
## Contributor License Agreement
We require all contributions beyond minor typo corrections to be covered under the Dojo Foundation's
[Contributor License Agreement][cla]. This can be done electronically, and essentially ensures that you are
making it clear that your contributions are your contributions, you have the legal right to contribute, and
you are transferring the copyright of your works to the Dojo Foundation.
If the GitHub user ID you are submitting your pull request from differs from the Dojo Community ID or e-mail address
which you have signed your CLA under, you should specifically note what you have your CLA filed under (and for CCLA
that you are listed under your company's authorised contributors).
# Submitting a Pull Request
The following are the general steps you should follow in creating a pull request. Subsequent pull requests only need
to follow step 3 and beyond:
1. Fork the repository on GitHub
2. Clone the forked repository to your machine
3. Create a feature branch in your local repository
4. Make your changes and commit them to your local repository
5. Rebase and push your commits to your GitHub remote fork/repository
6. Issue a Pull Request to the official repository
7. Your Pull Request is reviewed by a committer and merged into the repository
*Note*: While there are other ways to accomplish the steps using other tools, the examples here will assume the most
actions will be performed via the `git` command line.
## 1. Fork the Repository
When logged in to your GitHub account, and you are viewing the dstore repository, you will see the *Fork* button.
Clicking this button will show you which organizations you can fork to. Choose your own account. Once the process
finishes, you will have your own repository that is "forked" from the official one.
Forking is a GitHub term and not a git term. Git is a wholly distributed source control system and simply worries
about local and remote repositories and allows you to manage your code against them. GitHub then adds this additional
layer of structure of how repositories can relate to each other.
## 2. Clone the Forked Repository
Once you have successfully forked your repository, you will need to clone it locally to your machine:
```sh
$ git clone git@github.com:username/dstore.git
```
This will clone your fork to your current path in a directory named `dstore`.
Be sure to read dstore's [README][dstore-readme] and ensure you have the necessary dependencies for development and
testing.
You should also set up an `upstream` remote. This will allow you to take changes from SitePen's repository,
merge them into your local clone, and rebase your branches:
```sh
$ cd dstore
$ git remote add upstream https://github.com/SitePen/dstore.git
$ git fetch upstream
```
Then you can retrieve upstream changes and rebase on them into your code like this:
```sh
$ git pull --rebase upstream master
```
For more information on maintaining a fork, please see the GitHub Help article [Fork a Repo][] and information on
[rebasing][] from git.
## 3. Create a Branch
The easiest workflow is to keep your master branch in sync with the upstream branch and do not push any of your own
commits to that branch (otherwise you may need to hard-reset it later). When you want to work on a new feature,
you then ensure you are on the branch you wish to base your work on (usually `master`) and create a new branch from
there. While the name of the branch can be anything, it can often be easy to use the issue number you might be working
on as a start. For example:
```sh
$ git checkout -b fix-123-short-description master
Switched to a new branch 'fix-123-short-description'
```
You will then be on the feature branch. You can verify what branch you are on like this:
```sh
$ git status
# On branch t12345
nothing to commit, working directory clean
```
## 4. Make Changes and Commit
Now you just need to make your changes. Once you have finished your changes (and tested them) you need to commit them
to your local repository (assuming you have staged your changes for committing via `git add`):
```sh
$ git status
# On branch t12345
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified: somefile.js
#
$ git commit -m 'Corrects some defect, fixes #123'
[t12345 0000000] Corrects some defect, fixes #123
1 file changed, 2 insertions(+), 2 deletions(-)
```
## 5. Rebase and Push Changes
If you have been working on your contribution for a while, the upstream repository may have changed. You may want to
ensure your work is on top of the latest changes so your pull request can be applied cleanly:
```sh
$ git pull --rebase upstream master
```
When you are ready to push your commit to your GitHub repository for the first time on this branch you would do the
following:
```sh
$ git push -u origin t12345
```
This sets up your local branch to track the newly-created branch on your remote. After the first time, you can simply
type:
```sh
$ git push
```
## 6. Issue a Pull Request
In order to have your commits merged into the main repository, you need to create a pull request. The instructions for
this can be found in the GitHub Help article [Creating a Pull Request][]. Essentially you do the following:
1. Go to the GitHub page for your repository (e.g. `https://github.com/username/dstore/`)
2. Select the feature branch from your repository
3. Click the Pull Request button
4. If necessary, select the appropriate branch that your branch should be merged into
4. Enter a title and description of your pull request, mentioning the corresponding [issue tracker][] ticket
5. Review the "Commits" and "Files changed" tabs; if you see more than just your commits here, double-check that you've
selected the correct branch to merge into
6. Click "Send Pull Request"
You will be notified about the status of your pull request based on your GitHub notification settings.
## 7. Request is Reviewed and Merged
Your request will be reviewed. It may be merged directly or [interactively rebased][], or you may receive feedback
or questions on your pull request.
# What Makes a Successful Pull Request?
Having your contribution accepted is more than just the mechanics of getting your contribution into a pull request;
there are several other things that are expected when contributing to dstore which are covered below.
## Version Support
Code added to dstore should work with Dojo 1.8 and later, and should support all modern browsers plus IE 8 and later.
## Coding Style and Linting
dstore follows SitePen's [style guide]. Pull requests should adhere to this.
## Documentation
If the pull request adds or changes features, make sure to update the documentation in the repository's `doc` folder
as needed.
dstore source code follows the Dojo Toolkit's approach to [inline API documentation][]. Any pull request should
ensure it has updated or added the appropriate inline documentation.
## Test Cases
If the pull request adds or changes functional behavior or is fixing a defect, the unit test cases should be modified to
reflect this. The committer reviewing your pull request is likely to request the appropriate changes to the test
cases.
dstore uses [Intern][] for automated testing. It is expected that you will have tested your changes against the
existing test cases and appropriate platforms prior to submitting your pull request.
## Licensing
All of your submissions will be licensed under a dual "New" BSD / AFLv2 license.
## Expect Discussion and Rework
Unless your changes are trivial or you have been working with contributing to dstore for a while,
expect a significant amount of feedback on your pull requests once a committer has the opportunity to review it.
Don't be offended or discouraged by feedback, but keep in mind that the committers have a keener eye to how your
changes may affect existing code, or contain corner cases or incompatibilities.
Also keep in mind that any pull request is essentially asking the dstore committers to continue maintaining the code you
contribute, so features that seem overly specific to a given application or which expose a wide array of edge cases
when used more generically may be deemed inappropriate for incorporation into the dstore repository. In such cases,
you are certainly encouraged to roll your own repository of extensions that you can continue to maintain and
provide to the community.
[github-docs]: http://help.github.com/send-pull-requests
[issue tracker]: https://github.com/SitePen/dstore/issues
[Dojo Toolkit Community page]: http://dojotoolkit.org/community/
[dojo-interest mailing list]: http://mail.dojotoolkit.org/mailman/listinfo/dojo-interest
[dojo-contrib]: http://mail.dojotoolkit.org/mailman/listinfo/dojo-contributors
[cla]: http://dojofoundation.org/about/cla
[Creating a Pull Request]: https://help.github.com/articles/creating-a-pull-request
[Fork a Repo]: https://help.github.com/articles/fork-a-repo
[dstore-readme]: README.md
[style guide]: https://github.com/SitePen/.jshintrc#readme
[inline API documentation]: http://dojotoolkit.org/reference-guide/developer/markup.html
[Intern]: http://theintern.io/
[interactively rebased]: http://git-scm.com/book/en/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages
[rebasing]: http://git-scm.com/book/en/Git-Branching-Rebasing
@@ -0,0 +1,183 @@
# Collection
A Collection is the interface for a collection of items, which can be filtered or sorted to create new collections. When implementing this interface, every method and property is optional, and is only needed if the functionality it provides is required. However, all the included collections implement every method. Note that the objects in the collection might not be immediately retrieved from the underlying data storage until they are actually accessed through `forEach()`, `fetch()`, or `fetchRange()`. These fetch methods return a snapshot of the data, and if the data has changed, these methods can later be used to retrieve the latest data.
## Querying
Several methods are available for querying collections. These methods allow you to define a query through several steps. Normally, stores are queried first by calling `filter()` to specify which objects to be included, if the filtering is needed. Next, if an order needs to be specified, the `sort()` method is called to ensure the results will be sorted. A typical query from a store would look like:
store.filter({priority: 'high'}).sort('dueDate').forEach(function (object) {
// called for each item in the final result set
});
In addition, the `track()` method may be used to track store changes, ensuring notifications include index information about object changes, and keeping result sets up-to-date after a query. The `fetch()` method is an alternate way to retrieve results, providing a promise to an array for accessing query results. The sections below describes each of these methods and how to use them.
## Filtering
Filtering is used to specify a subset of objects to be returned in a filtered collection. The simplest use of the `filter()` method is to call it with a plain object as the argument, that specifies name-value pairs that the returned objects must match. Or a filter builder can be used to construct more sophisticated filter conditions. To use the filter builder, first construct a new filter object from the `Filter` constructor on the collection you would be querying:
var filter = new store.Filter();
We now have a `filter` object, that represents a filter, without any operators applied yet. We can create new filter objects by calling the operator methods on the filter object. The operator methods will return new filter objects that hold the operator condition. For example, to specify that we want to retrieve objects with a `priority` property with a value of `"high"`, and `stars` property with a value greater than `5`, we could write:
var highPriorityFiveStarFilter = filter.eq('priority', 'high').gt('stars', 5);
This filter object can then be passed as the argument to the `filter()` method on a collection/store:
var highPriorityFiveStarCollection = store.filter(highPriorityFiveStarFilter);
The following methods are available on the filter objects. First are the property filtering methods, which each take a property name as the first argument, and a property value to compare for the second argument:
* `eq`: Property values must equal the filter value argument.
* `ne`: Property values must not equal the filter value argument.
* `lt`: Property values must be less than the filter value argument.
* `lte`: Property values must be less than or equal to the filter value argument.
* `gt`: Property values must be greater than the filter value argument.
* `gte`: Property values must be greater than or equal to the filter value argument.
* `in`: An array should be passed in as the second argument, and property values must be equal to one of the values in the array.
* `match`: Property values must match the provided regular expression.
* `contains`: Filters for objects where the specified property's value is an array and the array contains any value that equals the provided value or satisfies the provided expression.
The following are combinatorial methods:
* `and`: This takes two arguments that are other filter objects, that both must be true.
* `or`: This takes two arguments that are other filter objects, where one of the two must be true.
### Nesting
A few of the filters can also be built upon with other collections (potentially from other stores). In particular, you can provide a collection as the argument for the `in` or `contains` filter. This provides functionality similar to nested queries or joins. This generally will need to be combined with a `select` to return the correct values for matching. For example, if we wanted to find all the tasks in high priority projects, where the `task` store has a `projectId` property/column that is a foreign key, referencing objects in a `project` store. We can perform our nested query:
var tasksOfHighPriorityProjects = taskStore.filter(
new Filter().in('projectId',
projectStore.filter({priority: 'high'}).select('id')));
### Implementations
Different stores may implement filtering in different ways. The `dstore/Memory` will perform filtering in memory. The `dstore/Request`/`dstore/Rest` stores will translate the filters into URL query strings to send to the server. Simple queries will be in standard URL-encoded query format and complex queries will conform to [RQL](https://github.com/persvr/rql) syntax (which is a superset of standard query format).
New filter methods can be created by subclassing `dstore/Filter` and adding new methods. New methods can be created by calling `Filter.filterCreator` and by providing the name of the new method. If you will be using new methods with stores that mix in `SimpleQuery` like memory stores, you can also add filter comparators by overriding the `_getFilterComparator` method, returning comparators for the additional types, and delegating to `this.inherited` for the rest.
For the `dstore/Request`/`dstore/Rest` stores, you can define alternate serializations of filters to URL queries for existing or new methods by overriding the `_renderFilterParams`. This method is called with a filter object (and by default is recursively called by combinatorial operators), and should return a string serialization of the filter, that will be inserted into the query string of the URL sent to the server.
The filter objects themselves consist of tree structures. Each filter object has two properties, the operator `type`, which corresponds to whichever operator was used (like `eq` or `and`), and the `args`, which is an array of values provided to the operator. With `and` and `or` operators, the arguments are other filter objects, forming a hierarchy. When filter operators are chained together (through sequential calls), they are combined with the `and` operator (each operator defined in a sub-filter object).
## Collection API
The following property and methods are available on dstore collections:
### Property Summary
Property | Description
-------- | -----------
`Model` | This constructor represents the data model class to use for the objects returned from the store. All objects returned from the store should have their prototype set to the prototype property of the model, such that objects from this store should return true from `object instanceof collection.Model`.
### Method Summary
#### `filter(query)`
This filters the collection, returning a new subset collection. The query can be an object, or a filter object, with the properties defining the constraints on matching objects. Some stores, like server or RQL stores, may accept string-based queries. Stores with in-memory capabilities (like `dstore/Memory`) may accept a function for filtering as well, but using the filter builder will ensure the greatest cross-store compatibility.
#### `sort(property, [descending])`
This sorts the collection, returning a new ordered collection. Note that if sort is called multiple times, previous sort calls may be ignored by the store (it is up to store implementation how to handle that). If a multiple sort order is desired, use the array of sort orders defined by below.
#### `sort([highestSortOrder, nextSortOrder...])`
This also sorts the collection, but can be called to define multiple sort orders by priority. Each argument is an object with a `property` property and an optional `descending` property (defaults to ascending, if not set), to define the order. For example: `collection.sort([{property:'lastName'}, {property: 'firstName'}])` would result in a new collection sorted by lastName, with firstName used to sort identical lastName values.
#### select([property, ...])
This selects specific properties that should be included in the returned objects.
#### select(property)
This will indicate that the return results will consist of the values of the given property of the queried objects. For example, this would return a collection of name values, pulled from the original collection of objects:
collection.select('name');
#### `forEach(callback, thisObject)`
This iterates over the query results. Note that this may be executed asynchronously and the callback may be called after this function returns. This will return a promise to indicate the completion of the iteration. This method forces a fetch of the data.
#### `fetch()`
Normally collections may defer the execution (like making an HTTP request) required to retrieve the results until they are actually accessed. Calling `fetch()` will force the data to be retrieved, returning a promise to an array.
#### `fetchRange({start: start, end: end})`
This fetches a range of objects from the collection, returning a promise to an array. The returned (and resolved) promise should have a `totalLength`
property with a promise that resolves to a number indicating the total number of objects available in the collection.
#### `on(type, listener)`
This allows you to define a listener for events that take place on the collection or parent store. When an event takes place, the listener will be called with an event object as the single argument. The following event types are defined:
Type | Description
-------- | -----------
`add` | This indicates that a new object was added to the store. The new object is available on the `target` property.
`update` | This indicates that an object in the stores was updated. The updated object is available on the `target` property.
`delete` | This indicates that an object in the stores was removed. The id of the object is available on the `id` property.
There is also a corresponding `emit(type, event)` method (from the [Store interface](Store.md#method-summary)) that can be used to emit events when objects have changed.
#### `track()`
This method will create a new collection that will be tracked and updated as the parent collection changes. This will cause the events sent through the resulting collection to include an `index` and `previousIndex` property to indicate the position of the change in the collection. This is an optional method, and is usually provided by `dstore/Trackable`. For example, you can create an observable store class, by using `dstore/Trackable` as a mixin:
var TrackableMemory = declare([Memory, Trackable]);
Trackable requires client side querying functionality. Client side querying functionality is available in `dstore/SimpleQuery` (and inherited by `dstore/Memory`). If you are using a `Request`, `Rest`, or other server side store, you will need to implement client-side query functionality (by implemented querier methods), or mixin `SimpleQuery`:
var TrackableRest = declare([Rest, SimpleQuery, Trackable]);
Once we have created a new instance from this store, we can track a collection, which could be the top level store itself, or a downstream filtered or sorted collection:
var store = new TrackableMemory({data: ...});
var filteredSorted = store.filter({inStock: true}).sort('price');
var tracked = filteredSorted.track();
Once we have a tracked collection, we can listen for notifications:
tracked.on('add, update, delete', function(event){
var newIndex = event.index;
var oldIndex = event.previousIndex;
var object = event.target;
});
Trackable requires fetched data to determine the position of modified objects and can work with either full or partial data. We can do a `fetch()` or `forEach()` to access all the items in the filtered collection:
tracked.fetch();
Or we can do a `fetchRange()` to make individual range requests for items in the collection:
tracked.fetchRange(0, 10);
Trackable will keep track of each page of data, and send out notifications based on the data it has available, along with index information, indicating the new and old position of the object that was modified. Regardless of whether full or partial data is fetched, tracked events and the indices they report are relative to the entire collection, not relative to individual fetched ranges. Tracked events also include a `totalLength` property indicating the total length of the collection.
If an object is added or updated, and falls outside of all of the fetched ranges, the index will be undefined. However, if the object falls between fetched ranges (but within one), there will also be a `beforeIndex` that indicates the index of the first object that the new or update objects comes before.
### Custom Querying
Custom query methods can be created using the `dstore/QueryMethod` module. We can define our own query method, by extending a store, and defining a method with the `QueryMethod`. The QueryMethod constructor should be passed an object with the following possible properties:
* `type` - This is a string, identifying the query method type.
* `normalizeArguments` - This can be a function that takes the arguments passed to the method, and normalizes them for later execution.
* `applyQuery` - This is an optional function that can be called on the resulting collection that is returned from the generated query method.
* `querierFactory` - This is an optional function that can be used to define the computation of the set of objects returned from a query, on client-side or in-memory stores. It is called with the normalized arguments, and then returns a new function that will be called with an array, and is expected to return a new array.
For example, we could create a `getChildren` method that queried for children object, by simply returning the children property array from a parent:
declare([Memory], {
getChildren: new QueryMethod({
type: 'children',
querierFactory: function (parent) {
var parentId = this.getIdentity(parent);
return function (data) {
// note: in this case, the input data is ignored as this querier
// returns an object's array of children instead
// return the children of the parent
// or an empty array if the parent no longer exists
var parent = this.getSync(parentId);
return parent ? parent.children : [];
};
}
})
@@ -0,0 +1,25 @@
This document provides some discussion of design decisions and contains rather informal notes. dstore is distinct from other data modelling frameworks in large part due to its goals. This document is intended to describe those goals and design rationales.
A few of the key goals of dstore:
* The ultimate goal of dstore is facilitate applications with an amazing user experience. One of the most important ingredients to this experience is high performance components that can quickly and smoothly respond to the user. Consequently, dstore places a high priority on ensuring that data can be accessed as quickly as possible with as little overhead as possible.
* dstore aims to provide a clean separation of concerns, helping to define a distinction between the data model and presentation (viewer and controller).
* dstore aims to encapsulate data entities to minimize the interface between data models and presentation, and ensure that the presentation code can fully access and interact with data with this interface.
* dstore aims to provide a consistent, predictable interface for accessing data.
Unlike dojo object stores, dstore handles querying by providing several different query methods that each handle the different parts of a query, including filter and sort. Each of these returns a new collection, which can then be further queried. This purpose of this approach is:
* Better encapsulation of a query result. A resulting collection can be passed to a grid without any extra coordination in regards to what query was used
* This encapsulation also helps provide more legible querying
* query metadata - All queries add query-related metadata to the collection they create. At first, we logged query metadata to separate `filtered`, `sorted`, and `ranged` properties, but doing so lost important information about the order of the queries. We migrated to a unified query log called `queryLog` which has the following benefits:
* Retains the order of query operations
* Allows easy examination and iteration of the query operations together
* Stores querier functions separately rather than composed so a single querier can be invoked without invoking the others
* queriers - Each query may have an associated querier function created based on the query arguments that takes an array of input data and returns an array of result data. For example, based on the filter arguments, a filter query may create a querier that takes an array of input data and returns an array of items that pass the filter. Client-side stores such as `dstore/Memory` completely rely on queriers to perform query operations, and the result-tracking mixin `Trackable` relies on queriers to determine a new or updated item's membership and location in a collection. Server-based collections such as `dstore/Rest` do not require queriers to operate, but queriers are required to properly maintain a server-based collection with `Trackable`.
* querier factory methods - A collection may provide a querier factory method for each query type. Querier factory methods use the naming convention `'_create<Type>Querier'`, take normalized arguments for their query type, and return a querier function based on those arguments. For example, a querier factory method for a sort query is named `'_createSortQuerier'`, takes normalized sort arguments, and returns a function that takes an array and sorts it according to the query's sort arguments. We explored using a single `queryEngine` property that could contain querier factories for supported query types, but when we started researching custom trackers for individual query types, it became clear the `queryEngine` approach was not sufficiently extensible. Instance methods make such functionality easy to extend through inheritance while objects on the prototype such as `queryEngine` are more difficult to extend.
* sort - We had considered making sort modify the order of the current collection, or return a new sorted collection. Doing modification in-place had the advantage of avoiding any cloning of arrays to create a distinct sorted array, and being consistent with Array#sort. Returning a new sorted collection had the advantage of being more functional and consistent with the rest of the dstore collection API. With our initial delegation-based approach to creating new collections, creating new collections for sorts could lead to memory being retained in intermediate steps in querying, but we switched to a copy-based approach which allows intermediate steps in a multi-step query to be garbage collected. Sorting now returns a new sorted collection (not in-place).
* Model constructor usage - One of the goals of the persisted objects is to allow an object to exist across numerous instantiations of an application. Consequently, there is a clear distinction between an existing object that is being restored into memory from storage, versus the creation of the a new object. The restoration of existing objects into memory is designed to be as fast as possible to facilitate optimal access to data (and potentially large sets of objects can be returned from stores), regardless of models used, and avoid any performance regressions from older versions of object stores, consequently this restoration typically uses a __proto__ assignment, rather than a constructor (see http://jsperf.com/setting-the-prototype for comparison of different approaches to recreating an object's prototype). On the otherhand, the model constructor is reserved for the actual creation of new objects. This helps to achieve both the clear distinction in the object lifecycle, as well as maintaining optimum speed for the object stores.
@@ -0,0 +1,5 @@
The purpose of this document is to give interested users a look into what may come in future releases.
* Tracking custom query types - Trackable currently supports tracking filter, sort, and similar query types which operate on an array, but there are other possible query types that are not easily supported by the current Trackable infrastructure (e.g., mapping a collection to another collection). We are researching a solution to these cases and are hoping to develop a solution that works well for both client and server-based stores.
* A `refresh` event - We are considering adding a `refresh` event for a number of reasons. As we investigate adding support for tracking custom query types, it seems clear that some query types cannot be adequately maintained on a per-change basis on the client side. In those cases, emitting a `refresh` event may be the only practical way to respond to a change. In addition, adding a `refresh` event would allow data in a Memory store to be completely replaced via `Memory#setData`.
* Support for caching of fetched ranges (partial) may be added in future versions. Currently only full `fetch()` requests can fulfill a collection cache for future requests.
@@ -0,0 +1,31 @@
# Store
A store is an extension of a [collection](./Collection.md) and is an entity that not only contains a set of objects, but also provides an interface for identifying, adding, modifying, removing, and querying data. Below is the definition of the store interface. Every method and property is optional, and is only needed if the functionality it provides is required (although the provided full stores (`Rest` and `Memory`) implement all the methods except `transaction()` and `getChildren()`). Every method returns a promise for the specified return value, unless otherwise noted.
In addition to the methods and properties inherited from [Collections](./Collection.md), the `Store` API also exposes the following properties and methods.
### Property Summary
Property | Description
-------- | -----------
`idProperty` | If the store has a single primary key, this indicates the property to use as the identity property. The values of this property should be unique. This defaults to "id".
`Model` | This is the model class to use for all the data objects that originate from this store. By default this will be set to null, so that all objects will be plain objects, but this property can be set to the class from `dmodel/Model` or any other model constructor. You can create your own model classes (and schemas), and assign them to a store. All objects that come from the store will have their prototype set such that they will be instances of the model. The default value of `null` will disable any prototype modifications and leave data as plain objects.
`defaultNewToStart` | If a new object is added to a store, this will indicate it if it should go to the start or end. By default, it will be placed at the end.
### Method Summary
Method | Description
------ | -------------
`get(id)` | This retrieves an object by its identity. This returns a promise for the object. If no object was found, the resolved value should be `undefined`.
`getIdentity(object)` | This returns an object's identity (note, this should always execute synchronously).
`put(object, [directives])` | This stores an object. It can be used to update or create an object. This returns a promise that may resolve to the object after it has been saved.
`add(object, [directives])` | This creates an object, and throws an error if the object already exists. This should return a promise for the newly created object.
`remove(id)` | This deletes an object, using the identity to indicate which object to delete. This returns a promise that resolves to a boolean value indicating whether the object was succcessfully removed.
`transaction()` | Starts a transaction and returns a transaction object. The transaction object should include a `commit()` and `abort()` to commit and abort transactions, respectively. Note, that a store user might not call `transaction()` prior to using put, delete, etc. in which case these operations effectively could be thought of as “auto-commit” style actions.
`create(properties)` | Creates and returns a new instance of the data model. The returned object will not be stored in the object store until it its save() method is called, or the store's add() is called with this object. This should always execute synchronously.
`getChildren(parent)` | This retrieves the children of the provided parent object. This should return a new collection representing the children.
`mayHaveChildren(parent)` | This should return true or false indicating whether or not a parent might have children. This should always return synchronously, as a way of checking if children might exist before actually retrieving all the children.
`getRootCollection()` | This should return a collection of the top level objects in a hierarchical store.
`emit(type, event)` | This can be used to dispatch event notifications, indicating changes to the objects in the collection. This should be called by `put`, `add`, and `remove` methods if the `autoEmit` property is `false`. This can also be used to notify stores if objects have changed from other sources (if a change has occurred on the server, from another user). There is a corresponding `on` method on [collections](./Collection.md#ontype-listener) for listening to data change events. Also, the [Trackable](./Stores.md#trackable) can be used to add index/position information to the events.
Stores that can perform synchronous operations may provide analogous methods for `get`, `put`, `add`, and `remove` that end with `Sync` to provide synchronous support. For example `getSync(id)` will directly return an object instead of a promise. The `dstore/Memory` store provides `Sync` methods.
@@ -0,0 +1,190 @@
# Included Stores
The dstore package includes several store implementations that can be used for the needs of different applications. These include:
* `Memory` - This is a simple memory-based store that takes an array and provides access to the objects in the array through the store interface.
* `Request` - This is a simple server-based collection that sends HTTP requests following REST conventions to access and modify data requested through the store interface.
* `Rest` - This is a store built on `Request` that implements add, remove, and update operations using HTTP requests following REST conventions.
* `RequestMemory` - This is a Memory based store that will retrieve its contents from a server/URL.
* `LocalDB` - This a store based on the browser's local database/storage capabilities. Data stored in this store will be persisted in the local browser.
* `Cache` - This is a store mixin that combines a master and caching store to provide caching functionality.
* `Trackable` - This a store mixin that adds index information to `add`, `update`, and `remove` events of tracked store instances. This adds a track() method for tracking stores.
* `Tree` - This is a store mixin that provides hierarchical querying functionality, defining a parent/child relationships for the display of data in a tree.
* `SimpleQuery` - This is a mixin with basic querying functionality, which is extended by the Memory store, and can be used to add client side querying functionality to the Request/Rest store.
* `Store` - This is a base store, with the base methods that are used by all other stores.
## Constructing Stores
All the stores can be instantiated with an options argument to the constructor, to provide properties to be copied to the store. This can include methods to be added to the new store.
Stores can also be constructed by combining a base store with mixins. The various store mixins are designed to be combined through dojo `declare` to create a class to instantiate a store. For example, if you wish to add tracking and tree functionality to a Memory store, we could combine these:
// create the class based on the Memory store with added functionality
var TrackedTreeMemoryStore = declare([Memory, Trackable, Tree]);
// now create an instance
var myStore = new TrackedTreeMemoryStore({data: [...]});
The store mixins can only be used as mixins, but stores can be combined with other stores as well. For example, if we wanted to add the Rest functionality to the RequestMemory store (so the entire store data was retrieved from the server on construction, but data changes are sent to the server), we could write:
var RestMemoryStore = declare([Rest, RequestMemory]);
// now create an instance
var myStore = new RestMemoryStore({target: '/data-source/'});
Another common case is needing to add tracking to the `dstore/Rest` store, which requires client side querying, which be provided by `dstore/SimpleQuery`:
var TrackedRestStore = declare([Rest, SimpleQuery, Trackable]);
## Memory
The Memory store is a basic client-side in-memory store that can be created from a simple JavaScript array. When creating a memory store, the data (which should be an array of objects) can be provided in the `data` property to the constructor. The data should be an array of objects, and all the objects are considered to be existing objects and must have identities (this is not "creating" new objects, no events are fired for the objects that are provided, nor are identities assigned).
For example:
myStore = new Memory({
data: [{
id: 1,
aProperty: ...,
...
}]
});
The `Memory` store provides synchronous equivalents of standard asynchronous store methods, including `getSync(id)`, `addSync(object, options)`, `putSync(object, options)`, and `removeSync(id)`. These methods directly return objects or results, without a promise.
## Request
This is a simple collection for accessing data by retrieval from a server (typically through XHR). The target URL path to use for requests can be defined with the `target` property. A request for data will be sent to the server when a fetch occurs (due a call to `fetch()`, `fetchRange()`, or `forEach()`). Request supports several properties for defining the generation of query strings:
* `sortParam` - This will specify the query parameter to use for specifying the sort order. This will default to `sort(<properties>)` in the query string.
* `selectParam` - This will specify the query parameter to use for specifying the `select` properties. This will default to `select(<properties>)` in the query string.
* `rangeStartParam` and `rangeCountParam` - This will specify the query parameter to use for specifying the range. This will default to `limit(<count>,<start>)` in the query string.
* `useRangeHeaders` - This will specify that range information should be specified in the `Range` header.
## Rest
This store extends the Request store, to add functionality for adding, updating, and removing objects. All modifications trigger HTTP requests to the server using the corresponding RESTful HTTP methods. A `get()` triggers a `GET`, `remove()` triggers a `DELETE`, and `add()` and `put()` will trigger a `PUT` if an id is available or provided, and a `POST` will be used to create new objects with server provided ids.
For example:
myStore = new Rest({
target: '/PathToData/'
});
All modification or retrieval methods (except `getIdentity()`) on `Request` and `Rest` execute asynchronously, returning a promise.
## Store
This is the base class used for all stores, providing basic functionality for tracking collection states and converting objects to be model instances. This (or any of the other classes above) can be extended for creating custom stores.
## RequestMemory
This store provides client-side querying functionality, but will load its data from the server, using the provided URL. This is
an asynchronous store since queries and data retrieval may be made before the data has been retrieved from the server.
## LocalDB
This a store based on the browser's local database/storage capabilities. Data stored in this store will be persisted in the local browser. The LocalDB will automatically load the best storage implementation based on browser's capabilities. These storage implementation follow the same interface. `LocalDB` will attempt to load one of these stores (highest priority first, and these can also be used directly if you do not want automatic selection):
* `dstore/db/IndexedDB` - This uses the IndexedDB API. This is available on the latest version of all major browsers (introduced in IE 10 and Safari 7.1/8, but with some serious bugs).
* `dstore/db/SQL` - This uses the WebSQL API. This is available on Safari and Chrome.
* `dstore/db/LocalStorage` - This uses the localStorage API. This is available on all major browsers, going back to IE8. The localStorage API does not provide any indexed querying, so this loads the entire database in memory. This can be very expensive for large datasets, so this store is generally avoided, except to provide functionality on old versions of IE.
* `dstore/db/has` - This is not a store, but provides feature `has` tests for `indexeddb` and `sql`.
The `LocalDB` stores requires a few extra parameters, not needed by other stores. First, it needs a database configuration object. A database configuration object defines all the stores or tables that are used by the stores, and which properties to index. There should be a single database configuration object for the entire application, and it should be passed to all the store instances. The configuration object should include a version (which should be incremented whenever the configuration is changed), and a set of stores in the `stores` object. Within the stores object, each property that will be used should be defined. Each property value should have a property configuration object with the following optional properties:
* `preference` - This defines the priority of using this property for index-based querying. This should be a larger number for more unique properties. A boolean property would generally have a `preference` of 1, and a completely unique property should be 100.
* `indexed` - This is a boolean indicating if a property should be indexed. This defaults to true.
* `multiEntry` - This indicates the property will have an array of values, and should be indexed correspondingly. Internet Explorer's implementation of IndexedDB does not currently support `multiEntry`.
* `autoIncrement` - This indicates if a property should automatically increment.
Alternately a number can be provided as a property configuration, and will be used as the `preference`.
An example database configuration object is:
var dbConfig = {
version: 5,
stores: {
posts: {
name: 10,
id: {
autoIncrement: true,
preference: 100
},
tags: {
multiEntry: true,
preference: 5
},
content: {
indexed: false
}
},
commments: {
author: {},
content: {
indexed: false
}
}
}
};
In addition, each store should define a `storeName` property to identify which database store corresponds to the store instance. For example:
var postsStore = new LocalDB({dbConfig: dbConfig, storeName: 'posts'});
var commentsStore = new LocalDB({dbConfig: dbConfig, storeName: 'comments'});
Once created, these stores can be used like any other store.
## Cache
This is a mixin that can be used to add caching functionality to a store. This can also be used to wrap an existing store, by using the static `create` function:
var cachedStore = Cache.create(existingStore, {
cachingStore: new Memory()
});
This store has the following properties and methods:
Name | Description
---- | -----------
`cachingStore` | This can be used to define the store to be used for caching the data. By default a Memory store will be used.
`isValidFetchCache` | This is a flag that indicates if the data fetched for a collection/store can be cached to fulfill subsequent fetches. This is false by default, and the value will be inherited by downstream collections. It is important to note that only full `fetch()` requests will fill the cache for subsequent `fetch()` requests. `fetchRange()` requests will not fulfill a collection, and subsequent `fetchRange()` requests will not go to the cache unless the collection has been fully loaded through a `fetch()` request.
`allLoaded` | This is a flag indicating that the given collection/store has its data loaded. This can be useful if you want to provide a caching store prepopulated with data for a given collection. If you are setting this to true, make sure you set `isValidFetchCache` to true as well to indicate that the data is available for fetching.
`canCacheQuery(method, args)' | This can be a boolean or a method that will indicate if a collection can be cached (if it should have `isValidFetchCache` set to true), based on the query method and arguments used to derive the collection.
`isLoaded(object)` | This can be defined to indicate if a given object in a query can be cached (by default, objects are cached).
## Tree
This is a mixin that provides basic support for hierarchical data. This implements several methods that can then be used by hierarchical UI components (like [dgrid](https://github.com/SitePen/dgrid) with a tree column). This mixin uses a parent-based approach to finding children, retrieving the children of an object by querying for objects that have `parent` property with the id of the parent object. In addition, objects may have a `hasChildren` property to indicate if they have children (if the property is absent, it is assumed that they may have children). This mixin implements the following methods:
* `getChildren(parent)` - This returns a collection representing the children of the provided parent object. This is produced by filtering for objects that have a `parent` property with the id of the parent object.
* `mayHaveChildren(parent)` - This synchronously returns a boolean indicating whether or not the parent object might have children (the actual children may need to be retrieved asynchronously).
* `getRootCollection()` - This returns the root collection, the collection of objects with `parent` property that is `null`.
The Tree mixin may serve as an example for alternate hierarchical implementations. By implementing these methods as they are in `dstore/Tree`, one could change the property names for data that uses different parent references or indications of children. Another option would be define the children of an object as direct references from the parent object. In this case, you would define `getChildren` to associate the `parent` object with the returned collection and override `fetch` and `fetchRange` to return a promise to the array of the children of the parent.
## Trackable
The Trackable mixin adds functionality for tracking the index positions of objects as they are added, updated, or deleted. The `Trackable` mixin adds a `track()` method to create a new tracked collection. When events are fired (from modification operations, or other sources), the tracked can match the changes from the events to any cached data in the collection (which may be ordered by sorting, or filtered), and decorates the events with index positions. More information about tracked collections and events can be found in the collections [documentation](Collection.md#track).
## Resource Query Language
[Resource Query Language (RQL)](https://github.com/persvr/rql) is a query language specifically designed to be easily embedded in URLs (it is a compatible superset of standard encoded query parameters), as well as easily interpreted within JavaScript for client-side querying. Therefore RQL is a query language suitable for consistent client and server-delegated queries. The dstore packages serializes complex filter/queries into RQL (RQL supersets standard query parameters, and so simple queries are simply serialized as standard query parameters).
dstore also includes support for using RQL as the query language for filtering. This can be enabled by mixin `dstore/extensions/RqlQuery` into your collection type:
require([
'dojo/_base/declare',
'dstore/Memory',
'dstore/extensions/RqlQuery'
], function (declare, Memory, RqlQuery) {
var RqlStore = declare([ Memory, RqlQuery ]);
var rqlStore = new RqlStore({
...
});
rqlStore.filter('price<10|rating>3').forEach(function (product) {
// return each product that has a price less than 10 or a rating greater than 3
});
}};
Make sure you have installed/included the [rql](https://github.com/persvr/rql) package if you are using the RQL query engine.
@@ -0,0 +1,45 @@
# Testing
dstore uses [Intern](http://theintern.io/) as its test runner. Tests can
either be run using the browser, or using [Sauce Labs](https://saucelabs.com/).
More information on writing your own tests with Intern can be found in the
[Intern wiki](https://github.com/theintern/intern/wiki).
## Setting up
**Note:** Commands listed in this section are all written assuming they are
run in the `dstore` directory.
Install dependencies for testing.
```
npm install
```
## Running via the browser
1. Open a browser to http://hostname/path_to_dstore/tests/runTests.html
2. View the console
## Running in the console
Run the tests with the following command:
```
node node_modules/intern-geezer/client config=tests/intern
```
## Running via Sauce Labs
Make sure the proper Sauce Labs credentials are set in the environment:
```
export SAUCE_USERNAME=<your_sauce_username>
export SAUCE_ACCESS_KEY=<your_sauce_access_key>
```
Then kick off the runner with the following command:
```
node node_modules/intern-geezer/runner config=tests/intern
```
@@ -0,0 +1,13 @@
define([
'dojo/_base/declare',
'rql/js-array',
'../SimpleQuery'
], function (declare, arrayEngine, SimpleQuery) {
return declare(SimpleQuery, {
_createFilterQuerier: function (filter) {
return filter.type === 'string'
? arrayEngine.query(filter.args[0])
: this.inherited(arguments);
}
});
});
@@ -0,0 +1,178 @@
define([
'dojo/_base/declare',
'dojo/_base/array',
'dojo/store/util/QueryResults'
/*=====, "dstore/api/Store" =====*/
], function (declare, arrayUtil, QueryResults /*=====, Store =====*/) {
// module:
// An adapter mixin that makes a dstore store object look like a legacy Dojo object store.
function passthrough(data) {
return data;
}
// No base class, but for purposes of documentation, the base class is dstore/api/Store
var base = null;
/*===== base = Store; =====*/
var adapterPrototype = {
// store:
// The dstore store that is wrapped as a Dojo object store
store: null,
constructor: function (store) {
this.store = store;
if (store._getQuerierFactory('filter') || store._getQuerierFactory('sort')) {
this.queryEngine = function (query, options) {
options = options || {};
var filterQuerierFactory = store._getQuerierFactory('filter');
var filter = filterQuerierFactory ? filterQuerierFactory(query) : passthrough;
var sortQuerierFactory = store._getQuerierFactory('sort');
var sort = passthrough;
if (sortQuerierFactory) {
sort = sortQuerierFactory(arrayUtil.map(options.sort, function (criteria) {
return {
property: criteria.attribute,
descending: criteria.descending
};
}));
}
var range = passthrough;
if (!isNaN(options.start) || !isNaN(options.count)) {
range = function (data) {
var start = options.start || 0,
count = options.count || Infinity;
var results = data.slice(start, start + count);
results.total = data.length;
return results;
};
}
return function (data) {
return range(sort(filter(data)));
};
};
}
var objectStore = this;
// we call notify on events to mimic the old dojo/store/Trackable
store.on('add,update,delete', function (event) {
var type = event.type;
var target = event.target;
objectStore.notify(
(type === 'add' || type === 'update') ? target : undefined,
(type === 'delete' || type === 'update') ?
('id' in event ? event.id : store.getIdentity(target)) : undefined);
});
},
query: function (query, options) {
// summary:
// Queries the store for objects. This does not alter the store, but returns a
// set of data from the store.
// query: String|Object|Function
// The query to use for retrieving objects from the store.
// options: dstore/api/Store.QueryOptions
// The optional arguments to apply to the resultset.
// returns: dstore/api/Store.QueryResults
// The results of the query, extended with iterative methods.
//
// example:
// Given the following store:
//
// ...find all items where "prime" is true:
//
// | store.query({ prime: true }).forEach(function(object){
// | // handle each object
// | });
options = options || {};
var results = this.store.filter(query);
var queryResults;
var tracked;
var total;
// Apply sorting
var sort = options.sort;
if (sort) {
if (Object.prototype.toString.call(sort) === '[object Array]') {
var sortOptions;
while ((sortOptions = sort.pop())) {
results = results.sort(sortOptions.attribute, sortOptions.descending);
}
} else {
results = results.sort(sort);
}
}
if (results.track && !results.tracking) {
// if it is trackable, always track, so that observe can
// work properly.
results = results.track();
tracked = true;
}
if ('start' in options) {
// Apply a range
var start = options.start || 0;
// object stores support sync results, so try that if available
queryResults = results[results.fetchRangeSync ? 'fetchRangeSync' : 'fetchRange']({
start: start,
end: options.count ? (start + options.count) : Infinity
});
}
queryResults = queryResults || results[results.fetchSync ? 'fetchSync' : 'fetch']();
total = queryResults.totalLength;
queryResults = new QueryResults(queryResults);
queryResults.total = total;
queryResults.observe = function (callback, includeObjectUpdates) {
// translate observe to event listeners
function convertUndefined(value) {
if (value === undefined && tracked) {
return -1;
}
return value;
}
var addHandle = results.on('add', function (event) {
callback(event.target, -1, convertUndefined(event.index));
});
var updateHandle = results.on('update', function (event) {
if (includeObjectUpdates || event.previousIndex !== event.index || !isFinite(event.index)) {
callback(event.target, convertUndefined(event.previousIndex), convertUndefined(event.index));
}
});
var removeHandle = results.on('delete', function (event) {
callback(event.target, convertUndefined(event.previousIndex), -1);
});
var handle = {
remove: function () {
addHandle.remove();
updateHandle.remove();
removeHandle.remove();
}
};
handle.cancel = handle.remove;
return handle;
};
return queryResults;
},
notify: function () {
}
};
var delegatedMethods = [ 'get', 'put', 'add', 'remove', 'getIdentity' ];
arrayUtil.forEach(delegatedMethods, function (methodName) {
adapterPrototype[methodName] = function () {
var store = this.store;
// try sync first, since dojo object stores support that directly
return (store[methodName + 'Sync'] || store[methodName]).apply(store, arguments);
};
});
return declare(base, adapterPrototype);
});
@@ -0,0 +1,128 @@
define([
'dojo/_base/declare',
'dojo/_base/lang',
'dojo/_base/array',
'dojo/when',
'../Store',
'../QueryResults'
], function (declare, lang, arrayUtil, when, Store, QueryResults) {
// module:
// An adapter mixin that makes a legacy Dojo object store look like a dstore object.
var modifyDelegate = function (name) {
return function () {
var objectStore = this.objectStore;
return objectStore[name].apply(objectStore, arguments);
};
};
return declare(Store, {
// objectStore:
// The object store wrapped by this adapter
objectStore: null,
get: function () {
// summary:
// Retrieves an object by its identity
// id: Number
// The identity to use to lookup the object
// returns: Object
// The object in the store that matches the given id.
var self = this,
objectStore = this.objectStore;
return when(objectStore.get.apply(objectStore, arguments), function (object) {
return self._restore(object);
});
},
put: modifyDelegate('put'),
add: modifyDelegate('add'),
remove: modifyDelegate('remove'),
getIdentity: modifyDelegate('getIdentity'),
_setIdentity: function (object, identityArg) {
return (object[this.objectStore.idProperty] = identityArg);
},
fetch: function () {
// summary:
// Fetches the query results. Note that the fetch may occur asynchronously
// returns: Array|Promise
// The results or a promise for the results
// create an object store query and query options based on current collection
// information
return this.fetchRange();
},
fetchRange: function (rangeArgs) {
// summary:
// Fetches the query results with a range. Note that the fetch may occur asynchronously
// returns: Array|Promise
// The results or a promise for the results
// create an object store query and query options based on current collection
// information
var queryOptions = {},
queryLog = this.queryLog,
getQueryArguments = function (type) {
return arrayUtil.map(
arrayUtil.filter(queryLog, function (entry) { return entry.type === type; }),
function (entry) {
return entry.normalizedArguments[0];
}
);
};
// take the last sort since multiple sorts are not supported by dojo/store
var sorted = getQueryArguments('sort').pop();
if (sorted) {
queryOptions.sort = sorted;
if (sorted instanceof Array) {
// object stores expect an attribute property
for (var i = 0; i < sorted.length; i++) {
var sortSegment = sorted[i];
sortSegment.attribute = sortSegment.property;
}
}
}
if (rangeArgs) {
// set the range
queryOptions.count = rangeArgs.end - ((queryOptions.start = rangeArgs.start) || 0);
}
var queryObject = {};
applyFilter(getQueryArguments('filter'));
function applyFilter(filtered) {
for (var i = 0; i < filtered.length; i++) {
var filter = filtered[i];
var type = filter.type;
var args = filter.args;
if (type === 'and') {
applyFilter(args);
} else if (type === 'eq' || type === 'match') {
queryObject[args[0]] = args[1];
} else if (type === 'string') {
queryObject = args[0];
} else if (type) {
throw new Error('"' + type + ' operator can not be converted to a legacy store query');
}
// else if (!type) { no-op }
}
}
var results = this.objectStore.query(queryObject, queryOptions);
if (results) {
// apply the object restoration
return new QueryResults(results.map(this._restore, this), {
totalLength: when(results.total)
});
}
return results;
}
});
});
@@ -0,0 +1,35 @@
var miniExcludes = {
'dstore/README.md': 1,
'dstore/package': 1
},
isTestRe = /\/test\//;
var packages = {};
try {
// retrieve the set of packages for determining which modules to include
require(['util/build/buildControl'], function (buildControl) {
packages = buildControl.packages;
});
} catch (error) {
console.error('Unable to retrieve packages for determining optional package support in dstore');
}
var profile = {
resourceTags: {
test: function (filename, mid) {
return isTestRe.test(filename);
},
miniExclude: function (filename, mid) {
return /\/(?:tests|demos|docs)\//.test(filename) || mid in miniExcludes;
},
amd: function (filename, mid) {
return /\.js$/.test(filename);
},
copyOnly: function (filename, mid) {
// conditionally omit modules dependent on rql packages
return (!packages['rql'] && /RqlQuery\.js/.test(filename));
}
}
};
@@ -0,0 +1,32 @@
{
"name": "dojo-dstore",
"author": "Kris Zyp",
"version": "1.1.0",
"description": "A data collection infrastructure",
"licenses": [
{
"type": "AFLv2.1",
"url": "http://trac.dojotoolkit.org/browser/dojo/trunk/LICENSE#L43"
},
{
"type": "BSD",
"url": "http://trac.dojotoolkit.org/browser/dojo/trunk/LICENSE#L13"
}
],
"repository": {
"type": "git",
"url": "http://github.com/SitePen/dstore"
},
"dependencies": {
"dojo": "^1.9.0"
},
"devDependencies": {
"intern-geezer": "^2",
"rql": "~0.3.3"
},
"directories": {
"lib": "."
},
"main": "./Store",
"dojoBuild": "package.js"
}
@@ -0,0 +1,232 @@
define([
'intern!object',
'intern/chai!assert',
'dojo/Deferred',
'dojo/when',
'dojo/json',
'dojo/aspect',
'dojo/_base/declare',
'dstore/Store',
'dstore/Memory',
'dstore/Request',
'./mockRequest',
'dstore/Cache'
], function (registerSuite, assert, Deferred, when, JSON, aspect, declare, Store, Memory, Request, mockRequest, Cache) {
/* jshint newcap: false */
var masterFetchCalled;
var Master = declare(Memory, {
fetch: function () {
masterFetchCalled = true;
return this.inherited(arguments);
}
});
function makeDeferred(){
return function () {
var result = this.inherited(arguments);
var deferred = new Deferred();
setTimeout(function () {
// Dojo's deferred resolve doesn't properly defer promises
when(result, function (result) {
deferred.resolve(result);
});
});
return deferred.promise;
};
}
var AsyncMemory = declare(Memory, {
fetch: makeDeferred(),
get: makeDeferred(),
put: makeDeferred(),
remove: makeDeferred()
});
var AsyncMaster = declare([AsyncMemory, Master], {});
function createTests(name, Master, cachingStore, createAfter){
var store;
return {
setup: function () {
var mixins = [Master];
if (!createAfter) {
mixins.push(Cache);
}
store = new declare(mixins)({
cachingStore: cachingStore,
data: [
{id: 1, name: 'one', prime: false},
{id: 2, name: 'two', even: true, prime: true},
{id: 3, name: 'three', prime: true},
{id: 4, name: 'four', even: true, prime: false},
{id: 5, name: 'five', prime: true}
]
});
if (createAfter) {
store = Cache.create(store, {
cachingStore: cachingStore
});
}
},
name: name,
'get': function () {
return when(store.get(1)).then(function (object) {
assert.strictEqual(object.name, 'one');
return cachingStore.get(1);
}).then(function (object) {
assert.strictEqual(object.name, 'one'); // second one should be cached
return store.get(1);
}).then(function (object) {
assert.strictEqual(object.name, 'one');
return store.get(4);
}).then(function (object) {
assert.strictEqual(object.name, 'four');
return cachingStore.get(4);
}).then(function (object) {
assert.strictEqual(object.name, 'four');
return store.get(4);
}).then(function (object) {
assert.strictEqual(object.name, 'four');
});
},
'filter': function () {
store.isLoaded = store.canCacheQuery = function () {
return false;
};
masterFetchCalled = false;
var collection = store.filter({prime: true});
return when(collection.fetch()).then(function (results) {
assert.isTrue(masterFetchCalled);
assert.strictEqual(results.length, 3);
return collection.cachingStore.get(3);
}).then(function (result) {
assert.strictEqual(result, undefined);
collection = store.filter({even: true});
masterFetchCalled = false;
collection.isLoaded = store.canCacheQuery = function () {
return true;
};
return collection.fetch();
}).then(function (results) {
assert.isTrue(masterFetchCalled);
assert.strictEqual(results[1].name, 'four');
collection.isValidFetchCache = true;
masterFetchCalled = false;
return collection.fetch();
}).then(function (results) {
assert.strictEqual(results.length, 2);
assert.isFalse(masterFetchCalled);
return store.put({id: 6, name: 'six', even: true});
}).then(function () {
masterFetchCalled = false;
return collection.fetch();
}).then(function (results) {
assert.isFalse(masterFetchCalled);
assert.strictEqual(results.length, 3);
store.remove(6);
store.isValidFetchCache = false;
return collection.cachingStore.get(3);
});
},
'filter with sort': function () {
return when(store.filter({prime: true}).sort('name').fetch()).then(function (results) {
assert.strictEqual(results.length, 3);
return store.filter({even: true}).sort('name').fetch();
}).then(function (results) {
assert.strictEqual(results[1].name, 'two');
});
},
'put update': function () {
return when(store.get(4)).then(function (four) {
four.square = true;
return store.put(four);
}).then(function () {
return store.get(4);
}).then(function (four) {
assert.isTrue(four.square);
return cachingStore.get(4);
}).then(function (four) {
assert.isTrue(four.square);
return store.get(4);
}).then(function (four) {
assert.isTrue(four.square);
});
},
'put new': function () {
return when(store.put({
id: 6,
perfect: true
})).then(function () {
return store.get(6);
}).then(function (six) {
assert.isTrue(six.perfect);
return cachingStore.get(6);
}).then(function (six) {
assert.isTrue(six.perfect);
});
},
'add duplicate': function () {
return when(store.add({
id: 6,
perfect: true
})).then(function () {
assert.fail('Should not succeed');
}, function () {
// successfully rejected
});
},
'add new': function () {
return when(store.add({
id: 7,
prime: true
})).then(function () {
return store.get(7);
}).then(function (seven) {
assert.isTrue(seven.prime);
return cachingStore.get(7);
}).then(function (seven) {
assert.isTrue(seven.prime);
});
},
'cached filtered data from all': function () {
store.isValidFetchCache = true;
delete store.isLoaded;
return when(store.fetch()).then(function () { // should result in everything being cached
masterFetchCalled = false;
return store.filter({prime: true}).fetch();
}).then(function (results) {
assert.strictEqual(results.length, 4);
assert.isFalse(masterFetchCalled);
});
},
'defaults to querier factories of the cachingStore': function () {
var store = new Store();
assert.isUndefined(store._getQuerierFactory('filter'));
var expectedQuerier = function () {};
var cachedStore = Cache.create(store, {
cachingStore: new Memory({
_createFilterQuerier: function () {
return expectedQuerier;
}
})
});
var cachedStoreQuerierFactory = cachedStore._getQuerierFactory('filter');
var cachingStoreQuerierFactory = cachedStore.cachingStore._getQuerierFactory('filter');
assert.strictEqual(cachedStoreQuerierFactory(), expectedQuerier);
assert.strictEqual(cachedStoreQuerierFactory(), cachingStoreQuerierFactory());
}
};
}
registerSuite(createTests('dstore Cache', Master, new Memory()));
registerSuite(createTests('dstore create Cache from instance', Master, new Memory(), true));
registerSuite(createTests('dstore Cache with async Master', AsyncMaster, new Memory()));
registerSuite(createTests('dstore Cache with both async', AsyncMaster, new AsyncMemory()));
});
@@ -0,0 +1,121 @@
define([
'intern!object',
'intern/chai!assert',
'dojo/_base/declare',
'dstore/Csv',
'dstore/Memory',
'dojo/text!./data/noquote.csv',
'dojo/text!./data/quote.csv',
'dojo/text!./data/contributors.csv'
], function (registerSuite, assert, declare, Csv, Memory, noquote, quote, contributors) {
var CsvMemory = declare([Memory, Csv]);
var xhrBase = require.toUrl('dstore/tests/data');
var csvs = {}; // holds retrieved raw CSV data
var stores = {}; // holds stores created after CSV data is retrieved
csvs.noQuote = noquote;
stores.noQuoteNoHeader = new CsvMemory({
data: noquote,
newline: '\n',
fieldNames: ['id', 'last', 'first', 'born', 'died']
});
stores.noQuoteWithHeader = new CsvMemory({
data: noquote,
newline: '\n'
// No fieldNames; first row will be treated as header row.
});
stores.noQuoteTrim = new CsvMemory({
data: noquote,
newline: '\n',
trim: true
});
csvs.quote = quote;
stores.quoteNoHeader = new CsvMemory({
data: quote,
newline: '\n',
fieldNames: ['id', 'name', 'quote']
});
stores.quoteWithHeader = new CsvMemory({
data: quote,
newline: '\n'
// No fieldNames; first row will be treated as header row.
});
csvs.contributors = contributors;
stores.contributors = new CsvMemory({
data: contributors,
newline: '\n'
});
registerSuite({
name: 'dstore CSV',
'no quote': function () {
var noHeader = stores.noQuoteNoHeader,
withHeader = stores.noQuoteWithHeader,
trim = stores.noQuoteTrim,
item, trimmedItem;
// Test header vs. no header...
assert.strictEqual(4, noHeader.data.length,
'Store with fieldNames should have 4 items.');
assert.strictEqual(3, withHeader.data.length,
'Store using header row should have 3 items.');
assert.strictEqual(5, noHeader.fieldNames.length,
'Store with fieldNames should have 5 fields.');
assert.strictEqual(6, withHeader.fieldNames.length,
'Store using header row should have 6 fields.');
assert.strictEqual('id', noHeader.getSync('id').id,
'First line should be considered an item when fieldNames are set');
assert.strictEqual('Albert', withHeader.getSync('1').first,
'Field names picked up from header row should be trimmed.');
assert.strictEqual(noHeader.getSync('1').last, withHeader.getSync('1').last,
'Item with id of 1 should have the same data in both stores.');
// Test trim vs. no trim...
item = withHeader.getSync('2');
trimmedItem = trim.getSync('2');
assert.strictEqual(' Nikola ', item.first,
'Leading/trailing spaces should be preserved if trim is false.');
assert.strictEqual('Nikola', trimmedItem.first,
'Leading/trailing spaces should be trimmed if trim is true.');
assert.strictEqual(' ', item.middle,
'Strings containing only whitespace should remain intact if trim is false.');
assert.strictEqual('', trimmedItem.middle,
'Strings containing only whitespace should be empty if trim is true.');
// Test data integrity...
item = withHeader.getSync('1');
assert.isTrue(item.middle === '', 'Test blank value.');
assert.strictEqual('1879-03-14', item.born, 'Test value after blank value.');
assert.isTrue(withHeader.getSync('3').died === '', 'Test blank value at end of line.');
},
'quote': function () {
var noHeader = stores.quoteNoHeader,
withHeader = stores.quoteWithHeader;
// Test header vs. no header...
assert.strictEqual(5, noHeader.data.length,
'Store with fieldNames should have 5 items.');
assert.strictEqual(4, withHeader.data.length,
'Store using header row should have 4 items.');
assert.strictEqual('id', noHeader.getSync('id').id,
'First line should be considered an item when fieldNames are set');
assert.strictEqual(noHeader.getSync('1').name, withHeader.getSync('1').name,
'Item with id of 1 should have the same data in both stores.');
// Test data integrity...
assert.strictEqual('""', withHeader.getSync('3').quote,
'Value consisting of two double-quotes should pick up properly.');
assert.strictEqual(' S, P, ...ace! ', withHeader.getSync('4').quote,
'Leading/trailing spaces within quotes should be preserved.');
assert.isTrue(/^Then[\s\S]*"Nevermore\."$/.test(withHeader.getSync('2').quote),
'Multiline value should remain intact.');
assert.isTrue(/smiling,\n/.test(withHeader.getSync('2').quote),
'Multiline value should use same newline format as input.');
},
'import export': function () {
assert.strictEqual(csvs.contributors, stores.contributors.toCsv(),
'toCsv() should generate data matching original if it is well-formed');
}
});
});
@@ -0,0 +1,307 @@
define([
'intern!object',
'intern/chai!assert',
'../db/has!indexeddb?../db/IndexedDB',
'../db/has!sql?../db/SQL',
'../db/LocalStorage',
'../LocalDB',
'dojo/promise/all',
'dojo/_base/array',
'dojo/sniff'
], function (registerSuite, assert, IndexedDB, SQL, LocalStorage, LocalDB, all, arrayUtil, has) {
var numbers = [
{id: 1, name: 'one', prime: false, mappedTo: 'E', words: ['banana']},
{id: 2, name: 'two', even: true, prime: true, mappedTo: 'D', words: ['banana', 'orange']},
{id: 3, name: 'three', prime: true, mappedTo: 'C', words: ['apple', 'orange']},
{id: 4, name: 'four', even: true, prime: false, mappedTo: null, nested: {a: 1}},
{id: 5, name: 'five', prime: true, mappedTo: 'A', nested: {a: 2}}
];
var letters = [
{id: 'A', lower: 'a', vowel: true},
{id: 'B', lower: 'b', vowel: false},
{id: 'C', lower: 'c', vowel: false},
{id: 'D', lower: 'd', vowel: false},
{id: 'E', lower: 'e', vowel: true},
{id: 'F', lower: 'f', vowel: false},
{id: 'G', lower: 'g', vowel: false},
{id: 'H', lower: 'h', vowel: false},
{id: 'I', lower: 'i', vowel: true},
{id: 'J', lower: 'j', vowel: false},
{id: 'K', lower: 'k', vowel: false},
{id: 'L', lower: 'l', vowel: false},
{id: 'M', lower: 'm', vowel: false},
{id: 'N', lower: 'n', vowel: false},
{id: 'O', lower: 'o', vowel: true},
{id: 'P', lower: 'p', vowel: false},
{id: 'Q', lower: 'q', vowel: false}
];
var dbConfig = {
version: 8,
stores: {
test: {
name: 10,
even: {},
id: {
autoIncrement: true,
preference: 100
},
words: {
multiEntry: true,
preference: 5
},
mappedTo: {
indexed: false
},
'nested.a': {
preference: 3
}
},
test2: {
id: {
preference: 100
},
lower: {
preference: 50
},
vowel: {}
}
}
};
if (IndexedDB) {
registerSuite(testsForDB('dstore/db/IndexedDB', IndexedDB));
}
if (window.openDatabase) {
registerSuite(testsForDB('dstore/db/SQL', SQL));
}
registerSuite(testsForDB('dstore/db/LocalStorage', LocalStorage));
function testsForDB(name, DB) {
// need to reset availability
dbConfig.available = null;
var numberStore = new DB({dbConfig: dbConfig, storeName: 'test'});
var letterStore = new DB({dbConfig: dbConfig, storeName: 'test2'});
var Filter = numberStore.Filter;
function testQuery(filter, options, expectedResults) {
if (!expectedResults) {
expectedResults = options;
options = undefined;
}
return function () {
if (options && options.multi && has('trident')) {
// sadly, IE doesn't support multiEntry yet
return;
}
var i = 0;
var collection = numberStore.filter(filter, options);
return collection.fetch().then(function (fetched) {
// check to make sure each one is in there
arrayUtil.forEach(fetched, function (object) {
assert.isTrue(arrayUtil.indexOf(expectedResults, object.id) > -1);
});
assert.strictEqual(expectedResults.length, fetched.length);
}).then(function () {
// now apply the sort
if (options) {
if (options.sort) {
collection = collection.sort(options.sort);
}
}
return collection.forEach(function (object) {
assert.strictEqual(expectedResults[i++], object.id);
});
}).then(function () {
return collection.select('id').fetch();
}).then(function (fetched) {
// the selected values
assert.deepEqual(expectedResults, fetched.slice(0));
}).then(function () {
assert.strictEqual(expectedResults.length, i);
var range = options && options.range || {start: 1, end: 3};
var expectedCount = expectedResults.length;
if (range) {
expectedResults = expectedResults.slice(range.start, range.end);
var fetchedRange = collection.fetchRange(range);
return fetchedRange.then(function (fetched) {
fetched.forEach(function (object, i) {
assert.strictEqual(expectedResults[i], object.id);
});
assert.strictEqual(expectedResults.length, fetched.length);
return fetchedRange.totalLength.then(function (totalLength) {
if (expectedCount > expectedResults.length) {
// IndexedDB will just estimate the count in this case
assert.isTrue(totalLength > expectedResults.length);
} else {
assert.strictEqual(totalLength, expectedCount);
}
});
});
}
});
};
}
return {
name: name,
setup: function () {
var results = [];
return numberStore.fetch().then(function (data) {
// make a copy
data = data.slice(0);
for (var i = 0, l = data.length; i < l; i++) {
results.push(numberStore.remove(data[i].id));
}
return all(results);
}).then(function () {
results = [];
// load new data
for (var i = 0; i < numbers.length; i++) {
results.push(numberStore.put(numbers[i]));
}
return all(results);
}).then(function () {
return letterStore.fetch();
}).then(function (data) {
results = [];
// make a copy
data = data.slice(0);
for (var i = 0, l = data.length; i < l; i++) {
results.push(letterStore.remove(data[i].id));
}
return all(results);
}).then(function() {
results = [];
// load new data
for (var i = 0; i < letters.length; i++) {
results.push(letterStore.put(letters[i]));
}
return all(results);
});
},
'{id: 2}': testQuery({id: 2}, [2]),
'{name: "four"}': testQuery({name: 'four'}, {range: {start: 0, end: 1}}, [4]),
'{name: "two"}': testQuery({name: 'two'}, [2]),
'{even: true}': testQuery({even: true}, {range: {start: 0, end: 1}}, [2, 4]),
'{even: true, name: "two"}': testQuery({even: true, name: 'two'}, [2]),
// test non-indexed values
'{mappedTo: "C"}': testQuery({mappedTo: 'C'}, [3]),
// union
'[{name: "two"}, {mappedTo: "C"}, {mappedTo: "D"}]':
testQuery(
new Filter().or(
new Filter({name: 'two'}),
new Filter({mappedTo: 'C'}),
new Filter({mappedTo: 'D'})), [2, 3]),
// jshint quotmark: false
"gte('id', 1).lte('id', 3)": testQuery(new Filter().gte('id', 1).lte('id', 3), {range: {start: 0, end: 1}}, [1, 2, 3]),
"gte('name', 'm').lte('name', 'three')": testQuery(new Filter().gte('name', 'm').lte('name', 'three'),
{sort:[{property: 'id'}]}, [1, 3]),
"gte('name', 'one').lte('name', 'three')": testQuery(new Filter().gte('name', 'one').lte('name', 'three'),
{sort:[{property: 'id'}]}, [1, 3]),
"gt('name', 'one').lte('name', 'three')":
testQuery(new Filter().gt('name', 'one').lte('name', 'three'),
{range: {start: 0, end: 2}, sort:[{property: 'mappedTo'}]}, [3]),
"gte('name', 'one').lt('name', 'three')":
testQuery(new Filter().gte('name', 'one').lt('name', 'three'),
{sort:[{property: 'name', descending: true}]}, [1]),
"gt('name', 'one').lt('name', 'three')":
testQuery(new Filter().gt('name', 'one').lt('name', 'three'), {sort:[{property: 'id'}]}, []),
"match('name', /^t/)": testQuery(new Filter().match('name', /^t/), {sort:[{property: 'name'}]}, [3, 2]),
'{name: "not a number"}': testQuery({name: 'not a number'}, {range: {start: 0, end: 1}}, []),
"contains('words', ['orange'])": testQuery(new Filter().contains('words', ['orange']), {multi: true}, [2, 3]),
"contains('words', [new Filter().match('words', /^or/)])": testQuery(new Filter().contains('words', [
new Filter().match('words', /^or/)]), {multi: true, sort:[{property: 'id'}], range: {start: 0, end: 1}}, [2, 3]),
"contains('words', ['apple', 'banana'])": testQuery(new Filter().contains('words', ['apple', 'banana']),
{multi: true, range: {start: 0, end: 2}}, []),
"contains('words', ['orange', 'banana']":
testQuery(new Filter().contains('words', ['orange', 'banana']), {multi: true, sort:[{property: 'mappedTo'}]}, [2]),
"contains('words', ['orange', 'banana'])":
testQuery(new Filter().gte('id', 0).lte('id', 4).contains('words', ['orange', 'banana']), {multi: true}, [2]),
// '{name: '*e'}': testQuery({name: '*e'}, [5, 1, 3]), don't know if we even support this yet
"gte('id', 1).lte('id', 3), sort by name +": testQuery(
new Filter().gte('id', 1).lte('id', 3), {sort:[{property: 'name'}]}, [1, 3, 2]),
"gte('nested.a', 1).lte('nested.a', 2), sort by name +": testQuery(
new Filter().gte('nested.a', 1).lte('nested.a', 2), {sort:[{property: 'nested.a'}]}, [4, 5]),
"eq('nested.a', 1)": testQuery(
new Filter().eq('nested.a', 1), {}, [4]),
"gte('id', 1).lte('id', 3), sort by name -":
testQuery(new Filter().gte('id', 1).lte('id', 3), {
sort:[{property: 'name', descending: true}],
range: {start: 0, end: 1}
}, [2, 3, 1]),
"gte('id', 0).lte('id', 4)": testQuery(new Filter().gte('id', 0).lte('id', 4),
{sort:[{property: 'id'}], range: {start: 1, end: 3}}, [1, 2, 3, 4]),
"in('id', [2, 4, 5])": testQuery(new Filter().in('id', [2, 4, 5]),
{sort:[{property: 'id'}], range: {start: 1, end: 3}}, [2, 4, 5]),
"in('id', [2, 4, 5]).select(['id', 'name'])": function () {
var selected = numberStore.filter(new Filter().in('id', [2, 4, 5])).select(['id', 'name']);
return selected.fetch().then(function (data) {
assert.deepEqual(data.slice(0), [
{id: 2, name: 'two'},
{id: 4, name: 'four'},
{id: 5, name: 'five'}
]);
});
},
"in('id', [2, 4, 5]).select('name')": function () {
var selected = numberStore.filter(new Filter().in('id', [2, 4, 5])).select('name');
return selected.fetch().then(function (data) {
assert.deepEqual(data.slice(0), [
'two',
'four',
'five'
]);
});
},
"in('mappedTo', letterStore.filter({vowel: true}).select('id'))":
testQuery(new Filter().in('mappedTo', letterStore.filter({vowel: true}).select('id')),
{sort:[{property: 'name'}], range: {start: 1, end: 3}}, [5, 1]),
'db interaction': function () {
return numberStore.get(1).then(function(one) {
assert.strictEqual(one.id, 1);
assert.strictEqual(one.name, 'one');
assert.strictEqual(one.prime, false);
assert.strictEqual(one.mappedTo, 'E');
return all([
numberStore.remove(2),
numberStore.remove(4),
numberStore.add({id: 6, name: 'six', prime: false, words: ['pineapple', 'orange juice']})
]).then(function() {
return all([
testQuery(new Filter().gte('name', 's').lte('name', 'u'), {sort:[{property: 'id'}]}, [3, 6])(),
testQuery(new Filter().contains('words', [new Filter().match('words', /^orange/)]), {multi: true}, [3, 6])()
]);
});
});
},
'reload db': function () {
// reload the DB store and make sure the data is still there
dbConfig.openRequest = null;
function Model() {}
Model.prototype.method = function () {
return true;
};
numberStore = new DB({
dbConfig: dbConfig,
storeName: 'test',
Model: Model
});
return numberStore.get(1).then(function(one) {
assert.strictEqual(one.id, 1);
assert.strictEqual(one.name, 'one');
assert.strictEqual(one.prime, false);
assert.strictEqual(one.mappedTo, 'E');
assert.isTrue(one.method());
return all([
testQuery(new Filter().gte('name', 's').lte('name', 'u'), {sort:[{property: 'id'}]}, [3, 6])(),
testQuery(new Filter().contains('words', [new Filter().match('words', /^orange/)]), {multi: true}, [3, 6])()
]);
});
},
'find a LocalDB': function () {
// make sure we resolved to at least one of them
assert.isTrue(LocalDB === IndexedDB || LocalDB === LocalStorage || LocalDB === SQL, 'resolved a store');
}
};
}
});
@@ -0,0 +1,189 @@
define([
'intern!object',
'intern/chai!assert',
'../db/has!indexeddb?../db/IndexedDB',
'../db/has!sql?../db/SQL',
'../db/LocalStorage',
'dojo/promise/all',
'dojo/sniff'
], function (registerSuite, assert, IndexedDB, SQL, LocalStorage, all, has) {
var data = [
{id: 1, name: 'one', prime: false, mappedTo: 'E', words: ['banana']},
{id: 2, name: 'two', even: true, prime: true, mappedTo: 'D', words: ['banana', 'orange']},
{id: 3, name: 'three', prime: true, mappedTo: 'C', words: ['apple', 'orange']},
{id: 4, name: 'four', even: true, prime: false, mappedTo: null},
{id: 5, name: 'five', prime: true, mappedTo: 'A'}
];
var dbConfig = {
version: 5,
stores: {
test: {
name: 10,
even: {},
id: {
autoIncrement: true,
preference: 20
},
words: {
multiEntry: true,
preference: 5
},
mappedTo: {
indexed: false
}
}
}
};
if (IndexedDB) {
registerSuite(testsForDB('dstore/db/IndexedDB', IndexedDB));
}
if (window.openDatabase) {
registerSuite(testsForDB('dstore/db/SQL', SQL));
}
registerSuite(testsForDB('dstore/db/LocalStorage', LocalStorage));
function testsForDB(name, DB) {
// need to reset availability
dbConfig.available = null;
var db = new DB({dbConfig: dbConfig, storeName: 'test'});
var Filter = db.Filter;
function testQuery(filter, options, results) {
if (!results) {
results = options;
options = undefined;
}
return function () {
if (options && options.multi && has('trident')) {
// sadly, IE doesn't support multiEntry yet
return;
}
var i = 0;
var collection = db.filter(filter, options);
if (options) {
if (options.sort) {
collection = collection.sort(options.sort);
}
if (options.range) {
var fetchedRange = collection.fetchRange(options.range);
return fetchedRange.then(function (fetched) {
fetched.forEach(function (object, i) {
assert.strictEqual(results[i], object.id);
});
assert.strictEqual(results.length, fetched.length);
return fetchedRange.totalLength.then(function (totalLength) {
assert.strictEqual(totalLength, options.range.count);
});
});
}
}
var forEachResults = collection.forEach(function (object) {
console.log("id check", i, results[i], object.id);
assert.strictEqual(results[i++], object.id);
});
return forEachResults.then(function () {
console.log("results", results);
assert.strictEqual(results.length, i);
if (options && options.range) {
return forEachResults.totalLength.then(function (total) {
assert.strictEqual(results.length, total);
});
}
});
};
}
return {
name: name,
setup: function () {
var results = [];
return db.fetch().then(function (data) {
// make a copy
data = data.slice(0);
for (var i = 0, l = data.length; i < l; i++) {
results.push(db.remove(data[i].id));
}
return all(results);
}).then(function() {
results = [];
// load new data
for (var i = 0; i < data.length; i++) {
results.push(db.put(data[i]));
}
return all(results);
});
},
'{id: 2}': testQuery({id: 2}, [2]),
'{name: "four"}': testQuery({name: 'four'}, [4]),
'{name: "two"}': testQuery({name: 'two'}, [2]),
'{even: true}': testQuery({even: true}, [2, 4]),
'{even: true, name: "two"}': testQuery({even: true, name: 'two'}, [2]),
// test non-indexed values
'{mappedTo: "C"}': testQuery({mappedTo: 'C'}, [3]),
// union
'[{name: "two"}, {mappedTo: "C"}, {mappedTo: "D"}]':
testQuery(
new Filter().or(
new Filter({name: 'two'}),
new Filter({mappedTo: 'C'}),
new Filter({mappedTo: 'D'})), [2, 3]),
'{id: {from: 1, to: 3}}': testQuery(new Filter().gte('id', 1).lte('id', 3), [1, 2, 3]),
'{name: {from: "m", to: "three"}}': testQuery(new Filter().gte('name', 'm').lte('name', 'three'), [1, 3]),
'{name: {from: "one", to: "three"}}': testQuery(new Filter().gte('name', 'one').lte('name', 'three'), [1, 3]),
'{name: {from: "one", excludeFrom: true, to: "three"}}':
testQuery(new Filter().gt('name', 'one').lte('name', 'three'), [3]),
'{name: {from: "one", to: "three", excludeTo: true}}':
testQuery(new Filter().gte('name', 'one').lt('name', 'three'), [1]),
'{name: {from: "one", excludeFrom: true, to: "three", excludeTo: true}}':
testQuery(new Filter().gt('name', 'one').lt('name', 'three'), []),
'{name: "t*"}': testQuery(new Filter().match('name', /^t/), {sort:[{property: 'name'}]}, [3, 2]),
'{name: "not a number"}': testQuery({name: 'not a number'}, []),
'{words: {contains: ["orange"]}}': testQuery(new Filter().contains('words', ['orange']), {multi: true}, [2, 3]),
'{words: {contains: ["or*"]}}': testQuery(new Filter().contains('words', [
new Filter().match('words', /^or/)]), {multi: true}, [2, 3]),
'{words: {contains: ["apple", "banana"]}}': testQuery(new Filter().contains('words', ['apple', 'banana']), {multi: true}, []),
'{words: {contains: ["orange", "banana"]}}':
testQuery(new Filter().contains('words', ['orange', 'banana']), {multi: true}, [2]),
'{id: {from: 0, to: 4}, words: {contains: ["orange", "banana"]}}':
testQuery(new Filter().gte('id', 0).lte('id', 4).contains('words', ['orange', 'banana']), {multi: true}, [2]),
// '{name: '*e'}': testQuery({name: '*e'}, [5, 1, 3]), don't know if we even support this yet
'{id: {from: 1, to: 3}}, sort by name +': testQuery(
new Filter().gte('id', 1).lte('id', 3), {sort:[{property: 'name'}]}, [1, 3, 2]),
'{id: {from: 1, to: 3}}, sort by name -':
testQuery(new Filter().gte('id', 1).lte('id', 3), {sort:[{property: 'name', descending: true}]}, [2, 3, 1]),
'{id: {from: 0, to: 4}}, paged': testQuery(new Filter().gte('id', 0).lte('id', 4),
{range: {start: 1, end: 3, count: 4}}, [2, 3]),
'db interaction': function () {
return db.get(1).then(function(one) {
assert.strictEqual(one.id, 1);
assert.strictEqual(one.name, 'one');
assert.strictEqual(one.prime, false);
assert.strictEqual(one.mappedTo, 'E');
return all([
db.remove(2),
db.remove(4),
db.add({id: 6, name: 'six', prime: false, words: ['pineapple', 'orange juice']})
]).then(function() {
return all([
testQuery(new Filter().gte('name', 's').lte('name', 'u'), {sort:[{property: 'id'}]}, [3, 6])(),
testQuery(new Filter().contains('words', [new Filter().match('words', /^orange/)]), {multi: true}, [3, 6])()
]);
});
});
},
'reload db': function () {
// reload the DB store and make sure the data is still there
dbConfig.openRequest = null;
db = new DB({dbConfig: dbConfig, storeName: 'test'});
return db.get(1).then(function(one) {
assert.strictEqual(one.id, 1);
assert.strictEqual(one.name, 'one');
assert.strictEqual(one.prime, false);
assert.strictEqual(one.mappedTo, 'E');
return all([
testQuery(new Filter().gte('name', 's').lte('name', 'u'), {sort:[{property: 'id'}]}, [3, 6])(),
testQuery(new Filter().contains('words', [new Filter().match('words', /^orange/)]), {multi: true}, [3, 6])()
]);
});
}
};
}
});
@@ -0,0 +1,507 @@
define([
'intern!object',
'intern/chai!assert',
'dojo/_base/declare',
'./sorting',
'dstore/Memory',
'dstore/QueryMethod'
], function (registerSuite, assert, declare, sorting, Memory, QueryMethod) {
var store;
var Model = declare(null, {
constructor: function (args) {
declare.safeMixin(this, args);
}
});
registerSuite({
name: 'dstore Memory',
beforeEach: function () {
store = new Memory({
data: [
{ id: 1, name: 'one', prime: false, mappedTo: 'E', nested: {a: 5} },
{ id: 2, name: 'two', even: true, prime: true, mappedTo: 'D', nested: {a: 4} },
{ id: 3, name: 'three', prime: true, mappedTo: 'C', nested: {a: 3} },
{ id: 4, name: 'four', even: true, prime: false, mappedTo: null , nested: {a: 2}},
{ id: 5, name: 'five', prime: true, mappedTo: 'A', nested: {a: 1} }
],
Model: Model,
filterFunction: function (object) {
return object.name === 'two';
}
});
// add a method to the model prototype
store.Model.prototype.describe = function () {
return this.name + ' is ' + (this.prime ? '' : 'not ') + 'a prime';
};
},
'getSync': function () {
assert.strictEqual(store.getSync(1).name, 'one');
assert.strictEqual(store.getSync(4).name, 'four');
assert.isTrue(store.getSync(5).prime);
},
'get': function () {
return store.get(1).then(function (object) {
assert.strictEqual(object.name, 'one');
});
},
'fetchSync and fetchRangeSync results.totalLength': function () {
var results = store.fetchSync(),
rangeResults = store.fetchRangeSync({ start: 0, end: 1 });
assert.isNumber(results.totalLength);
assert.isNumber(rangeResults.totalLength);
assert(results.totalLength, results.length);
assert.strictEqual(rangeResults.totalLength, results.totalLength);
},
'Model': function () {
assert.strictEqual(store.getSync(1).describe(), 'one is not a prime');
assert.strictEqual(store.getSync(3).describe(), 'three is a prime');
assert.strictEqual(store.filter({even: true}).fetchSync()[1].describe(), 'four is not a prime');
},
'no Model': function() {
var noModelStore = new Memory({
data: [
{id: 1, name: 'one', prime: false, mappedTo: 'E'}
],
Model: null
});
assert.strictEqual(noModelStore.getSync(1).get, undefined);
assert.strictEqual(noModelStore.getSync(1).save, undefined);
},
'filter': function () {
var filter = new store.Filter();
assert.strictEqual(store.filter(filter.eq('prime', true)).fetchSync().length, 3);
var count = 0;
store.filter(filter.eq('prime', true)).fetch().forEach(function (object) {
count++;
assert.equal(object.prime, true);
});
assert.equal(count, 3);
assert.strictEqual(store.filter({even: true}).fetchSync()[1].name, 'four');
},
'async filter': function () {
var filter = new store.Filter();
return store.filter(filter.eq('even', true)).fetch().then(function (results) {
assert.strictEqual(results.length, 2);
});
},
'filter with string': function () {
assert.strictEqual(store.filter({name: 'two'}).fetchSync().length, 1);
assert.strictEqual(store.filter({name: 'two'}).fetchSync()[0].name, 'two');
},
'filter with regexp': function () {
assert.strictEqual(store.filter({name: /^t/}).fetchSync().length, 2);
assert.strictEqual(store.filter({name: /^t/}).fetchSync()[1].name, 'three');
assert.strictEqual(store.filter({name: /^o/}).fetchSync().length, 1);
assert.strictEqual(store.filter({name: /o/}).fetchSync().length, 3);
},
'filter with or': function () {
var filter = new store.Filter();
var primeOrEven = filter.or(filter.eq('prime', true), filter.eq('even', true));
assert.strictEqual(store.filter(primeOrEven).fetchSync().length, 4);
},
'filter with gt and lt': function () {
var filter = new store.Filter();
var betweenTwoAndFour = filter.gt('id', 2).lt('id', 5);
assert.strictEqual(store.filter(betweenTwoAndFour).fetchSync().length, 2);
var overTwo = {
id: filter.gt(2)
};
assert.strictEqual(store.filter(overTwo).fetchSync().length, 3);
var TwoToFour = filter.gte('id', 2).lte('id', 5);
assert.strictEqual(store.filter(TwoToFour).fetchSync().length, 4);
},
'filter with nested gt and lt': function () {
var filter = new store.Filter();
var betweenTwoAndFour = filter.gt('nested.a', 2).lt('nested.a', 5);
assert.strictEqual(store.filter(betweenTwoAndFour).fetchSync().length, 2);
var overTwo = {
'nested.a': filter.gt(2)
};
assert.strictEqual(store.filter(overTwo).fetchSync().length, 3);
var TwoToFour = filter.gte('nested.a', 2).lte('nested.a', 5);
assert.strictEqual(store.filter(TwoToFour).fetchSync().length, 4);
},
'filter with test function': function () {
assert.strictEqual(store.filter({id: {test: function (id) {
return id < 4;
}}}).fetchSync().length, 3);
assert.strictEqual(store.filter({even: {test: function (even, object) {
return even && object.id > 2;
}}}).fetchSync().length, 1);
},
'filter with sort': function () {
assert.strictEqual(store.filter({prime: true}).sort('name').fetchSync().length, 3);
assert.strictEqual(store.filter({even: true}).sort('name').fetchSync()[1].name, 'two');
assert.strictEqual(store.filter({even: true}).sort(function (a, b) {
return a.name < b.name ? -1 : 1;
}).fetchSync()[1].name, 'two');
assert.strictEqual(store.filter(null).sort('mappedTo').fetchSync()[4].name, 'four');
},
'filter with nested sort': function () {
assert.strictEqual(store.filter({prime: true}).sort('nested.a').fetchSync().length, 3);
assert.strictEqual(store.filter({even: true}).sort('nested.a').fetchSync()[1].name, 'two');
assert.strictEqual(store.filter({'nested.a': 2}).sort('mappedTo').fetchSync()[0].name, 'four');
},
'filter with paging': function () {
assert.strictEqual(store.filter({prime: true}).fetchRangeSync({start: 1, end: 2}).length, 1);
var count = 0;
store.filter({prime: true}).fetchRange({start: 1, end: 2}).forEach(function (object) {
count++;
assert.equal(object.prime, true);
});
assert.equal(count, 1);
assert.strictEqual(store.filter({prime: true}).fetchRangeSync({start: 1, end: 2}).totalLength, 3);
assert.strictEqual(store.filter({even: true}).fetchRangeSync({start: 1, end: 2})[0].name, 'four');
},
'filter with string-named function': function () {
assert.strictEqual(store.filter('filterFunction').fetchSync().length, 1);
},
'filter with inheritance': function () {
var store = new Memory({
data: [
{id: 1, name: 'one', prime: false},
{id: 2, name: 'two', even: true, prime: true}
],
getIdentity: function () {
return 'id-' + this.inherited(arguments);
},
newMethod: function () {
return 'hello';
}
});
var filtered = store.filter({even: true}).sort('name');
var one = filtered.getSync('id-1');
one.changed = true;
filtered.put(one);
assert.strictEqual(filtered.getIdentity(one), 'id-1');
assert.strictEqual(filtered.newMethod(), 'hello');
store.remove('id-1');
assert.strictEqual(filtered.getSync('id-1'), undefined);
},
'alternate query method': function () {
var store = new Memory({
data: [
{id: 1, name: 'one', prime: false},
{id: 2, name: 'two', even: true, prime: true, children: [
{id: 2.1, name: 'two point one', whole: false, even: false},
{id: 2.2, name: 'two point two', whole: false, even: true},
{id: 2.3, name: 'two point three', whole: false, even: false}
]}
],
getChildren: new QueryMethod({
type: 'children',
querierFactory: function (parent) {
return function () {
return parent.children;
};
},
applyQuery: function(newCollection) {
newCollection.isAChildCollection = true;
return newCollection;
}
})
});
// make the children
var two = store.getSync(2);
// and test the new query method
var ids = [];
var filteredChildren = store.getChildren(two).filter({even: false});
filteredChildren.forEach(function (child) {
ids.push(child.id);
});
assert.equal(filteredChildren.queryLog.length, 2);
assert.equal(filteredChildren.queryLog[0].type, 'children');
assert.equal(filteredChildren.queryLog[1].type, 'filter');
assert.isTrue(filteredChildren.isAChildCollection);
assert.deepEqual(ids, [2.1, 2.3]);
},
'put update': function () {
var four = store.getSync(4);
four.square = true;
store.put(four);
four = store.getSync(4);
assert.isTrue(four.square);
},
'put new': function () {
store.put({
id: 6,
perfect: true
});
assert.isTrue(store.getSync(6).perfect);
},
'put with options.beforeId': function () {
// Make default put index 0 so it is clear beforeId:null is working
store.defaultNewToStart = true;
store.put({ id: 2 }, { beforeId: 4 });
store.put({ id: 0 }, { beforeId: null });
var results = store.fetchSync();
// Move from a lower source index to a higher destination index because Memory previously had an
// off-by-one bug where it removing an updated item from a lower index and inserted it one past
// the correct destination index
assert.strictEqual(results[2].id, 2);
assert.strictEqual(results[3].id, 4);
assert.strictEqual(results[results.length - 1].id, 0);
},
'add with options.beforeId': function () {
// Make default put index 0 so it is clear beforeId:null is working
store.defaultNewToStart = true;
store.add({ id: 42 }, { beforeId: 3 });
store.add({ id: 24 }, { beforeId: null });
var results = store.fetchSync();
assert.strictEqual(results[2].id, 42);
assert.strictEqual(results[3].id, 3);
assert.strictEqual(results[results.length - 1].id, 24);
},
'create and remove': function () {
var newObject = store.create({
id: 10,
name: 'ten'
});
assert.strictEqual(store.getSync(10), undefined);
store.put(newObject);
assert.isObject(store.getSync(10));
store.remove(10);
assert.strictEqual(store.getSync(10), undefined);
},
'add duplicate': function () {
store.put({
id: 6,
perfect: true
});
var succeeded = false;
store.add({
id: 6,
perfect: true
}).then(function() {
succeeded = true;
}, function() {
// should be rejected as a duplicate
});
assert.isFalse(succeeded);
},
'add new': function () {
store.add({
id: 7,
prime: true
});
assert.isTrue(store.getSync(7).prime);
},
'remove': function () {
store.add({
id: 7,
prime: true
});
assert.isTrue(store.removeSync(7));
assert.strictEqual(store.getSync(7), undefined);
},
'remove from object': function () {
var newObject = store.addSync({
id: 7,
prime: true
});
return store.remove(newObject.id).then(function (result) {
assert.isTrue(result);
assert.strictEqual(store.getSync(7), undefined);
});
},
'remove missing': function () {
var expectedLength = store.fetchSync().length;
assert(!store.removeSync(77));
// make sure nothing changed
assert.strictEqual(store.fetchSync().length, expectedLength);
},
'put typed object': function () {
function MyClass() {}
var myObject = new MyClass();
myObject.id = 10;
store.put(myObject);
// make sure we don't mess up the class of the input
assert.isTrue(myObject instanceof MyClass);
// make sure the the object in the store is the right type
assert.isTrue(store.getSync(10) instanceof store.Model);
},
'filter after changes': function () {
store.remove(2);
store.add({ id: 6, perfect: true });
assert.strictEqual(store.filter({prime: true}).fetchSync().length, 2);
assert.strictEqual(store.filter({perfect: true}).fetchSync().length, 1);
},
'ItemFileReadStore style data': function () {
var anotherStore = new Memory({
data: {
items: [
{name: 'one', prime: false},
{name: 'two', even: true, prime: true},
{name: 'three', prime: true}
],
identifier: 'name'
}
});
assert.strictEqual(anotherStore.getSync('one').name, 'one');
assert.strictEqual(anotherStore.filter({name: 'one'}).fetchSync()[0].name, 'one');
},
'add new id assignment': function () {
var object = {
random: true
};
object = store.addSync(object);
assert.isTrue(!!object.id);
},
'query results length properties': function () {
var filteredCollection = store.filter(function (o) {
return o.id <= 3;
});
var sortedCollection = store.sort('id');
var results = store.fetchRangeSync({start: 0, end: 3});
assert.strictEqual(results.totalLength, 5);
assert.strictEqual(results.length, 3);
},
'composite key': function () {
var store = new Memory({
data: [
{ x: 1, y: 1, name: '1,1' },
{ x: 2, y: 1, name: '2,1' },
{ x: 1, y: 2, name: '1,2' },
{ x: 2, y: 2, name: '2,2' }
],
getIdentity: function (object) {
return object.x + ',' + object.y;
}
});
assert.equal(store.getSync('1,1').name, '1,1');
assert.equal(store.getIdentity(store.getSync('1,2')), '1,2');
store.add({x: 3, y: 2, name: '3,2'});
assert.equal(store.getSync('3,2').name, '3,2');
store.put({x: 1, y: 1, name: 'changed'});
assert.equal(store.getSync('1,1').name, 'changed');
},
'source collection.data does not become subcollection.data': function () {
// Note: This is not a great test because it tests an implementation detail rather than public interface.
// However, it is a detail that is unlikely to change and one we've experienced regression with,
// so we believe it is a value to test it directly here.
var sourceCollection = store.filter({ prime: true });
sourceCollection.fetchSync();
assert.isDefined(sourceCollection.data);
var subCollection = sourceCollection.filter({ id: 1 });
subCollection.fetchSync();
assert.notDeepEqual(sourceCollection.data, subCollection.data);
},
'subclasses can provide initial `data` at runtime': function () {
var expectedData = [ 1, 2, 3 ];
var TestStore = declare(Memory, {
constructor: function () {
this.data = expectedData;
},
setData: function (data) {
this.actualData = data;
this.inherited(arguments);
}
});
var testStore = new TestStore();
assert.deepEqual(testStore.actualData, expectedData);
},
'sorting with null and numbers': function () {
var store = new Memory({
data: [
{id: 1, value: 1},
{id: 2, value: 2},
{id: 3, value: null},
{id: 4, value: -1}
]
});
var results = store.sort('value').fetchSync();
assert.equal(results[0].id, 4);
assert.equal(results[1].id, 1);
assert.equal(results[2].id, 2);
assert.equal(results[3].id, 3);
var results = store.sort('value', true).fetchSync();
assert.equal(results[0].id, 3);
assert.equal(results[1].id, 2);
assert.equal(results[2].id, 1);
assert.equal(results[3].id, 4);
},
'sorting with null and strings': function () {
var store = new Memory({
data: [
{id: 1, value: 'a'},
{id: 2, value: 'b'},
{id: 3, value: null},
{id: 4, value: ''}
]
});
var results = store.sort('value').fetchSync();
assert.equal(results[0].id, 4);
assert.equal(results[1].id, 1);
assert.equal(results[2].id, 2);
assert.equal(results[3].id, 3);
var results = store.sort('value', true).fetchSync();
assert.equal(results[0].id, 3);
assert.equal(results[1].id, 2);
assert.equal(results[2].id, 1);
assert.equal(results[3].id, 4);
},
nestedSuite: sorting('dstore Memory sorting', function before(data) {
return function before() {
store = new Memory({data: data});
};
}, function sort() {
return store.sort.apply(store, arguments).fetchSync();
})
});
});
@@ -0,0 +1,369 @@
define([
'intern!object',
'intern/chai!assert',
'dojo/_base/declare',
'dojo/_base/lang',
'dojo/json',
'dojo/request/registry',
'dojo/when',
'dojo/promise/all',
'dstore/Request',
'dstore/SimpleQuery',
'./mockRequest',
'dojo/text!./data/treeTestRoot'
], function (
registerSuite,
assert,
declare,
lang,
JSON,
request,
when,
whenAll,
Request,
SimpleQuery,
mockRequest,
treeTestRootData
) {
var Model = declare(null, {});
function runHeaderTest(method, args) {
return store[method].apply(store, args).then(function () {
mockRequest.assertRequestHeaders(requestHeaders);
mockRequest.assertRequestHeaders(globalHeaders);
});
}
function runCollectionTest(collection, rangeArgs, expected) {
var expectedResults = [
{ id: 1, name: 'one' },
{ id: 2, name: 'two' }
];
mockRequest.setResponseText(JSON.stringify(expectedResults));
var results;
if (!expected) {
expected = rangeArgs;
results = collection.fetch();
} else {
mockRequest.setResponseHeaders({
'Content-Range': rangeArgs.start + '-' + rangeArgs.end + '/' + expectedResults.length
});
results = collection.fetchRange(rangeArgs);
results = results.totalLength.then(function (totalLength) {
assert.strictEqual(totalLength, expectedResults.length);
return results;
});
}
return results.then(function (results) {
expected.headers && mockRequest.assertRequestHeaders(expected.headers);
expected.queryParams && mockRequest.assertQueryParams(expected.queryParams);
// We cannot just assert deepEqual with results and expectedResults
// because the store converts results into model instances with additional members.
assert.strictEqual(results.length, expectedResults.length);
for(var i = 0; i < results.length; ++i) {
var result = results[i],
expectedResult = expectedResults[i];
for(var key in expectedResult) {
assert.strictEqual(result[key], expectedResult[key]);
}
}
});
}
var globalHeaders = {
'test-global-header-a': 'true',
'test-global-header-b': 'yes'
};
var requestHeaders = {
'test-local-header-a': 'true',
'test-local-header-b': 'yes',
'test-override': 'overridden'
};
var store;
var registryHandle;
function createRequestTests (Store) {
return {
name: 'dstore Request',
before: function () {
registryHandle = request.register(/.*mockRequest.*/, mockRequest);
},
after: function () {
registryHandle.remove();
},
beforeEach: function () {
mockRequest.setResponseText('{}');
mockRequest.setResponseHeaders({});
store = new Store({
target: '/mockRequest/',
headers: globalHeaders,
Model: Model
});
store.Model.prototype.describe = function () {
return 'name is ' + this.name;
};
createRequestTests.store = store;
},
'filter': function () {
mockRequest.setResponseText(treeTestRootData);
return when(store.filter('data/treeTestRoot').fetch()).then(function (results) {
var object = results[0];
assert.strictEqual(object.name, 'node1');
assert.strictEqual(object.describe(), 'name is node1');
assert.strictEqual(object.someProperty, 'somePropertyA');
});
},
'filter iterative': function () {
mockRequest.setResponseText(treeTestRootData);
var i = 0;
return store.filter('data/treeTestRoot').forEach(function (object) {
i++;
assert.strictEqual(object.name, 'node' + i);
// the method we added
assert.equal(typeof object.describe, 'function');
});
},
'filter object': function () {
var filter = { prop1: 'Prop1Value', prop2: 'Prop2Value' };
return runCollectionTest(store.filter(filter), { queryParams: filter });
},
'filter builder': function () {
var filter = new store.Filter();
var betweenTwoAndFour = filter.gt('id', 2).lt('price', 5);
return runCollectionTest(store.filter(betweenTwoAndFour), { queryParams: {
id: 'gt=2',
price: 'lt=5'
}});
},
'filter builder ne or': function () {
var filter = new store.Filter();
var betweenTwoAndFour = filter.ne('id', 2).or(filter.eq('foo', true), filter.eq('foo'));
return runCollectionTest(store.filter(betweenTwoAndFour), { queryParams: {
id: 'ne=2|foo=true|foo=undefined'
}});
},
'filter relational': function () {
var filter = new store.Filter();
var innerFilter = new store.Filter().eq('foo', true);
var nestedFilter = filter['in']('id', store.filter(innerFilter).select('id'));
return runCollectionTest(store.filter(nestedFilter), { queryParams: {
id: 'in=(/mockRequest/?foo=true&select(id))'
}});
},
'sort': function () {
var sortedCollection = store.sort({
property: 'prop1',
descending: true
}, {
property: 'prop2'
}, {
property: 'prop3',
descending: true
});
return runCollectionTest(sortedCollection, {
queryParams: {
'sort(-prop1,+prop2,-prop3)': ''
}
});
},
'sort with this.sortParam': function () {
store.sortParam = 'sort-param';
var sortedCollection = store.sort({
property: 'prop1',
descending: true
}, {
property: 'prop2'
}, {
property: 'prop3',
descending: true
});
return runCollectionTest(sortedCollection, {
queryParams: {
'sort-param': '-prop1,+prop2,-prop3'
}
});
},
'sort with different prefixes': function () {
store.descendingPrefix = '--';
store.ascendingPrefix = '++';
var sortedCollection = store.sort({
property: 'prop1',
descending: true
}, {
property: 'prop2'
}, {
property: 'prop3',
descending: true
});
return runCollectionTest(sortedCollection, {
queryParams: {
'sort(--prop1,++prop2,--prop3)': ''
}
});
},
'select': function () {
var selectCollection = store.select(['prop1', 'prop2']);
return runCollectionTest(selectCollection, {
queryParams: {
'select(prop1,prop2)': ''
}
});
},
'select with selectParam': function () {
store.selectParam = 'select-param';
var selectCollection = store.select(['prop1', 'prop2']);
return runCollectionTest(selectCollection, {
queryParams: {
'select-param': 'prop1,prop2'
}
});
},
'range': function () {
return runCollectionTest(store, {start: 15, end: 25}, {
queryParams: {
'limit(10,15)': ''
}
});
},
'range with rangeParam': function () {
store.rangeStartParam = 'start';
store.rangeCountParam = 'count';
return runCollectionTest(store, {start: 15, end: 25}, {
queryParams: {
'start': '15',
'count': '10'
}
});
},
'range with headers': function () {
store.useRangeHeaders = true;
return runCollectionTest(store, {start: 15, end: 25}, {
headers: {
'Range': 'items=15-24'
}
});
},
'range with headers without end': function () {
store.useRangeHeaders = true;
return runCollectionTest(store, {start: 15, end: Infinity}, {
headers: {
'Range': 'items=15-Infinity'
}
});
},
'filter+sort+fetchRange': function () {
var filter = { prop1: 'Prop1Value', prop2: 'Prop2Value' };
var collection = store.filter(filter).sort('prop1');
return runCollectionTest(collection, {start: 15, end: 25}, {
queryParams: lang.mixin({}, filter, {
'limit(10,15)': '',
'sort(+prop1)': ''
})
});
},
'composition with client-side queriers': function () {
var RestWithQueryEngine = declare([ Store, SimpleQuery ], {
target: '/mockRequest/'
});
var store = new RestWithQueryEngine(),
expectedResults = [
{ id: 1, name: 'one', odd: true },
{ id: 2, name: 'two', odd: false },
{ id: 3, name: 'three', odd: true }
];
mockRequest.setResponseText(JSON.stringify(expectedResults));
var filter = { odd: true },
filteredCollection = store.filter(filter),
sortedCollection,
getTopQueryLogEntry = function (collection) {
var queryLog = collection.queryLog;
return queryLog[queryLog.length - 1];
},
querier;
assert.strictEqual(filteredCollection.queryLog.length, 1);
return when(filteredCollection.fetch()).then(function (results) {
mockRequest.assertQueryParams(filter);
assert.strictEqual(results.length, expectedResults.length);
var queryLogEntry = getTopQueryLogEntry(filteredCollection);
assert.property(queryLogEntry, 'querier');
querier = queryLogEntry.querier;
var filteredResults = querier(expectedResults);
assert.equal(filteredResults.length, 2);
assert.deepEqual(filteredResults[0], expectedResults[0]);
assert.deepEqual(filteredResults[1], expectedResults[2]);
sortedCollection = filteredCollection.sort('id', true);
assert.strictEqual(sortedCollection.queryLog.length, 2);
return sortedCollection.fetch();
}).then(function (results) {
mockRequest.assertQueryParams({ 'sort(-id)': '' });
assert.strictEqual(results.length, expectedResults.length);
var queryLogEntry = getTopQueryLogEntry(sortedCollection);
assert.property(queryLogEntry, 'querier');
querier = (function () {
var existingQueryer = querier,
newQueryer = queryLogEntry.querier;
return function (data) {
return newQueryer(existingQueryer(data));
};
})();
var sortedFilteredResults = querier(expectedResults);
assert.equal(sortedFilteredResults.length, 2);
assert.deepEqual(sortedFilteredResults[0], expectedResults[2]);
assert.deepEqual(sortedFilteredResults[1], expectedResults[0]);
return sortedCollection.fetchRange({start: 0, end: 25});
}).then(function (results) {
mockRequest.assertQueryParams({
'sort(-id)': '',
'limit(25)': ''
});
assert.strictEqual(results.length, expectedResults.length);
});
}
};
}
registerSuite(createRequestTests(Request));
registerSuite({
name: 'dstore Request default',
cancel: function () {
var store = new Request({
target: '/test/'
});
var results = store.fetch();
assert.property(results.response, 'then');
results.cancel();
}
});
return createRequestTests;
});
@@ -0,0 +1,157 @@
define([
'intern!object',
'intern/chai!assert',
'require',
'dojo/_base/array',
'dojo/_base/declare',
'../RequestMemory',
'../Trackable'
], function (registerSuite, assert, require, arrayUtil, declare, RequestMemory, Trackable) {
var store;
function mapResultIds(results) {
return arrayUtil.map(results, function (item) {
return item.id;
});
}
registerSuite({
name: 'RequestMemory',
beforeEach: function () {
store = new (declare([RequestMemory, Trackable]))({
target: require.toUrl('dstore/tests/data/treeTestRoot')
});
},
'.get': function () {
return store.get('node2').then(function (item) {
assert.strictEqual(
JSON.stringify(item),
JSON.stringify({ 'id': 'node2', 'name':'node2', 'someProperty':'somePropertyB' })
);
});
},
'.put': function () {
var updatedItem;
var updateEventFired;
store.on('update', function (event) {
assert.isObject(event.target);
updateEventFired = true;
});
return store.get('node5').then(function (item) {
item.changed = true;
updatedItem = item;
var putResult = store.put(updatedItem);
assert.isDefined(putResult && putResult.then, 'put should return a promise');
return putResult;
}).then(function () {
return store.get('node5');
}).then(function (item) {
assert.strictEqual(JSON.stringify(item), JSON.stringify(updatedItem));
assert.isTrue(updateEventFired);
});
},
'.put downstream': function () {
var updatedItem;
var updateEventFired;
var node4View = store.filter({name: 'node4'});
node4View.on('update', function (event) {
assert.isObject(event.target);
updateEventFired = true;
});
return node4View.get('node4').then(function (item) {
item.changed = true;
updatedItem = item;
return store.put(updatedItem);
}).then(function () {
return store.get('node4');
}).then(function (item) {
assert.strictEqual(JSON.stringify(item), JSON.stringify(updatedItem));
assert.isTrue(updateEventFired);
});
},
'.add': function () {
var newItem = { 'id': 'node6', 'name':'node6', 'someProperty':'somePropertyB' };
var addEventFired;
store.on('add', function () {
addEventFired = true;
});
var addResult = store.add(newItem);
assert.isDefined(addResult && addResult.then, 'add should return a promise');
return addResult.then(function () {
return store.get('node6');
}).then(function (item) {
assert.strictEqual(JSON.stringify(item), JSON.stringify(newItem));
assert.isTrue(addEventFired);
});
},
'.add downstream': function () {
var newItem = { 'id': 'node7', 'name':'node7', 'someProperty':'somePropertyB' };
var addEventFired;
var node7View = store.filter({name: 'node7'});
node7View.on('add', function () {
addEventFired = true;
});
var addResult = store.add(newItem);
return addResult.then(function () {
assert.isTrue(addEventFired);
});
},
'.remove': function () {
return store.get('node3').then(function (item) {
assert.ok(item);
var removeResult = store.remove('node3');
assert.isDefined(removeResult && removeResult.then, 'remove should return a promise');
return removeResult;
}).then(function () {
return store.get('node3');
}).then(function (item) {
assert.strictEqual(arguments.length, 1);
assert.isUndefined(item);
});
},
'filter': function () {
var results = store.filter({ someProperty: 'somePropertyB' }).fetch().then(mapResultIds);
return results.then(function (data) {
assert.deepEqual(data.slice(), [ 'node2', 'node5' ]);
});
},
'.sort': function () {
var results = store.sort([
{ property: 'someProperty', descending: true },
{ property: 'name', descending: false }
]).fetch().then(mapResultIds);
return results.then(function (data) {
assert.deepEqual(data.slice(), [ 'node3', 'node2', 'node5', 'node1', 'node4' ]);
});
},
'.fetchRange': function () {
var results = store.fetchRange({start: 1, end: 4}).then(mapResultIds);
return results.then(function (data) {
assert.deepEqual(data.slice(), [ 'node2', 'node3', 'node4' ]);
});
},
'combined queries': function () {
var results = store
.filter(function (item) {
return item.someProperty !== 'somePropertyB';
})
.sort('name', true)
.fetchRange({start: 1, end: 3}).then(mapResultIds);
return results.then(function (data) {
assert.deepEqual(data.slice(), [ 'node3', 'node1' ]);
});
}
});
});
@@ -0,0 +1,181 @@
define([
'intern!object',
'intern/chai!assert',
'dstore/Rest',
'./Request',
'dojo/_base/lang',
'dojo/aspect',
'./mockRequest',
'dojo/text!./data/node1.1'
], function (
registerSuite,
assert,
Rest,
createRequestTests,
lang,
aspect,
mockRequest,
nodeData_1_1
) {
function runHeaderTest(method, args) {
return store[method].apply(store, args).then(function () {
mockRequest.assertRequestHeaders(requestHeaders);
mockRequest.assertRequestHeaders(globalHeaders);
});
}
var globalHeaders = {
'test-global-header-a': 'true',
'test-global-header-b': 'yes'
};
var requestHeaders = {
'test-local-header-a': 'true',
'test-local-header-b': 'yes',
'test-override': 'overridden'
};
var store;
var tests = createRequestTests(Rest);
aspect.after(tests, 'beforeEach', function () {
store = createRequestTests.store;
})
lang.mixin(tests, {
name: 'dstore Rest',
'get': function () {
mockRequest.setResponseText(nodeData_1_1);
return store.get('data/node1.1').then(function (object) {
assert.strictEqual(object.name, 'node1.1');
assert.strictEqual(object.describe(), 'name is node1.1');
assert.strictEqual(object.someProperty, 'somePropertyA1');
});
},
'headers get 1': function () {
return runHeaderTest('get', [ 'mockRequest/1', requestHeaders ]);
},
'headers get 2': function () {
return runHeaderTest('get', [ 'mockRequest/2', { headers: requestHeaders } ]);
},
'headers remove': function () {
return runHeaderTest('remove', [ 'mockRequest/3', { headers: requestHeaders } ]);
},
'headers put': function () {
return runHeaderTest('put', [
{},
{
id: 'mockRequest/4',
headers: requestHeaders
}
]);
},
'headers add': function () {
return runHeaderTest('add', [
{},
{
id: 'mockRequest/5',
headers: requestHeaders
}
]);
},
'put object without ID': function () {
var objectWithoutId = { name: 'one' };
mockRequest.setResponseText(store.stringify(objectWithoutId));
return store.put(objectWithoutId).then(function () {
mockRequest.assertHttpMethod('POST');
});
},
'put object with ID': function () {
var objectWithId = { id: 1, name: 'one' };
mockRequest.setResponseText(store.stringify(objectWithId));
return store.put(objectWithId).then(function () {
mockRequest.assertHttpMethod('PUT');
});
},
'put object with store.defaultNewToStart': function () {
function testPutPosition(object, options, expectedHeaders) {
store.defaultNewToStart = undefined;
return store.put(object, options).then(function () {
mockRequest.assertRequestHeaders(expectedHeaders.defaultUndefined);
}).then(function () {
store.defaultNewToStart = false;
return store.put(object, options);
}).then(function () {
mockRequest.assertRequestHeaders(expectedHeaders.defaultEnd);
store.defaultNewToStart = true;
return store.put(object, options);
}).then(function () {
mockRequest.assertRequestHeaders(expectedHeaders.defaultStart);
});
}
var objectWithId = { id: 1, name: 'one' },
objectWithoutId = { name: 'missing identity' },
optionsWithoutOverwrite = {},
optionsWithOverwriteTrue = { overwrite: true },
optionsWithOverwriteFalse = { overwrite: false },
noExpectedPositionHeaders = {
defaultUndefined: { 'Put-Default-Position': null },
defaultEnd: { 'Put-Default-Position': null },
defaultStart: { 'Put-Default-Position': null }
},
expectedPositionHeaders = {
defaultUndefined: { 'Put-Default-Position': 'end' },
defaultEnd: { 'Put-Default-Position': 'end' },
defaultStart: { 'Put-Default-Position': 'start' }
};
var tests = [
[ objectWithId, optionsWithoutOverwrite, noExpectedPositionHeaders ],
[ objectWithId, optionsWithOverwriteTrue, noExpectedPositionHeaders ],
[ objectWithId, optionsWithOverwriteFalse, expectedPositionHeaders ],
[ objectWithoutId, optionsWithoutOverwrite, expectedPositionHeaders ],
[ objectWithoutId, optionsWithOverwriteTrue, expectedPositionHeaders ],
[ objectWithoutId, optionsWithOverwriteFalse, expectedPositionHeaders ]
];
var promise = testPutPosition.apply(null, tests[0]);
for (var i = 0; i < tests.length; ++i) {
promise = promise.then(function () {
return testPutPosition.apply(null, tests[i]);
});
}
},
'put object with options.beforeId': function () {
store.defaultNewToStart = true;
return store.put({ id: 1, name: 'one' }, { beforeId: 123 }).then(function () {
mockRequest.assertRequestHeaders({
'Put-Before': 123,
'Put-Default-Position': null
});
}).then(function () {
return store.put({ id: 2, name: 'two' }, { beforeId: null });
}).then(function () {
mockRequest.assertRequestHeaders({
'Put-Before': null,
'Put-Default-Position': 'end'
});
});
},
'get and save': function () {
var expectedObject = { id: 1, name: 'one' };
mockRequest.setResponseText(store.stringify(expectedObject));
return store.get('anything').then(function (object) {
mockRequest.setResponseText(store.stringify(expectedObject));
return store.put(object).then(function (result) {
assert.deepEqual(store.stringify(result), store.stringify(expectedObject));
});
});
}
});
registerSuite(tests);
});
@@ -0,0 +1,89 @@
define([
'../SimpleQuery',
'dojo/_base/declare',
'../Filter',
'intern!object',
'intern/chai!assert'
], function (SimpleQuery, declare, Filter, registerSuite, assert) {
var testData = [
{ id: 1, name: 'one', odd: true },
{ id: 2, name: 'two', odd: false },
{ id: 3, name: 'three', odd: true },
{ id: 4, name: 'four', odd: false },
{ id: 5, name: 'five', odd: true }
];
var simpleQuery = new SimpleQuery();
registerSuite({
name: 'SimpleQuery',
'filter with predicate': function () {
var filter = simpleQuery._createFilterQuerier({
type: 'function',
args: [function (o) { return o.odd; }]
});
assert.deepEqual(filter(testData), [
{ id: 1, name: 'one', odd: true },
{ id: 3, name: 'three', odd: true },
{ id: 5, name: 'five', odd: true }
]);
},
'filter with object': function () {
var filterExpression = new Filter();
var filter = simpleQuery._createFilterQuerier(filterExpression.eq('odd', false));
assert.deepEqual(filter(testData), [
{ id: 2, name: 'two', odd: false },
{ id: 4, name: 'four', odd: false }
]);
},
'sort with array of sort attributes': function () {
var sort = simpleQuery._createSortQuerier([
{ property: 'odd' },
{ property: 'name', descending: true }
]);
assert.deepEqual(sort(testData), [
{ id: 2, name: 'two', odd: false },
{ id: 4, name: 'four', odd: false },
{ id: 3, name: 'three', odd: true },
{ id: 1, name: 'one', odd: true },
{ id: 5, name: 'five', odd: true }
]);
},
'sort with comparator': function () {
var sort = simpleQuery._createSortQuerier(function (a, b) {
a = a.name;
b = b.name;
return (a < b) ? -1 : (a === b ? 0 : 1);
});
assert.deepEqual(sort(testData), [
{ id: 5, name: 'five', odd: true },
{ id: 4, name: 'four', odd: false },
{ id: 1, name: 'one', odd: true },
{ id: 3, name: 'three', odd: true },
{ id: 2, name: 'two', odd: false }
]);
},
'nested queries': function () {
var f = new Filter();
var isEven = f.eq('odd', false);
var isOdd = f.eq('odd', true);
var isZero = f.eq('name', 'zero');
var isOne = f.eq('name', 'one');
var query = f.or(f.and(isEven, isZero), f.and(isOdd, isOne));
var filter = simpleQuery._createFilterQuerier(query);
assert.deepEqual(filter(testData), [
{ id: 1, name: 'one', odd: true }
]);
}
});
});
@@ -0,0 +1,233 @@
define([
'../Store',
'dojo/_base/declare',
'dojo/_base/lang',
'intern!object',
'intern/chai!assert'
], function (Store, declare, lang, registerSuite, assert) {
var Model = declare(null, {
constructor: function (args) {
declare.safeMixin(this, args);
}
});
var store;
registerSuite({
name: 'dstore Store',
beforeEach: function () {
store = new Store();
},
'getIdentity and _setIdentity': {
'direct property access and assignment': function () {
var object = { id: 'default', 'custom-id': 'custom' };
assert.strictEqual(store.getIdentity(object), 'default');
store._setIdentity(object, 'assigned-id');
assert.strictEqual(object.id, 'assigned-id');
assert.strictEqual(store.getIdentity(object), object.id);
store.idProperty = 'custom-id';
assert.strictEqual(store.getIdentity(object), 'custom');
store._setIdentity(object, 'assigned-id');
assert.strictEqual(object['custom-id'], 'assigned-id');
assert.strictEqual(store.getIdentity(object), object['custom-id']);
},
'getter and setter': function () {
var object = {
_properties: {
id: 'default',
'custom-id': 'custom',
},
get: function (name) {
return this._properties[name];
},
set: function (name, value) {
this._properties[name] = value;
}
};
assert.strictEqual(store.getIdentity(object), 'default');
store._setIdentity(object, 'assigned-id');
assert.strictEqual(object._properties.id, 'assigned-id');
assert.strictEqual(store.getIdentity(object), object._properties.id);
store.idProperty = 'custom-id';
assert.strictEqual(store.getIdentity(object), 'custom');
store._setIdentity(object, 'assigned-id');
assert.strictEqual(object._properties['custom-id'], 'assigned-id');
assert.strictEqual(store.getIdentity(object), object._properties['custom-id']);
}
},
'filter': function () {
var filter1 = { prop1: 'one' },
expectedQueryLog1 = [ {
type: 'filter', arguments: [ filter1 ], normalizedArguments: [ {
type: 'eq',
args: ['prop1', 'one']
} ]
} ],
filter2 = function filterFunc() {},
expectedQueryLog2 = expectedQueryLog1.concat({
type: 'filter', arguments: [ filter2 ], normalizedArguments: [ {
type: 'function',
args: [filter2]
} ]
}),
filteredCollection;
filteredCollection = store.filter(filter1);
// deepEqual just won't work on the data in these
assert.equal(JSON.stringify(filteredCollection.queryLog), JSON.stringify(expectedQueryLog1));
filteredCollection = filteredCollection.filter(filter2);
assert.equal(JSON.stringify(filteredCollection.queryLog), JSON.stringify(expectedQueryLog2));
},
'sort': function () {
var sortObject = { property: 'prop1', descending: true },
sortObjectArray = [ sortObject, { property: 'prop2' } ],
comparator = function comparator() {},
expectedQueryLog1 = [ {
type: 'sort',
arguments: [ sortObject.property, sortObject.descending ],
normalizedArguments: [ [ sortObject ] ]
} ],
expectedQueryLog2 = [ {
type: 'sort',
arguments: [ sortObject ],
normalizedArguments: [ [ sortObject ] ]
} ],
expectedQueryLog3 = expectedQueryLog2.concat({
type: 'sort',
arguments: [ sortObjectArray ],
normalizedArguments: [ [ sortObject, lang.mixin({ descending: false }, sortObjectArray[1]) ] ]
}),
expectedQueryLog4 = expectedQueryLog3.concat({
type: 'sort', arguments: [ comparator ], normalizedArguments: [ comparator ]
}),
sortedCollection;
sortedCollection = store.sort(sortObject.property, sortObject.descending);
assert.deepEqual(sortedCollection.queryLog, expectedQueryLog1);
sortedCollection = store.sort(sortObject);
assert.deepEqual(sortedCollection.queryLog, expectedQueryLog2);
sortedCollection = sortedCollection.sort(sortObjectArray);
assert.deepEqual(sortedCollection.queryLog, expectedQueryLog3);
sortedCollection = sortedCollection.sort(comparator);
assert.deepEqual(sortedCollection.queryLog, expectedQueryLog4);
},
'restore': function () {
var TestModel = declare(Model, {
_restore: function (Constructor) {
// use constructor based restoration
var restored = new Constructor(this);
restored.restored = true;
return restored;
}
});
var store = new Store({
Model: TestModel
});
var restoredObject = store._restore({foo: 'original'});
assert.strictEqual(restoredObject.foo, 'original');
assert.strictEqual(restoredObject.restored, true);
assert.isTrue(restoredObject instanceof TestModel);
},
events: function () {
var methodCalls = [],
events = [];
// rely on autoEventEmits
var store = new (declare(Store, {
put: function (object) {
methodCalls.push('put');
return object;
},
add: function (object) {
methodCalls.push('add');
return object;
},
remove: function (id) {
methodCalls.push('remove');
}
}))();
store.on('add', function (event) {
events.push(event.type);
});
// test comma delimited as well
store.on('update, delete', function (event) {
events.push(event.type);
});
store.put({});
store.add({});
store.remove(1);
assert.deepEqual(methodCalls, ['put', 'add', 'remove']);
assert.deepEqual(events, ['update', 'add', 'delete']);
},
'events with beforeId': function () {
var store = new Store(),
beforeIds = [];
store.on('add, update', function (event) {
beforeIds.push(event.beforeId);
});
store.add({}, { beforeId: 123 });
store.put({}, { beforeId: 321 });
assert.deepEqual(beforeIds, [ 123, 321 ]);
},
'emit should catch errors thrown by listeners': function () {
var store = new Store(),
testEmit = lang.hitch(store, 'emit', 'test-event');
assert.doesNotThrow(testEmit);
store.on('test-event', function () {
throw new Error('listener error');
});
assert.doesNotThrow(testEmit);
},
forEach: function () {
var store = new (declare(Store, {
fetch: function () {
return [0, 1, 2];
}
}))();
var results = [];
store.forEach(function (item, i, instance) {
assert.strictEqual(item, i);
results.push(item);
assert.strictEqual(instance, store);
});
assert.deepEqual(results, [0, 1, 2]);
},
'extends declare-based Model constructors and adds a _store reference on the prototype': function () {
var DeclaredTestModel = declare(null);
var store = new Store({ Model: DeclaredTestModel });
assert.notStrictEqual(store.Model, DeclaredTestModel);
assert.instanceOf(new store.Model(), DeclaredTestModel);
},
'does not extend Model constructors not based on declare': function () {
function TestModel() {}
var store = new Store({ Model: TestModel });
assert.strictEqual(store.Model, TestModel);
}
});
});
@@ -0,0 +1,638 @@
define([
'intern!object',
'intern/chai!assert',
'dojo/_base/array',
'dojo/_base/declare',
'dojo/_base/lang',
'dstore/Memory',
'dstore/Store',
'dstore/Trackable',
'dstore/SimpleQuery'
], function (registerSuite, assert, arrayUtil, declare, lang, Memory, Store, Trackable, SimpleQuery) {
function createData(numItems) {
var data = [];
var i;
for (i = 0; i < numItems; i++) {
data.push({id: i, name: 'item ' + i, order: i});
}
return data;
}
function createTestSuite(suiteName, createStore) {
// A store for testing Trackable with only partial in-memory data
var PartialDataStore = declare([ Store, SimpleQuery ], (function () {
var proto = {
constructor: function () {
this.backingMemoryStore = createStore({ data: this.data }, Memory);
delete this.data;
}
};
arrayUtil.forEach(
[
'getIdentity', 'get', 'add', 'put', 'remove', 'getSync', 'addSync', 'putSync', 'removeSync',
'fetch', 'fetchRange', 'fetchSync', 'fetchRangeSync'
],
function (method) {
proto[method] = function () {
var backingStore = this.backingMemoryStore;
return backingStore[method].apply(backingStore, arguments);
};
}
);
arrayUtil.forEach(['filter', 'sort'], function (method) {
proto[method] = function () {
var newBackingStore = this.backingMemoryStore[method].apply(this.backingMemoryStore, arguments);
return lang.mixin(this.inherited(arguments), {
backingMemoryStore: newBackingStore
});
};
});
return proto;
})());
function createPrimeNumberStore() {
return createStore({
data: [
{id: 0, name: 'zero', even: true, prime: false},
{id: 1, name: 'one', prime: false},
{id: 2, name: 'two', even: true, prime: true},
{id: 3, name: 'three', prime: true},
{id: 4, name: 'four', even: true, prime: false},
{id: 5, name: 'five', prime: true}
]
}, Memory);
}
function createPartialDataStore(numItems) {
return createStore({
data: createData(numItems),
// Make backing store an observed collection so its data is kept up-to-date
track: function () {
this.backingMemoryStore = this.backingMemoryStore.track();
return this.inherited(arguments);
}
}, PartialDataStore);
}
var store = createPrimeNumberStore();
return {
name: suiteName,
'get': function () {
assert.strictEqual(store.getSync(1).name, 'one');
assert.strictEqual(store.getSync(4).name, 'four');
assert.isTrue(store.getSync(5).prime);
},
'filter': function () {
var filteredCollection = store.filter({prime: true});
var changes = [], secondChanges = [];
var tracked = filteredCollection.track();
tracked.fetch();
assert.strictEqual(tracked._results.length, 3);
tracked.on('add, update, delete', function (event) {
changes.push(event);
});
var secondObserverUpdate = tracked.on('update', function (event) {
secondChanges.push(event);
});
var secondObserverRemove = tracked.on('delete', function (event) {
secondChanges.push(event);
});
var secondObserverAdd = tracked.on('add', function (event) {
secondChanges.push(event);
});
var expectedChanges = [],
expectedSecondChanges = [];
var two = tracked._results[0];
two.prime = false;
store.put(two); // should remove it from the array
assert.strictEqual(tracked._results.length, 2);
expectedChanges.push({
type: 'update',
target: two,
index: undefined,
previousIndex: 0,
totalLength: 2
});
expectedSecondChanges.push(expectedChanges[expectedChanges.length - 1]);
secondObserverUpdate.remove();
secondObserverRemove.remove();
secondObserverAdd.remove();
var one = store.getSync(1);
one.prime = true;
store.put(one); // should add it
expectedChanges.push({
type: 'update',
target: one,
index: 2,
previousIndex: undefined,
totalLength: 3
});
assert.strictEqual(tracked._results.length, 3);
// shouldn't be added
var six = {id: 6, name: 'six'};
store.add(six);
assert.strictEqual(tracked._results.length, 3);
expectedChanges.push({
type: 'add',
target: store._restore(six),
index: undefined,
totalLength: 3
// no index because the addition doesn't have a place in the filtered results
});
// should be added
var seven = {id: 7, name: 'seven', prime: true};
store.add(seven);
assert.strictEqual(tracked._results.length, 4);
expectedChanges.push({
type: 'add',
target: store._restore(seven),
index: 3,
totalLength: 4
});
var three = store.getSync(3);
store.remove(3);
expectedChanges.push({
type: 'delete',
id: 3,
target: store._restore(three),
previousIndex: 0,
totalLength: 3
});
assert.strictEqual(tracked._results.length, 3);
assert.deepEqual(secondChanges, expectedSecondChanges);
assert.deepEqual(changes, expectedChanges);
},
'filter with zero id': function () {
var filteredCollection = store.filter({});
var results;
filteredCollection.fetch().then(function (data) {
results = data;
});
assert.strictEqual(results.length, 7);
var tracked = filteredCollection.track();
tracked.on('update', function (event) {
// we only do puts so previous & new indices must always been the same
assert.ok(event.index === event.previousIndex);
});
store.put({id: 5, name: '-FIVE-', prime: true});
store.put({id: 0, name: '-ZERO-', prime: false});
},
'paging with store.data': function () {
var results,
bigStore = createStore({ data: createData(100) }, Memory),
bigFiltered = bigStore.filter({}).sort('order');
var observations = [];
// temporarily hide this so we don't load everything
bigFiltered.fetchSync = null;
var bigObserved = bigFiltered.track();
delete bigFiltered.fetchSync;
bigObserved.on('update', function (event) {
observations.push(event);
console.log(' observed: ', event);
});
bigObserved.on('add', function (event) {
observations.push(event);
console.log(' observed: ', event);
});
bigObserved.on('delete', function (event) {
observations.push(event);
console.log(' observed: ', event);
});
bigObserved.fetchRange({ start: 0, end: 25 });
bigObserved.fetchRange({ start: 25, end: 50 });
bigObserved.fetchRange({ start: 50, end: 75 });
bigObserved.fetchRange({ start: 75, end: 100 });
var results = bigObserved._partialResults;
bigStore.add({id: 101, name: 'one oh one', order: 2.5});
assert.strictEqual(results.length, 101);
assert.strictEqual(observations.length, 1);
bigStore.remove(101);
assert.strictEqual(observations.length, 2);
assert.strictEqual(results.length, 100);
bigStore.add({id: 102, name: 'one oh two', order: 26.5});
assert.strictEqual(results.length, 101);
assert.strictEqual(observations.length, 3);
},
'paging with store._partialResults': function () {
var bigStore = createPartialDataStore(100),
bigFiltered = bigStore.filter({}).sort('order'),
latestObservation,
item,
bigObserved;
// temporarily hide this so we don't load everything
bigFiltered.fetchSync = null;
bigObserved = bigFiltered.track();
delete bigFiltered.fetchSync;
var assertObservationIs = function (expectedObservation) {
expectedObservation = lang.delegate(expectedObservation);
if (expectedObservation.type in { add: 1, update: 1 }
&& !('index' in expectedObservation)) {
expectedObservation.index = undefined;
}
if (expectedObservation.type in { update: 1, 'delete': 1 }
&& !('previousIndex' in expectedObservation)) {
expectedObservation.previousIndex = undefined;
}
assert.deepEqual(latestObservation, expectedObservation);
};
bigObserved.on('update', function (event) {
latestObservation = event;
});
bigObserved.on('add', function (event) {
latestObservation = event;
});
bigObserved.on('delete', function (event) {
latestObservation = event;
});
// An update outside of requested ranges has an indeterminate index
item = bigStore.getSync(0);
item.order = 1.25;
bigStore.put(item);
assertObservationIs({ type: 'update', target: item });
// An addition outside of requested ranges has an indeterminate index
item = bigStore._restore({ id: 1.5, name: 'item 1.5', order: 1.5 });
bigStore.add(item);
assertObservationIs({ type: 'add', target: item });
// Remove additional item to make subsequent item indices and id's line up
bigStore.remove(item.id);
assertObservationIs({ type: 'delete', id: item.id });
// An update sorted to the beginning of a range and the data has a known index
bigObserved.fetchRange({ start: 0, end: 25 });
item = bigStore.getSync(0);
item.order = 0;
bigStore.put(item);
assertObservationIs({ type: 'update', target: item, index: 0, previousIndex: 1, totalLength: 100 });
// An addition sorted to the beginning of a range and the data has a known index
item = bigStore._restore({ id: -1, name: 'item -1', order: -1 });
bigStore.add(item);
assertObservationIs({ type: 'add', target: item, index: 0, totalLength: 101 });
// Remove additional item to make subsequent item indices and id's line up
bigStore.remove(item.id);
assertObservationIs({ type: 'delete', id: item.id, previousIndex: 0, totalLength: 100 });
// An update sorted to the end of a range has an indeterminate index
item = bigStore.getSync(24);
item.name = 'item 24 updated';
bigStore.put(item);
assertObservationIs({ type: 'update', target: item, previousIndex: 24, totalLength: 99 });
// An addition sorted to the end of a range has an indeterminate index
item = bigStore._restore({ id: 24.1, name: 'item 24.1', order: 24.1 });
bigStore.add(item);
assertObservationIs({ type: 'add', target: item, totalLength: 99 });
// Remove additional item to make subsequent item indices and id's line up
bigStore.remove(item.id);
assertObservationIs({ type: 'delete', id: item.id, totalLength: 99 });
// The previous update with an undetermined index resulted in an item dropping from the first range
// and the first range being reduced to 0-23 instead of 0-24.
// Requesting 24-50 instead of 25-50 in order to request a contiguous range.
// Trackable should treat contiguous requested ranges as a single range.
bigObserved.fetchRange({ start: 24, end: 50 });
// An update sorted to the end of a range but adjacent to another range has a known index
item = bigStore.getSync(22);
item.order = 23.1;
bigStore.put(item);
assertObservationIs({ type: 'update', target: item, index: 23, previousIndex: 22, totalLength: 100 });
// An addition sorted to the end of a range but adjacent to another range has a known index
item = bigStore._restore({ id: 23.2, name: 'item 23.2', order: 23.2 });
bigStore.add(item);
assertObservationIs({ type: 'add', target: item, index: 24, totalLength: 101 });
// Remove additional item to make subsequent item indices and id's line up
bigStore.remove(item.id);
assertObservationIs({ type: 'delete', id: item.id, previousIndex: 24, totalLength: 100 });
// An update sorted to the beginning of a range but adjacent to another range has a known index
item = bigStore.getSync(25);
item.order = 23.9;
bigStore.put(item);
assertObservationIs({ type: 'update', target: item, index: 24, previousIndex: 25, totalLength: 100 });
// An addition sorted to the beginning of a range but adjacent to another range has a known index
item = bigStore._restore({ id: 23.8, name: 'item 23.8', order: 23.8 });
bigStore.add(item);
assertObservationIs({ type: 'add', target: item, index: 24, totalLength: 101 });
// Remove additional item to make subsequent item indices and id's line up
bigStore.remove(item.id);
assertObservationIs({ type: 'delete', id: item.id, previousIndex: 24, totalLength: 100 });
// Request range at end of data
bigObserved.fetchRange({ start: 75, end: 100 });
// An update at the end of a range and the data has a known index
item = bigStore.getSync(98);
item.order = 99.1;
bigStore.put(item);
assertObservationIs({ type: 'update', target: item, index: 99, previousIndex: 98, totalLength: 100 });
// An addition at the end of a range and the data has a known index
item = bigStore._restore({ id: 99.2, name: 'item 99.2', order: 99.2 });
bigStore.add(item);
assertObservationIs({ type: 'add', target: item, index: 100, totalLength: 101 });
// An update at the beginning of a range has an indeterminate index
item = bigStore.getSync(76);
item.order = 74.9;
bigStore.put(item);
assertObservationIs({ type: 'update', target: item, beforeIndex: 75, previousIndex: 76, totalLength: 100 });
// An addition at the beginning of a range has an indeterminate index
item = bigStore._restore({ id: 74.8, name: 'item 74.8', order: 74.8 });
bigStore.add(item);
assertObservationIs({ type: 'add', target: item, beforeIndex: 76, totalLength: 100 });
},
'paging releaseRange with store._partialResults': function () {
var itemCount = 100,
store = createPartialDataStore(itemCount),
rangeToBeEclipsed = { start: 5, end: 15 },
rangeToBeSplit = { start: 25, end: 45 },
rangeToBeHeadTrimmed = { start: 55, end: 65 },
rangeToBeTailTrimmed = { start: 80, end: 95 },
eclipsingRange = { start: 0, end: 20 },
splittingRange = { start: 30, end: 40 },
headTrimmingRange = { start: 50, end: 60 },
tailTrimmingRange = { start: 90, end: 100 };
// temporarily hide this so we don't load everything
store.fetchSync = null;
var trackedStore = store.track();
// restore
delete store.fetchSync;
var assertRangeDefined = function (start, end) {
for(var i = start; i < end; ++i) {
assert.notEqual(trackedStore._partialResults[i], undefined);
}
},
assertRangeUndefined = function (start, end) {
for(var i = start; i < end; ++i) {
assert.equal(trackedStore._partialResults[i], undefined);
}
};
// Remove all of a range
trackedStore.fetchRange({ start: rangeToBeEclipsed.start, end: rangeToBeEclipsed.end });
assertRangeDefined(rangeToBeEclipsed.start, rangeToBeEclipsed.end);
trackedStore.releaseRange(eclipsingRange.start, eclipsingRange.end);
assertRangeUndefined(rangeToBeEclipsed.start, rangeToBeEclipsed.end);
// Split a range
trackedStore.fetchRange({ start: rangeToBeSplit.start, end: rangeToBeSplit.end });
assertRangeDefined(rangeToBeSplit.start, rangeToBeSplit.end);
trackedStore.releaseRange(splittingRange.start, splittingRange.end);
assertRangeDefined(rangeToBeSplit.start, splittingRange.start);
assertRangeUndefined(splittingRange.start, splittingRange.end);
assertRangeDefined(splittingRange.end, rangeToBeSplit.end);
// Remove from range head
trackedStore.fetchRange({ start: rangeToBeHeadTrimmed.start, end: rangeToBeHeadTrimmed.end });
assertRangeDefined(rangeToBeHeadTrimmed.start, rangeToBeHeadTrimmed.end);
trackedStore.releaseRange(headTrimmingRange.start, headTrimmingRange.end);
assertRangeUndefined(headTrimmingRange.start, headTrimmingRange.end);
assertRangeDefined(headTrimmingRange.end, rangeToBeHeadTrimmed.end);
// Remove from range tail
trackedStore.fetchRange({ start: rangeToBeTailTrimmed.start, end: rangeToBeTailTrimmed.end });
assertRangeDefined(rangeToBeTailTrimmed.start, rangeToBeTailTrimmed.end);
trackedStore.releaseRange(tailTrimmingRange.start, tailTrimmingRange.end);
assertRangeDefined(rangeToBeTailTrimmed.start, tailTrimmingRange.start);
assertRangeUndefined(tailTrimmingRange.start, tailTrimmingRange.end);
},
'new item with default index': function () {
var store = createPartialDataStore(100);
// temporarily hide this so we don't load everything
store.fetchSync = null;
var trackedStore = store.track();
// restore
delete store.fetchSync;
return trackedStore.fetchRange({ start: 0, end: 25 }).then(function () {
var addEvent = null;
trackedStore.on('add', function (event) {
addEvent = event;
});
// add a new item with the default of bottom
// a new item with a default index outside a known range is treated as if it has no known index
var expectedNewItem = store._restore({ id: 200, name: 'item-200', order: Infinity });
store.add(expectedNewItem);
assert.isNotNull(addEvent);
assert.deepEqual(addEvent.target, expectedNewItem);
assert.isTrue('index' in addEvent);
assert.isUndefined(addEvent.index);
// choose a defaultIndex at the top (in known range)
store.defaultNewToStart = true;
// a new item with a default index within a known range has a known index
addEvent = null;
expectedNewItem = store._restore({ id: 201, name: 'item-201', order: Infinity });
store.add(expectedNewItem);
assert.isDefined(addEvent);
assert.deepEqual(addEvent.target, expectedNewItem);
assert.propertyVal(addEvent, 'index', 0);
store.defaultNewToStart = false;
return trackedStore.fetchRange({ start: 25, end: 102 }).then(function () {
// now add to the bottom, where it is in range
expectedNewItem = store._restore({ id: 202, name: 'item-202', order: Infinity });
store.add(expectedNewItem);
assert.isDefined(addEvent);
assert.deepEqual(addEvent.target, expectedNewItem);
assert.propertyVal(addEvent, 'index', 102);
});
});
},
'new item in empty store - with queryExecutor': function () {
var store = createStore({ data: [] }, Memory),
collection = store.filter({ type: 'test-item' }).track();
var actualEvent;
collection.on('add', function (event) {
actualEvent = event;
});
var expectedTarget = collection.addSync({
type: 'test-item',
id: 1,
name: 'one'
});
assert.deepEqual(actualEvent, {
type: 'add',
index: 0,
target: expectedTarget,
totalLength: 1
});
},
'new item in empty store - without queryExecutor': function () {
var store = createStore({ data: [] }, Memory),
collection = store.track();
var actualEvent;
collection.on('add', function (event) {
actualEvent = event;
});
var expectedTarget = collection.addSync({
type: 'test-item',
id: 1,
name: 'one'
});
assert.deepEqual(actualEvent, {
type: 'add',
index: 0,
target: expectedTarget,
totalLength: 1
});
},
'new item - with options.beforeId and queryExecutor': function () {
var store = createPrimeNumberStore(),
evenCollection = store.filter({ even: true }).track();
var data = evenCollection._results;
store.defaultNewToStart = true;
store.add({ id: 6, name: 'six', even: true }, { beforeId: 2 });
store.add({ id: -2, name: 'negative-two', even: true }, { beforeId: null });
assert.strictEqual(data[1].id, 6);
assert.strictEqual(data[2].id, 2);
assert.strictEqual(data[data.length - 1].id, -2);
},
'new item - with options.beforeId and no queryExecutor': function () {
var store = createPrimeNumberStore(),
collection = store.track();
collection.fetchSync();
var data = collection._results;
store.defaultNewToStart = true;
store.add({ id: 6, name: 'six', even: true }, { beforeId: 2 });
store.add({ id: -2, name: 'negative-two', even: true }, { beforeId: null });
assert.strictEqual(data[2].id, 6);
assert.strictEqual(data[3].id, 2);
assert.strictEqual(data[data.length - 1].id, -2);
},
'updated item - with options.beforeId and queryExecutor': function () {
var store = createPrimeNumberStore(),
evenCollection = store.filter({ even: true }).track();
var data = evenCollection._results;
store.defaultNewToStart = true;
store.put(store.getSync(4), { beforeId: 2 });
store.put(store.getSync(0), { beforeId: null });
assert.strictEqual(data[0].id, 4);
assert.strictEqual(data[1].id, 2);
assert.strictEqual(data[data.length - 1].id, 0);
},
'updated item - with options.beforeId and no queryExecutor': function () {
var store = createPrimeNumberStore(),
collection = store.track();
var data = collection._results;
store.defaultNewToStart = true;
store.put(store.getSync(4), { beforeId: 2 });
store.put(store.getSync(3), { beforeId: null });
assert.strictEqual(data[2].id, 4);
assert.strictEqual(data[3].id, 2);
assert.strictEqual(data[data.length - 1].id, 3);
},
'type': function () {
assert.isFalse(store === store.track(function () {}));
},
'track and collection.tracking.remove': function () {
var store = createStore({ data: createData(10) }, Memory),
trackedCollection = store.track();
assert.property(trackedCollection, 'tracking');
// Fetch so tracking has data to work with
trackedCollection.fetch();
var lastEvent = null;
trackedCollection.on('add, update', function (event) {
lastEvent = event;
});
store.put({ id: 11, name: 'item-11', order: 11 });
assert.isNotNull(lastEvent);
assert.isDefined(lastEvent.index);
trackedCollection.tracking.remove();
lastEvent = null;
store.put({ id: 12, name: 'item-12', order: 12 });
assert.isNull(lastEvent);
},
'fetch preserves totalLength API': function () {
var store = createStore({ data: [] }, Memory);
var trackedCollection = store.track();
var results = trackedCollection.fetch();
assert.isDefined(results.totalLength, 'totalLength should be defined on fetch results');
},
'fetchRange preserves totalLength API': function () {
var store = createStore({ data: [] }, Memory);
var trackedCollection = store.track();
var results = trackedCollection.fetchRange({ start: 0, end: 10 });
assert.isDefined(results.totalLength, 'totalLength should be defined on fetch results');
}
};
}
registerSuite(createTestSuite('Trackable mixin', function (properties, BaseStore) {
return new (declare([ BaseStore, Trackable ]))(properties);
}));
registerSuite(createTestSuite('Trackable.create()', function (properties, BaseStore) {
return Trackable.create(new BaseStore(properties));
}));
});
@@ -0,0 +1,67 @@
define([
'intern!object',
'intern/chai!assert',
'dojo/_base/declare',
'dstore/Memory',
'dstore/Tree'
], function (registerSuite, assert, declare, Memory, Tree) {
var TestStore = declare([ Memory, Tree ]);
var store = new TestStore({
Model: null,
data: [
{ parent: null, id: '1', name: 'root1' },
{ parent: '1', id: '1.1', name: 'child1.1' },
{ parent: '1', id: '1.2', name: 'child1.2' },
{ parent: '1.2', id: '1.2.1', name: 'grandchild1.2.1' },
{ parent: '1.2', id: '1.2.2', name: 'grandchild1.2.2' },
{ parent: '1', id: '1.3', name: 'child1.3' },
{ parent: null, id: '2', name: 'root2' },
{ parent: '2', id: '2.1', name: 'child2.1' },
{ parent: '2', id: '2.2', name: 'child2.2' },
{ parent: null, id: '3', name: 'root3' }
]
});
registerSuite({
name: 'dstore/Tree',
'getRootCollection': function () {
// slice() to get a copy of the results without a totalLength property
var results = store.getRootCollection().fetchSync().slice();
assert.deepEqual(results, [
{ parent: null, id: '1', name: 'root1' },
{ parent: null, id: '2', name: 'root2' },
{ parent: null, id: '3', name: 'root3' }
]);
},
'mayHaveChildren': function () {
assert.isTrue(store.mayHaveChildren({}));
assert.isTrue(store.mayHaveChildren({ hasChildren: true }));
assert.isFalse(store.mayHaveChildren({ hasChildren: false }));
},
'getChildren': function () {
var childlessObject = store.getSync('3');
var children = store.getChildren(childlessObject).fetchSync().slice();
assert.deepEqual(children, []);
var parentObject = store.getSync('1');
children = store.getChildren(parentObject).fetchSync().slice();
assert.deepEqual(children, [
{ parent: '1', id: '1.1', name: 'child1.1' },
{ parent: '1', id: '1.2', name: 'child1.2' },
{ parent: '1', id: '1.3', name: 'child1.3' }
]);
var grandparentObject = store.getSync('1.2');
children = store.getChildren(grandparentObject).fetchSync().slice();
assert.deepEqual(children, [
{ parent: '1.2', id: '1.2.1', name: 'grandchild1.2.1' },
{ parent: '1.2', id: '1.2.2', name: 'grandchild1.2.2' }
]);
}
});
});
@@ -0,0 +1,23 @@
define([
'./Store',
'./SimpleQuery',
'./Memory',
'./Request',
'./Rest',
'intern/node_modules/dojo/has!host-browser?./RequestMemory',
'./Trackable',
'./Cache',
'./Tree',
'./Csv',
'./Tree',
'./extensions/RqlQuery',
'./legacy/DstoreAdapter-Memory',
'intern/node_modules/dojo/has!host-browser?./legacy/DstoreAdapter-Rest',
'./charting/StoreSeries',
'./legacy/StoreAdapter-Memory',
'./legacy/StoreAdapter-AsyncMemory',
'intern/node_modules/dojo/has!host-browser?./legacy/StoreAdapter-JsonRest',
'./legacy/StoreAdapter-DojoData',
'intern/node_modules/dojo/has!host-browser?./LocalDB'
], function () {
});
@@ -0,0 +1,187 @@
define([
'intern!object',
'intern/chai!assert',
'dojo/_base/declare',
'dojo/json',
'dstore/Memory',
'dstore/Trackable',
'dstore/charting/StoreSeries'
], function(registerSuite, assert, declare, JSON, Memory, Trackable, StoreSeries){
var testSeriesObj = {
// This is a mock Series object for testing.
isTestSeries: true,
chart: {
updateSeries: function(){
// do nothing
},
delayedRender: function(){
// do nothing
}
}
};
var storeData, store;
registerSuite({
name: 'dstore charting StoreSeries',
beforeEach: function(){
storeData = [];
for(var i = 1; i <= 5; i++){
storeData.push({id: i, value: i, data: 'data' + i});
}
store = new Memory({
data: storeData
});
},
'initial fetch - default value': function(){
var series = new StoreSeries(store);
var data = series.data;
assert.lengthOf(data, 5);
assert.include(data, 1);
assert.include(data, 2);
assert.include(data, 3);
assert.include(data, 4);
assert.include(data, 5);
},
'initial fetch - value = data': function(){
var series = new StoreSeries(store, 'data');
var data = series.data;
assert.lengthOf(data, 5);
assert.include(data, 'data1');
assert.include(data, 'data2');
assert.include(data, 'data3');
assert.include(data, 'data4');
assert.include(data, 'data5');
},
'initial fetch - value function': function(){
var series = new StoreSeries(store, function(object){
return object.data + '-' + object.id;
});
var data = series.data;
assert.lengthOf(data, 5);
assert.include(data, 'data1-1');
assert.include(data, 'data2-2');
assert.include(data, 'data3-3');
assert.include(data, 'data4-4');
assert.include(data, 'data5-5');
},
'initial fetch - value object': function(){
var series = new StoreSeries(store, { data1: 'value', data2: 'data' });
var data = series.data;
assert.lengthOf(data, 5);
assert.strictEqual(JSON.stringify(data[0]), '{"data1":1,"data2":"data1"}');
assert.strictEqual(JSON.stringify(data[4]), '{"data1":5,"data2":"data5"}');
},
'set series object': function(){
testSeriesObj.updated = false;
var series = new StoreSeries(store, 'value');
series.setSeriesObject(testSeriesObj);
assert.strictEqual(series.series, testSeriesObj);
},
'fetch': function(){
var series = new StoreSeries(store, 'data');
var data = series.data;
assert.lengthOf(data, 5);
assert.include(data, 'data1');
assert.include(data, 'data2');
assert.include(data, 'data3');
assert.include(data, 'data4');
assert.include(data, 'data5');
store.put({id: 4, value: 4, data: 'mod data 44'});
data = series.data;
assert.lengthOf(data, 5);
assert.include(data, 'data1');
assert.include(data, 'data2');
assert.include(data, 'data3');
assert.include(data, 'data4');
assert.include(data, 'data5');
series.fetch();
data = series.data;
assert.lengthOf(data, 5);
assert.include(data, 'data1');
assert.include(data, 'data2');
assert.include(data, 'data3');
assert.include(data, 'mod data 44');
assert.include(data, 'data5');
},
'observable store - update': function(){
store = new (declare([Memory, Trackable]))({
data: storeData
});
var series = new StoreSeries(store, 'data');
var data = series.data;
assert.lengthOf(data, 5);
assert.include(data, 'data1');
assert.include(data, 'data2');
assert.include(data, 'data3');
assert.include(data, 'data4');
assert.include(data, 'data5');
store.put({id: 1, value: 1, data: 'mod data 11'});
data = series.data;
assert.lengthOf(data, 5);
assert.include(data, 'mod data 11');
assert.include(data, 'data2');
assert.include(data, 'data3');
assert.include(data, 'data4');
assert.include(data, 'data5');
store.put({id: 5, value: 5, data: 'mod data 55'});
data = series.data;
assert.lengthOf(data, 5);
assert.include(data, 'mod data 11');
assert.include(data, 'data2');
assert.include(data, 'data3');
assert.include(data, 'data4');
assert.include(data, 'mod data 55');
},
'observable store - delete': function(){
store = new (declare([Memory, Trackable]))({
data: storeData
});
var series = new StoreSeries(store, 'data');
var data = series.data;
assert.lengthOf(data, 5);
assert.include(data, 'data1');
assert.include(data, 'data2');
assert.include(data, 'data3');
assert.include(data, 'data4');
assert.include(data, 'data5');
store.remove(5);
data = series.data;
assert.lengthOf(data, 4);
assert.include(data, 'data1');
assert.include(data, 'data2');
assert.include(data, 'data3');
assert.include(data, 'data4');
store.remove(1);
data = series.data;
assert.lengthOf(data, 3);
assert.include(data, 'data2');
assert.include(data, 'data3');
assert.include(data, 'data4');
}
});
});
@@ -0,0 +1,8 @@
{ "identifier": "symbol", "idAttribute":"symbol", "label": "symbol","items": [
{ "symbol":"ANDT", "name":"Anduct", "historicPrice":[0.01,3.52,3.66,3.11,3.90,3.11,3.11], "open":3.13, "price":3.52, "updown":"^", "change":"+0.39 (+21%)", "low":3.13, "high":3.69 },
{ "symbol":"ATEU", "name":"Ations Europe", "historicPrice":[6.72,6.76,6.61,6.41,6.31,6.99,7.20], "open":6.72, "price":6.76, "updown":"^", "change":"+0.04 (+01%)", "low":6.56, "high":6.77 },
{ "symbol":"BGCN", "name":"Bagies Consulting", "historicPrice":[4.11,3.98,4.05,4.20,4.16,4.22,3.80], "open":4.11, "price":3.98, "updown":"v", "change":"-0.13 (-05%)", "low":3.77, "high":4.11 },
{ "symbol":"BAYC", "name":"BAY Corporation", "historicPrice":[9.79,9.60,9.50,2.23,9.45,9.76,9.99], "open":9.79, "price":9.60, "updown":"v", "change":"-0.19 (+14%)", "low":9.60, "high":9.81 },
{ "symbol":"CRCR", "name":"Corcor PLC", "historicPrice":[8.44,8.44,8.54,8.60,9.65,8.42,8.44], "open":8.44, "price":8.44, "updown":"--", "change":"+0.00 (+00%)", "low":8.22, "high":8.44 },
{ "symbol":"DTOA", "name":"Datio PLC", "historicPrice":[2.11,2.47,3.11,3.06,3.01,3.01,3.00], "open":2.11, "price":2.47, "updown":"^", "change":"+0.36 (+36%)", "low":2.11, "high":3.01 }
]}
@@ -0,0 +1,184 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<![endif]>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>dstore StoreSeries Test</title>
<link rel="stylesheet" href="../../../dijit/themes/claro/document.css"/>
<link rel="stylesheet" href="../../../dijit/themes/claro/claro.css"/>
<link rel="stylesheet" href="../../../dijit/tests/css/dijitTests.css"/>
<style>
.dojoxLegendNode {
border: 1px solid #ccc;
margin: 5px 10px 5px 10px;
padding: 3px;
}
.dojoxLegendText {
vertical-align: text-top;
padding-right: 10px
}
#charts {
clear: both;
margin-bottom: 50px;
}
.chart-area {
float: left;
border: 1px solid #ccc;
width: 450px;
height: 350px;
margin: 3px;
}
.chart {
width: 450px;
height: 300px;
}
</style>
</head>
<body class="claro">
<h1>StoreSeries Test</h1>
<p>
Use the spinner fields at the bottom to change the data. The charts listen to store changes an update automatically.
</p>
<div id="charts">
<div class="chart-area">
<div id="lines_legend"></div>
<div id="lines" class="chart"></div>
</div>
<div class="chart-area">
<div id="cols_legend"></div>
<div id="cols" class="chart"></div>
</div>
<div class="chart-area">
<div id="pie_legend"></div>
<div id="pie" class="chart"></div>
</div>
</div>
<div id="spinners"></div>
<script src="../../../dojo/dojo.js" data-dojo-config="isDebug: false, async: true"></script>
<script>
require(["dojo/_base/declare", "dojo/request", "dojox/charting/Chart", "dstore/charting/StoreSeries", "dojox/charting/themes/ThreeD",
"dojox/charting/widget/Legend", "dojox/charting/plot2d/Markers",
"dojox/charting/plot2d/Columns", "dojox/charting/plot2d/Pie", "dojox/charting/action2d/Tooltip",
"dojox/charting/action2d/MoveSlice", "dojox/charting/action2d/Magnify", "dojox/charting/action2d/Shake",
"dstore/Memory", "dstore/Trackable", "dijit/form/NumberSpinner", "dojo/_base/lang", "dojo/_base/array",
"dojo/dom-construct", "dojo/dom", "dojo/aspect", "dojox/charting/axis2d/Default"
], function(declare, request, Chart, StoreSeries, ThreeD, Legend, Markers, Columns, Pie, Tooltip, MoveSlice, Magnify, Shake, Memory, Trackable, NumberSpinner, lang, array, domConstruct, dom, aspect){
request("stock.json", { handleAs: "json" }).then(function(data){
var store = new (declare([Memory, Trackable]))({
data: data
});
makeCharts(store);
makeSpinners(store);
});
function addLegend(chart, node){
var legend = new Legend({chart: chart}, node);
aspect.after(chart, "render", lang.hitch(legend, "refresh"));
}
var templates = {
low: "<strong>{0}</strong>: <strong>low {1}</strong> &ndash; {2} &ndash; {3}",
price: "<strong>{0}</strong>: {1} &ndash; <strong>price {2}</strong> &ndash; {3}",
high: "<strong>{0}</strong>: {1} &ndash; {2} &ndash; <strong>high {3}</strong>"
};
function valTrans(value, object){
return {
y: object[value],
tooltip: lang.replace(
templates[value],
array.map(["symbol", "low", "price", "high"], function(field){
return object[field];
})
)
};
}
var chartL, chartC, chartP;
var makeCharts = function(store){
chartL = new Chart("lines").
setTheme(ThreeD).
addAxis("x", {fixLower: "major", fixUpper: "major", natural: true, majorTickStep: 5}).
addAxis("y", {vertical: true, fixLower: "major", fixUpper: "major", includeZero: true}).
addPlot("default", {type: Markers}).
addSeries("Price", new StoreSeries(
store, "price"));
new Magnify(chartL);
new Tooltip(chartL);
chartL.render();
addLegend(chartL, "lines_legend");
chartC = new Chart("cols").
setTheme(ThreeD).
addAxis("x", {natural: true}).
addAxis("y", {vertical: true, fixLower: "major", fixUpper: "major", includeZero: true}).
addPlot("default", {type: Columns}).
addSeries("Low", new StoreSeries(
store, lang.hitch(null, valTrans, "low"))).
addSeries("Price", new StoreSeries(
store, lang.hitch(null, valTrans, "price"))).
addSeries("High", new StoreSeries(
store, lang.hitch(null, valTrans, "high")));
new Shake(chartC, "default", {shiftY: 0});
new Tooltip(chartC);
chartC.render();
addLegend(chartC, "cols_legend");
chartP = new Chart("pie").
setTheme(ThreeD).
addPlot("default", {type: Pie, radius: 125}).
addSeries("Price", new StoreSeries(
store, {y: "price", text: "symbol", tooltip: "price"}));
new Tooltip(chartP);
new MoveSlice(chartP);
chartP.render();
addLegend(chartP, "pie_legend");
};
var makeSpinners = function(store){
var objects = store.data;
array.forEach(objects, function(m){
var nm = m.symbol;
var num = m.price;
console.log(nm, num);
var w = new NumberSpinner({
onChange: function(val){
val = val === 0 ? 0.01 : val; //HACKS the no label-when-zero bug
console.log("OC:", nm, val);
m.price = val;
store.put(m);
//store.setValues(m, "historicPrice", store.getValues("historicPrice").push(val));
console.log("OC:", nm, val);
},
value: num,
constraints: {min: 0, max: 10, places: 2},
className: "myField",
intermediateChanges: true
});
domConstruct.place('<label>' + nm + '</label>', dom.byId("spinners"), "last");
domConstruct.place(w.domNode, "spinners", "last");
});
var labels = array.map(objects, function(object, index){
return {
value: index + 1,
text: object.symbol
}
});
chartC.addAxis("x", {natural: true, labels: labels}).render();
};
});
</script>
</body>
</html>
@@ -0,0 +1,620 @@
id,last,first
1,Aaron,Brandon
2,Acord,Gary
3,Addesa,Michael
4,Agliullin,Mars
5,Aguilar,Vincente Jimenez
6,Albornez Mulligan,Jordi
7,Algesten,Martin
8,Allen,Robert
9,Alpert,John Izaaic
10,Ambrosi,Lucas
11,Anderson,Christian O.
12,Anderson,Matt
13,Andrews,Jesse
14,Anuruddha,Dilan
15,Armstrong,Kevin
16,Arnold,Brian
17,Arps,Jens
18,Arthur,Breton
19,Ascher,David
20,Ash,Christopher Baird
21,Atre,Mac
22,Aylott,Thomas
23,Bakaus,Paul
24,Baker,Christopher
25,Baker,Jonathan
26,Baker,Shawn
27,Balogh,Jeff
28,Baltopoulos,Ioannis
29,Bannister,Michael
30,Barber,Chris
31,Barnt,Dave
32,Barrett,Christopher
33,Barzilai,Joseph
34,Basler,Mark
35,Bates,Simon
36,Baz,David
37,Beard,Jacob Evan
38,Belongi,Mignon
39,Benton,Morgan
40,Berkhout,Just
41,Bernstein,Daniel
42,Bersac,Etienne
43,Bertet,Pierre
44,Best,Jason
45,Bevin,Geert
46,Bhatia,Naresh
47,Bing,Dong
48,Bird,Stefan
49,Bittinger,Nathan
50,Bodzak,Lee
51,Bolter,David
52,Bond-Caran,Jonathan
53,Bond,Edward
54,Bordet,Simone
55,Bottard,Eric
56,Bouchon,Paul
57,Boyer,Marc
58,Braid,Cameron
59,Braun,Terry
60,Brettle,Dean William
61,Brewster,John
62,Brinnand,John
63,Brown,Dallas
64,Brown,Eric W.
65,Buch,Daniel James
66,Buffone,Robert
67,Buffoni,Fabio
68,Burke,James Richard
69,Burke,Thomas
70,Cahill,Earl
71,Cakalic,James
72,Cappiello,John
73,Carpenter,Lee
74,Carreira,Jason
75,Carrera,Daniel
76,Carstensen,Brian
77,Cartier,Dennis
78,Caudill,Mark
79,Cavalier,Brian
80,Cavatorta,Andy
81,Chauduary,Dipen
82,Chen,Andrew
83,Childers,James
84,Chimene,Jeffrey
85,Chiumenti,Andrea
86,Cho,Youngho
87,Chong,Petra
88,Christiansen,Jason
89,Chung,Stephen
90,Cisternino,Antonio
91,Clark,Patrick
92,Clayton,David
93,Clemmons,Eric
94,Cochran,Samuel
95,Comer,Kevin
96,Conaway,Kevin
97,Conlon,Martin John
98,Conroy,Matt
99,Cooper,James Cameron
100,Cope,Wesley D.
101,Coquet,Miguel
102,Costan,Victor Marius
103,Crowder,Thomas
104,Cuervo,Jorge Martin
105,D'Souza,Michelle
106,Dalouche,Sami
107,Damour,Remy
108,Dangoor,Kevin
109,Davis,Conor
110,Davis,David Wayne
111,Davis,Lee
112,Davor,HRG
113,DeJarnett,Philip
114,Della-Franca,Sean
115,Demay,Vincent
116,Dierendonck,Gergoire
117,Dimmick,Damon
118,Distefano,Salvatoie
119,Dominguez,Mariano
120,Dondich,Taylor
121,Dorn,Christoph
122,Doughtie,Gavin
123,Drew,David
124,Dubois,Julien
125,Dubon,Stephane
126,Dudzik,Michael
127,Duivenbode,Lance
128,Dunklau,Ronan
129,Dvoryansky,Leonid
130,Dwelle,Tim
131,Eagan,Sean
132,Easterbrook,Zachary
133,Edmondson,Paul
134,Eernisse,Matthew
135,Elliot,Thomas
136,Ersoy,Ole
137,Evans,Aaron M.
138,Fabritz,Jason
139,Fabulich,Daniel Grijalva
140,Fair,Seth
141,Falduto,Ariel O.
142,Fan,Bertrand
143,Feldman,David A.
144,Fenwick,Nicholas
145,Ferrari,Alessandro
146,Ferrydiansyah,Reza
147,Field,Terry
148,Firsov,Olekasandr
149,Fitzgerald,Nick
150,Fitzgerald,Patrick
151,Follia,Pierpaolo
152,Forbes,Bryan
153,Forrester,Ron
154,Fortson,Frank
155,Foster,Sam
156,Foulds,Ian
157,Fox,Pat
158,France Telecom,
159,Franks,Carl
160,Franqueiro,Kenneth G.
161,Franz,Martin
162,Frohman,Lance
163,Fryer,Anthony
164,Fudge,David
165,Galipo,Don
166,Ganegedara,Hiran Shyanaka
167,Garcia,John
168,Gardner,Brett
169,Garfield,Justin
170,Garner,Sean
171,Garrioch,Robb N.
172,Ge,David
173,Gerber,Achim
174,Giammarchi,Andrea
175,Gielczynski,Miriam
176,Gilbert,Alain
177,Gill,Rawld
178,Girard,Wade
179,Glebovskiy,Alexander
180,Goessner,Stefan
181,Goldstein,Peter
182,Golebski,Marcin
183,Goncharov,Mykyta Sergiyovych
184,Gordon,Max
185,Gorman,Mitch
186,Gornick,Joseph Richard
187,Grainger,Brendan
188,Grandrath,Martin
189,Green,Amit
190,Greenberg,Jeffrey
191,Grencik,Jozef
192,Grimm,Steven
193,Groth,Ryan
194,Guillen,Revin
195,Gupta,Aman
196,Gwyn,Philip
197,Hakansson,Finn
198,Hale,Mark
199,Hamilton,Gabe
200,Hampton,Shawn
201,Hanbanchong,Aphichit
202,Hann,John M.
203,Harris,Jeffrey
204,Harrison,Matthew
205,Harter,Laurie
206,Hartmann,Michel
207,Hashim,Ahmed
208,Hayden,Jennifer
209,Hayes,Kyle
210,Hays,Jason Scott
211,Heeringson,Jaanus
212,Heil,Jerome
213,Heimbuch,Ryan C.
214,Henderson,Cal
215,Hennebrueder,Sebastian
216,Henricson,Mats
217,Herrmann,Doug
218,Hershberger,Matthew
219,Hiester,Christopher
220,Higgins,Peter
221,Hitt,Jason
222,Hixon,Alexander
223,Hjelte,Henrik
224,Hockey,Benjamin James
225,Hofbauer,Christian
226,Hoffman,Uwe
227,Holm,Torkel
228,Horoszowski,Matthew
229,Horowitz,Richard
230,Hu,Jian
231,Huang,Ming Zhe
232,Hudson,David
233,Humphreys,Martin
234,Hussenet,Claude
235,Illit,Marcel
236,Ionushonis,Victoria
237,Ippolito,Robert
238,Irish,Paul
239,Irrschik,Manuels
240,Irwin,Matthew
241,Isik,Hakan
242,Issakov,Antony
243,James,Stephen
244,Jekel,Peter
245,Jenkins,Adam
246,Jenkins,Scott
247,Johansson,Fredrik
248,Johansson,Niklas
249,Johansyah,Robertus Harmawon
250,Johns,Morris Peter
251,Johnson,Aaron
252,Johnson,Samuel B
253,Joldersma,Benjamin
254,Jones,Randall
255,Jones,Russell
256,Jonsson,Olle
257,Joshi,Neil
258,Julien,Mathevet
259,Jurkiewicz,Jared
260,Kaihol,Antti
261,Kang,Huynh
262,Kantor,Ilia
263,Karr,David
264,Katz,Omer
265,Keese,Bill
266,Kelly,Dirk
267,Kime,Matthew
268,Kimmel,Maximilian
269,Kingma,D.
270,Kings-Lynne,Christopher
271,Kisel,Siarhey
272,Klein,Stéphane
273,Klubnik,Justin
274,Knapp,Matthew
275,Koberg,Robert S.
276,Kokot,Peter
277,Kolba,Nicholas
278,Komarneni,Vamsikrishna
279,Koonce,Grayson
280,Krantz,Viktor
281,Kress Jorg,
282,Kriesing,Wolfram
283,Kuhnert," George ""Jesse"""
284,Kuka,Radovan
285,Kulesa,Chad
286,Kumar,Naresh
287,Kuzmik,Roman
288,Lain,Chih Chao
289,Lam,Daniel
290,Landolt,Dean
291,Laparo,Craig
292,Lapointe,Louis
293,Lazutkin,Eugene
294,Lear,Chris
295,Lee,Laurence A.
296,Leite,Kristian
297,Lendvai,Attila
298,Leonard,Jean-Rubin
299,Leonardi,David
300,Levinson,Todd
301,Leydier,Thierry
302,Li,Bin
303,Lightbody,Patrick
304,Linnenfelser,Marcel
305,Lipps,Jonathan
306,Liu,Heng
307,Lodewick,Thomas
308,Logemann,Marc
309,Lokanuvat,Sakchai
310,Lopes,Rui Godinho
311,Lopez,Gerald
312,Lorentsen,Bo
313,Lowery,Ben
314,Lucas,Brian
315,Lulek,Marcin
316,Lv,Yong
317,Lynch,James William
318,Lyon,Matt
319,Lytle,Seth
320,MacDonald,Jay John
321,Machi,Dustin
322,Maclennan,Caleb
323,Madineni,Pradeep
324,Maquire,Jordan
325,Malage,Osandi Chirantra Midreviy
326,Malpass,Ian Andrew
327,Manninen,Juho
328,Manteau,Pierre-Emmanuel
329,Marginian,David
330,Marginian,David Brian
331,Mark,David
332,Marko,Martin
333,Marsh,John T. Jr.
334,Martin,Benoit
335,Martin,Nicholas
336,Martinez,Jose Antonio
337,Mason,Seth
338,Mathias,Aaron
339,Matzner Bernd,
340,Mauger,Ryan
341,Mautone,Steven
342,Mayfield,Justin
343,McCallister,Brian
344,McCullough,Ryan
345,McGee,Daniel
346,McMaster,Doug
347,McNab,David
348,Medeiros,Miller H. Borges
349,Melo,Vinicius
350,Meschian,Rouben
351,Metyas,Remoun
352,Michelangeli,Enzo
353,Michopoulos,Haris
354,Migazzi,Pascal
355,Mikula,Tomas
356,Miles,Scott Joseph
357,Mills,Drew
358,Minarik,Andrej
359,Moeller,Jonathan
360,Mohan,Nirdesh
361,Monroe,Daniel
362,Montes,Luis
363,Morawski,Jason
364,Moreira,Jose
365,Motovilov,Max
366,Mott,Carla V.
367,Muhlestein,Dennis
368,Mullen,Patrick
369,Municio,Angel
370,Murphey,Rebecca
371,Murray,Gregory Lee
372,Nachbaur,Michael Alexander
373,Nairn,Rowan
374,Nakamura,Hioaki
375,Nasonov,Igor
376,Neden,Sean
377,Nelson,Stephen
378,Nepomnyashy,Marat
379,Neuberg,Bradley Keith
380,Newbill,Christopher
381,Newlau,Andrei
382,Newman,Joshua
383,Nguyen,TA
384,Nguyen,Thanh
385,Noheda,Jose
386,Nucera,Roberto
387,O Shea,Sean
388,Obermann,Gerhard
389,Ogilvie,Cyan Jon
390,Oliver,Vicky
391,Ondrek,Samuel
392,Onken,Nikolai
393,Oriol,Guillaume
394,Overton,James Alexander
395,Oyapero,Owalabi
396,Papayan,Vladislav
397,Papineau,Jeff
398,Parker,David
399,Pasquier,Eric
400,Pate,Benjamin
401,Patil,Ashish
402,Peart,Steve
403,Peierls,Tim
404,Penner,Robert
405,Penniman,Cary
406,Perdue,Crispin
407,Pereira,Rom
408,Perminov,Ilya
409,Petrov,Stamen
410,Phetra,Polawat
411,Pillai,Anand I.
412,Pliam,John
413,Plumlee,Scott
414,Popelo,Andrey
415,Popescu,Alexandru
416,Porcari,Giovanni
417,Prakaptsou,Artsem
418,Prevoteau,Eric
419,Prokopiev,Eugene
420,Pupius,Dan
421,Pu,Li
422,Rahalski,Vitali
423,Rakovsky,Adrian
424,Reed,Joshua Allen
425,Rees,David
426,Reicke,Craig
427,Reimann,Marcus
428,Remeika,Bob
429,Repta,Martin
430,Rhode,Devin
431,Rhodes,Aaron
432,Rice,Torrey
433,Riley,William
434,Rinehart,Randy
435,Rizzo,Nicola
436,Roberts,Baron
437,Roberts,Neil
438,Romero,Carlos
439,Rought,Edward T.
440,Rouse,Joseph
441,Roy,Dibyendu
442,Ruffles,Tim
443,Ruggia,Pablo
444,Ruggier,Mario
445,Ruoss,Stefan
446,Ruspini,Daniel
447,Russell,Matthew A.
448,Russell," Robert ""Alex"""
449,Safiev,Anuarbek
450,Sagolaev,Ivan
451,Saint-Just Philippe,
452,Sakar,Ahmet Taha
453,Salipo,Dan
454,Salminen,Jukka
455,Salt,Kevin
456,Sanders,Robert
457,Santalucia,Benjamin
458,Santovito,Filippo
459,Saremi,Jeff
460,Savage,Phillip
461,Sayfullin,Robert
462,Schall,Michael T.
463,Schiemann,Dylan
464,Schindler,William F.
465,Schmidt,Andreas
466,Scholz,Kyle
467,Schontzler,David
468,Schreiber,Maik
469,Schuerig,Michael
470,Schuster,Stefan
471,Seeger,Chad
472,Segal,Erel
473,Sekharan,Satishkumar
474,Semmens,Lance
475,Sexton,Alexander
476,Shah,Anjur
477,Shah,Maulin
478,Shamgin,Vladimir
479,Shaver,Robert
480,Shaw,Thomas R.
481,Shi,Hong
482,Shih,Kenneth
483,Shimizu,Fumiyuki
484,Shinnick,Thomas Loren
485,Shneyderman,Alex
486,Siemoneit,Oliver
487,Simpson,Matt
488,Simser,Daniel M.
489,Sitter,Sean
490,Skinner,Brian Douglas
491,Smeets,Bram
492,Smelkovs,Konrade
493,Smith,Bradford Carl
494,Smith,Donald Larry Jr.
495,Smith,Kevin A
496,Smith,Mark
497,Smith,Michael J.
498,Smith,Micheil
499,Smith,Stephen
500,Snopek,David
501,Snover,Colin
502,Sobol,Steve
503,Solomenchuk,Vladimir
504,Sorensen,Asael
505,Sorensen,Matt
506,Sotherland,Jamie
507,Souzis,Adam
508,Sowden,Paul
509,Speich,Simon
510,Squisky,
511,Stallworthy,Phillip
512,Stancapiano,Luca
513,Stanfill,Erin
514,Staravoitau,Aliaksei
515,Staskawicz,Liam
516,Staves,Aaron
517,Stearns,Geoff
518,Steenveld,Andre
519,Stefaniuk,Daniel
520,Steffensen,Gregory
521,Stepanoski,Mauro Alberto
522,Stern,David
523,Stojic,Ivan
524,Stott,Nathan Rains
525,Strimpel,Jason
526,Sulliman,Hani
527,Sumilang,Richard
528,Svensson,Peter
529,Sykes,Jon
530,Syndodinos,Dionysios
531,Szklanny,Les
532,Tan,Yi
533,Tanfous,Hassen Ben
534,Tangey,Greg
535,Tarassenko,Gleb
536,Taylor,Michael A.
537,Taylor,Jamie
538,Teer,Ellis
539,Tempelton,Sean
540,Teulings,Tijs
541,Tiedt,Karl
542,Tilley,Travis
543,Tipling,Bjorn
544,Todd,Aaron
545,Toone,Nathan
546,Trank,Aaron
547,Trenka,Ron
548,Trenka,Thomas
549,Trutwin,Josh
550,Tynan,Dylan
551,Tyson,Matt
552,Ukrop,Jakub
553,Ultis,Jonathan
554,Upton,Thomas
555,Uren,Richard
556,Vachou,Travis
557,Valdelievre,Florent
558,Valencia,Miguel Angel Perez
559,Van De Sande,Brett
560,Van Woerkom,Marc
561,Vandenberg,John Mark
562,VanderPlye,Nicholas
563,Vantoll III,Theodore
564,Veness,Chris
565,Venkatachalam,Vidyasagar
566,Vettervanta,
567,Vichas,Deno
568,Vincze,Gabor
569,Visic,Mario
570,Von Klipstein,Tobias
571,Wagener,Peter
572,Waite,Robert
573,Walker,Andrew
574,Walker,Joe
575,Wallez,Sylvain
576,Wang,Pei
577,Wei,Coach
578,Weinberger,Ferdinand
579,Weisberg,Adrian
580,Weiss,Robert
581,Welte,Robert John
582,Wenk,Norman
583,Werner,Punz
584,Whiteman,Todd
585,Wiersma,Erik
586,Wilcox,Mike
587,Wildman,Allison
588,Wilkins,Greg
589,Wilkins,Gregory John
590,Williams,David
591,Williams,Jason
592,Williams,Matthew Owens
593,Williamson,Tim
594,Wilson,Andrew
595,Wilson,Mike
596,Wood,Peter William Alfred
597,Wooten,Isaac T.
598,Wu,Thomas
599,Wubben,Mark
600,Wyss,Hannes F.
601,Xi,Kun
602,Xu,Xi
603,Yarimagan,Ilgin
604,Yeow,Cheah
605,Young,Matthew
606,Young,Rex
607,Yu,Leon
608,Zamir,Brett
609,Zammetti,Frank
610,Zastrow,Bettina
611,Zboron,Lukas
612,Zhang,Carrie
613,Zhang,Stephen
614,Zhang,Xiaoming
615,Zhen,Wang
616,Zhu,Wei
617,Zipkin,Joel
618,Zou,Ran
619,Zyp,Kris
1 id last first
2 1 Aaron Brandon
3 2 Acord Gary
4 3 Addesa Michael
5 4 Agliullin Mars
6 5 Aguilar Vincente Jimenez
7 6 Albornez Mulligan Jordi
8 7 Algesten Martin
9 8 Allen Robert
10 9 Alpert John Izaaic
11 10 Ambrosi Lucas
12 11 Anderson Christian O.
13 12 Anderson Matt
14 13 Andrews Jesse
15 14 Anuruddha Dilan
16 15 Armstrong Kevin
17 16 Arnold Brian
18 17 Arps Jens
19 18 Arthur Breton
20 19 Ascher David
21 20 Ash Christopher Baird
22 21 Atre Mac
23 22 Aylott Thomas
24 23 Bakaus Paul
25 24 Baker Christopher
26 25 Baker Jonathan
27 26 Baker Shawn
28 27 Balogh Jeff
29 28 Baltopoulos Ioannis
30 29 Bannister Michael
31 30 Barber Chris
32 31 Barnt Dave
33 32 Barrett Christopher
34 33 Barzilai Joseph
35 34 Basler Mark
36 35 Bates Simon
37 36 Baz David
38 37 Beard Jacob Evan
39 38 Belongi Mignon
40 39 Benton Morgan
41 40 Berkhout Just
42 41 Bernstein Daniel
43 42 Bersac Etienne
44 43 Bertet Pierre
45 44 Best Jason
46 45 Bevin Geert
47 46 Bhatia Naresh
48 47 Bing Dong
49 48 Bird Stefan
50 49 Bittinger Nathan
51 50 Bodzak Lee
52 51 Bolter David
53 52 Bond-Caran Jonathan
54 53 Bond Edward
55 54 Bordet Simone
56 55 Bottard Eric
57 56 Bouchon Paul
58 57 Boyer Marc
59 58 Braid Cameron
60 59 Braun Terry
61 60 Brettle Dean William
62 61 Brewster John
63 62 Brinnand John
64 63 Brown Dallas
65 64 Brown Eric W.
66 65 Buch Daniel James
67 66 Buffone Robert
68 67 Buffoni Fabio
69 68 Burke James Richard
70 69 Burke Thomas
71 70 Cahill Earl
72 71 Cakalic James
73 72 Cappiello John
74 73 Carpenter Lee
75 74 Carreira Jason
76 75 Carrera Daniel
77 76 Carstensen Brian
78 77 Cartier Dennis
79 78 Caudill Mark
80 79 Cavalier Brian
81 80 Cavatorta Andy
82 81 Chauduary Dipen
83 82 Chen Andrew
84 83 Childers James
85 84 Chimene Jeffrey
86 85 Chiumenti Andrea
87 86 Cho Youngho
88 87 Chong Petra
89 88 Christiansen Jason
90 89 Chung Stephen
91 90 Cisternino Antonio
92 91 Clark Patrick
93 92 Clayton David
94 93 Clemmons Eric
95 94 Cochran Samuel
96 95 Comer Kevin
97 96 Conaway Kevin
98 97 Conlon Martin John
99 98 Conroy Matt
100 99 Cooper James Cameron
101 100 Cope Wesley D.
102 101 Coquet Miguel
103 102 Costan Victor Marius
104 103 Crowder Thomas
105 104 Cuervo Jorge Martin
106 105 D'Souza Michelle
107 106 Dalouche Sami
108 107 Damour Remy
109 108 Dangoor Kevin
110 109 Davis Conor
111 110 Davis David Wayne
112 111 Davis Lee
113 112 Davor HRG
114 113 DeJarnett Philip
115 114 Della-Franca Sean
116 115 Demay Vincent
117 116 Dierendonck Gergoire
118 117 Dimmick Damon
119 118 Distefano Salvatoie
120 119 Dominguez Mariano
121 120 Dondich Taylor
122 121 Dorn Christoph
123 122 Doughtie Gavin
124 123 Drew David
125 124 Dubois Julien
126 125 Dubon Stephane
127 126 Dudzik Michael
128 127 Duivenbode Lance
129 128 Dunklau Ronan
130 129 Dvoryansky Leonid
131 130 Dwelle Tim
132 131 Eagan Sean
133 132 Easterbrook Zachary
134 133 Edmondson Paul
135 134 Eernisse Matthew
136 135 Elliot Thomas
137 136 Ersoy Ole
138 137 Evans Aaron M.
139 138 Fabritz Jason
140 139 Fabulich Daniel Grijalva
141 140 Fair Seth
142 141 Falduto Ariel O.
143 142 Fan Bertrand
144 143 Feldman David A.
145 144 Fenwick Nicholas
146 145 Ferrari Alessandro
147 146 Ferrydiansyah Reza
148 147 Field Terry
149 148 Firsov Olekasandr
150 149 Fitzgerald Nick
151 150 Fitzgerald Patrick
152 151 Follia Pierpaolo
153 152 Forbes Bryan
154 153 Forrester Ron
155 154 Fortson Frank
156 155 Foster Sam
157 156 Foulds Ian
158 157 Fox Pat
159 158 France Telecom
160 159 Franks Carl
161 160 Franqueiro Kenneth G.
162 161 Franz Martin
163 162 Frohman Lance
164 163 Fryer Anthony
165 164 Fudge David
166 165 Galipo Don
167 166 Ganegedara Hiran Shyanaka
168 167 Garcia John
169 168 Gardner Brett
170 169 Garfield Justin
171 170 Garner Sean
172 171 Garrioch Robb N.
173 172 Ge David
174 173 Gerber Achim
175 174 Giammarchi Andrea
176 175 Gielczynski Miriam
177 176 Gilbert Alain
178 177 Gill Rawld
179 178 Girard Wade
180 179 Glebovskiy Alexander
181 180 Goessner Stefan
182 181 Goldstein Peter
183 182 Golebski Marcin
184 183 Goncharov Mykyta Sergiyovych
185 184 Gordon Max
186 185 Gorman Mitch
187 186 Gornick Joseph Richard
188 187 Grainger Brendan
189 188 Grandrath Martin
190 189 Green Amit
191 190 Greenberg Jeffrey
192 191 Grencik Jozef
193 192 Grimm Steven
194 193 Groth Ryan
195 194 Guillen Revin
196 195 Gupta Aman
197 196 Gwyn Philip
198 197 Hakansson Finn
199 198 Hale Mark
200 199 Hamilton Gabe
201 200 Hampton Shawn
202 201 Hanbanchong Aphichit
203 202 Hann John M.
204 203 Harris Jeffrey
205 204 Harrison Matthew
206 205 Harter Laurie
207 206 Hartmann Michel
208 207 Hashim Ahmed
209 208 Hayden Jennifer
210 209 Hayes Kyle
211 210 Hays Jason Scott
212 211 Heeringson Jaanus
213 212 Heil Jerome
214 213 Heimbuch Ryan C.
215 214 Henderson Cal
216 215 Hennebrueder Sebastian
217 216 Henricson Mats
218 217 Herrmann Doug
219 218 Hershberger Matthew
220 219 Hiester Christopher
221 220 Higgins Peter
222 221 Hitt Jason
223 222 Hixon Alexander
224 223 Hjelte Henrik
225 224 Hockey Benjamin James
226 225 Hofbauer Christian
227 226 Hoffman Uwe
228 227 Holm Torkel
229 228 Horoszowski Matthew
230 229 Horowitz Richard
231 230 Hu Jian
232 231 Huang Ming Zhe
233 232 Hudson David
234 233 Humphreys Martin
235 234 Hussenet Claude
236 235 Illit Marcel
237 236 Ionushonis Victoria
238 237 Ippolito Robert
239 238 Irish Paul
240 239 Irrschik Manuels
241 240 Irwin Matthew
242 241 Isik Hakan
243 242 Issakov Antony
244 243 James Stephen
245 244 Jekel Peter
246 245 Jenkins Adam
247 246 Jenkins Scott
248 247 Johansson Fredrik
249 248 Johansson Niklas
250 249 Johansyah Robertus Harmawon
251 250 Johns Morris Peter
252 251 Johnson Aaron
253 252 Johnson Samuel B
254 253 Joldersma Benjamin
255 254 Jones Randall
256 255 Jones Russell
257 256 Jonsson Olle
258 257 Joshi Neil
259 258 Julien Mathevet
260 259 Jurkiewicz Jared
261 260 Kaihol Antti
262 261 Kang Huynh
263 262 Kantor Ilia
264 263 Karr David
265 264 Katz Omer
266 265 Keese Bill
267 266 Kelly Dirk
268 267 Kime Matthew
269 268 Kimmel Maximilian
270 269 Kingma D.
271 270 Kings-Lynne Christopher
272 271 Kisel Siarhey
273 272 Klein Stéphane
274 273 Klubnik Justin
275 274 Knapp Matthew
276 275 Koberg Robert S.
277 276 Kokot Peter
278 277 Kolba Nicholas
279 278 Komarneni Vamsikrishna
280 279 Koonce Grayson
281 280 Krantz Viktor
282 281 Kress Jorg
283 282 Kriesing Wolfram
284 283 Kuhnert George "Jesse"
285 284 Kuka Radovan
286 285 Kulesa Chad
287 286 Kumar Naresh
288 287 Kuzmik Roman
289 288 Lain Chih Chao
290 289 Lam Daniel
291 290 Landolt Dean
292 291 Laparo Craig
293 292 Lapointe Louis
294 293 Lazutkin Eugene
295 294 Lear Chris
296 295 Lee Laurence A.
297 296 Leite Kristian
298 297 Lendvai Attila
299 298 Leonard Jean-Rubin
300 299 Leonardi David
301 300 Levinson Todd
302 301 Leydier Thierry
303 302 Li Bin
304 303 Lightbody Patrick
305 304 Linnenfelser Marcel
306 305 Lipps Jonathan
307 306 Liu Heng
308 307 Lodewick Thomas
309 308 Logemann Marc
310 309 Lokanuvat Sakchai
311 310 Lopes Rui Godinho
312 311 Lopez Gerald
313 312 Lorentsen Bo
314 313 Lowery Ben
315 314 Lucas Brian
316 315 Lulek Marcin
317 316 Lv Yong
318 317 Lynch James William
319 318 Lyon Matt
320 319 Lytle Seth
321 320 MacDonald Jay John
322 321 Machi Dustin
323 322 Maclennan Caleb
324 323 Madineni Pradeep
325 324 Maquire Jordan
326 325 Malage Osandi Chirantra Midreviy
327 326 Malpass Ian Andrew
328 327 Manninen Juho
329 328 Manteau Pierre-Emmanuel
330 329 Marginian David
331 330 Marginian David Brian
332 331 Mark David
333 332 Marko Martin
334 333 Marsh John T. Jr.
335 334 Martin Benoit
336 335 Martin Nicholas
337 336 Martinez Jose Antonio
338 337 Mason Seth
339 338 Mathias Aaron
340 339 Matzner Bernd
341 340 Mauger Ryan
342 341 Mautone Steven
343 342 Mayfield Justin
344 343 McCallister Brian
345 344 McCullough Ryan
346 345 McGee Daniel
347 346 McMaster Doug
348 347 McNab David
349 348 Medeiros Miller H. Borges
350 349 Melo Vinicius
351 350 Meschian Rouben
352 351 Metyas Remoun
353 352 Michelangeli Enzo
354 353 Michopoulos Haris
355 354 Migazzi Pascal
356 355 Mikula Tomas
357 356 Miles Scott Joseph
358 357 Mills Drew
359 358 Minarik Andrej
360 359 Moeller Jonathan
361 360 Mohan Nirdesh
362 361 Monroe Daniel
363 362 Montes Luis
364 363 Morawski Jason
365 364 Moreira Jose
366 365 Motovilov Max
367 366 Mott Carla V.
368 367 Muhlestein Dennis
369 368 Mullen Patrick
370 369 Municio Angel
371 370 Murphey Rebecca
372 371 Murray Gregory Lee
373 372 Nachbaur Michael Alexander
374 373 Nairn Rowan
375 374 Nakamura Hioaki
376 375 Nasonov Igor
377 376 Neden Sean
378 377 Nelson Stephen
379 378 Nepomnyashy Marat
380 379 Neuberg Bradley Keith
381 380 Newbill Christopher
382 381 Newlau Andrei
383 382 Newman Joshua
384 383 Nguyen TA
385 384 Nguyen Thanh
386 385 Noheda Jose
387 386 Nucera Roberto
388 387 O Shea Sean
389 388 Obermann Gerhard
390 389 Ogilvie Cyan Jon
391 390 Oliver Vicky
392 391 Ondrek Samuel
393 392 Onken Nikolai
394 393 Oriol Guillaume
395 394 Overton James Alexander
396 395 Oyapero Owalabi
397 396 Papayan Vladislav
398 397 Papineau Jeff
399 398 Parker David
400 399 Pasquier Eric
401 400 Pate Benjamin
402 401 Patil Ashish
403 402 Peart Steve
404 403 Peierls Tim
405 404 Penner Robert
406 405 Penniman Cary
407 406 Perdue Crispin
408 407 Pereira Rom
409 408 Perminov Ilya
410 409 Petrov Stamen
411 410 Phetra Polawat
412 411 Pillai Anand I.
413 412 Pliam John
414 413 Plumlee Scott
415 414 Popelo Andrey
416 415 Popescu Alexandru
417 416 Porcari Giovanni
418 417 Prakaptsou Artsem
419 418 Prevoteau Eric
420 419 Prokopiev Eugene
421 420 Pupius Dan
422 421 Pu Li
423 422 Rahalski Vitali
424 423 Rakovsky Adrian
425 424 Reed Joshua Allen
426 425 Rees David
427 426 Reicke Craig
428 427 Reimann Marcus
429 428 Remeika Bob
430 429 Repta Martin
431 430 Rhode Devin
432 431 Rhodes Aaron
433 432 Rice Torrey
434 433 Riley William
435 434 Rinehart Randy
436 435 Rizzo Nicola
437 436 Roberts Baron
438 437 Roberts Neil
439 438 Romero Carlos
440 439 Rought Edward T.
441 440 Rouse Joseph
442 441 Roy Dibyendu
443 442 Ruffles Tim
444 443 Ruggia Pablo
445 444 Ruggier Mario
446 445 Ruoss Stefan
447 446 Ruspini Daniel
448 447 Russell Matthew A.
449 448 Russell Robert "Alex"
450 449 Safiev Anuarbek
451 450 Sagolaev Ivan
452 451 Saint-Just Philippe
453 452 Sakar Ahmet Taha
454 453 Salipo Dan
455 454 Salminen Jukka
456 455 Salt Kevin
457 456 Sanders Robert
458 457 Santalucia Benjamin
459 458 Santovito Filippo
460 459 Saremi Jeff
461 460 Savage Phillip
462 461 Sayfullin Robert
463 462 Schall Michael T.
464 463 Schiemann Dylan
465 464 Schindler William F.
466 465 Schmidt Andreas
467 466 Scholz Kyle
468 467 Schontzler David
469 468 Schreiber Maik
470 469 Schuerig Michael
471 470 Schuster Stefan
472 471 Seeger Chad
473 472 Segal Erel
474 473 Sekharan Satishkumar
475 474 Semmens Lance
476 475 Sexton Alexander
477 476 Shah Anjur
478 477 Shah Maulin
479 478 Shamgin Vladimir
480 479 Shaver Robert
481 480 Shaw Thomas R.
482 481 Shi Hong
483 482 Shih Kenneth
484 483 Shimizu Fumiyuki
485 484 Shinnick Thomas Loren
486 485 Shneyderman Alex
487 486 Siemoneit Oliver
488 487 Simpson Matt
489 488 Simser Daniel M.
490 489 Sitter Sean
491 490 Skinner Brian Douglas
492 491 Smeets Bram
493 492 Smelkovs Konrade
494 493 Smith Bradford Carl
495 494 Smith Donald Larry Jr.
496 495 Smith Kevin A
497 496 Smith Mark
498 497 Smith Michael J.
499 498 Smith Micheil
500 499 Smith Stephen
501 500 Snopek David
502 501 Snover Colin
503 502 Sobol Steve
504 503 Solomenchuk Vladimir
505 504 Sorensen Asael
506 505 Sorensen Matt
507 506 Sotherland Jamie
508 507 Souzis Adam
509 508 Sowden Paul
510 509 Speich Simon
511 510 Squisky
512 511 Stallworthy Phillip
513 512 Stancapiano Luca
514 513 Stanfill Erin
515 514 Staravoitau Aliaksei
516 515 Staskawicz Liam
517 516 Staves Aaron
518 517 Stearns Geoff
519 518 Steenveld Andre
520 519 Stefaniuk Daniel
521 520 Steffensen Gregory
522 521 Stepanoski Mauro Alberto
523 522 Stern David
524 523 Stojic Ivan
525 524 Stott Nathan Rains
526 525 Strimpel Jason
527 526 Sulliman Hani
528 527 Sumilang Richard
529 528 Svensson Peter
530 529 Sykes Jon
531 530 Syndodinos Dionysios
532 531 Szklanny Les
533 532 Tan Yi
534 533 Tanfous Hassen Ben
535 534 Tangey Greg
536 535 Tarassenko Gleb
537 536 Taylor Michael A.
538 537 Taylor Jamie
539 538 Teer Ellis
540 539 Tempelton Sean
541 540 Teulings Tijs
542 541 Tiedt Karl
543 542 Tilley Travis
544 543 Tipling Bjorn
545 544 Todd Aaron
546 545 Toone Nathan
547 546 Trank Aaron
548 547 Trenka Ron
549 548 Trenka Thomas
550 549 Trutwin Josh
551 550 Tynan Dylan
552 551 Tyson Matt
553 552 Ukrop Jakub
554 553 Ultis Jonathan
555 554 Upton Thomas
556 555 Uren Richard
557 556 Vachou Travis
558 557 Valdelievre Florent
559 558 Valencia Miguel Angel Perez
560 559 Van De Sande Brett
561 560 Van Woerkom Marc
562 561 Vandenberg John Mark
563 562 VanderPlye Nicholas
564 563 Vantoll III Theodore
565 564 Veness Chris
566 565 Venkatachalam Vidyasagar
567 566 Vettervanta
568 567 Vichas Deno
569 568 Vincze Gabor
570 569 Visic Mario
571 570 Von Klipstein Tobias
572 571 Wagener Peter
573 572 Waite Robert
574 573 Walker Andrew
575 574 Walker Joe
576 575 Wallez Sylvain
577 576 Wang Pei
578 577 Wei Coach
579 578 Weinberger Ferdinand
580 579 Weisberg Adrian
581 580 Weiss Robert
582 581 Welte Robert John
583 582 Wenk Norman
584 583 Werner Punz
585 584 Whiteman Todd
586 585 Wiersma Erik
587 586 Wilcox Mike
588 587 Wildman Allison
589 588 Wilkins Greg
590 589 Wilkins Gregory John
591 590 Williams David
592 591 Williams Jason
593 592 Williams Matthew Owens
594 593 Williamson Tim
595 594 Wilson Andrew
596 595 Wilson Mike
597 596 Wood Peter William Alfred
598 597 Wooten Isaac T.
599 598 Wu Thomas
600 599 Wubben Mark
601 600 Wyss Hannes F.
602 601 Xi Kun
603 602 Xu Xi
604 603 Yarimagan Ilgin
605 604 Yeow Cheah
606 605 Young Matthew
607 606 Young Rex
608 607 Yu Leon
609 608 Zamir Brett
610 609 Zammetti Frank
611 610 Zastrow Bettina
612 611 Zboron Lukas
613 612 Zhang Carrie
614 613 Zhang Stephen
615 614 Zhang Xiaoming
616 615 Zhen Wang
617 616 Zhu Wei
618 617 Zipkin Joel
619 618 Zou Ran
620 619 Zyp Kris
@@ -0,0 +1,6 @@
[
{ "id": 1, "name": "foo" },
{ "id": 2, "name": "bar" },
{ "id": 3, "name": "baz" },
{ "id": 4, "name": "bim" }
]
@@ -0,0 +1,4 @@
{ "id": "node1.1","name":"node1.1", "someProperty":"somePropertyA1", "children": [
{ "$ref": "node1.1.1", "name": "node1.1.1"},
{ "$ref": "node1.1.2", "name": "node1.1.2"}
]}
@@ -0,0 +1 @@
{ "id": "node1.2","name":"node1.2", "someProperty":"somePropertyA2"}
@@ -0,0 +1,4 @@
id,last, first,middle,born,died
3,Hawking,Stephen,William,1942-01-08,
1,Einstein,Albert,,1879-03-14,1955-04-18
2, Tesla, Nikola , ,1856-07-10, 1943-01-07
1 id last first middle born died
2 3 Hawking Stephen William 1942-01-08
3 1 Einstein Albert 1879-03-14 1955-04-18
4 2 Tesla Nikola 1856-07-10 1943-01-07
@@ -0,0 +1,47 @@
<?php
// This script implements a simple mock service for QueryRead tests.
header('Content-Type: application/json');
$total = $_GET['id'] ? 1 : 100;
// Allow override of checked parameters to allow testing non-default values
// on QueryRead properties.
$startParam = $_GET['startParam'] ? $_GET['startParam'] : 'start';
$countParam = $_GET['countParam'] ? $_GET['countParam'] : 'count';
$sortParam = $_GET['sortParam'] ? $_GET['sortParam'] : 'sort';
$itemsProperty = $_GET['itemsProperty'] ? $_GET['itemsProperty'] : 'items';
$totalProperty = $_GET['totalProperty'] ? $_GET['totalProperty'] : 'total';
// Set boundaries and sorting based on parameters to be set by store logic.
$start = $_GET[$startParam] ? $_GET[$startParam] : 0;
$end = $_GET[$countParam] ? min($start + $_GET[$countParam] - 1, $total - 1) : $total;
$desc = $_GET[$sortParam] ? substr($_GET[$sortParam], 0, 1) == '-' : false;
if ($_GET[$sortParam]) {
$desc = substr($_GET[$sortParam], 0, 1) == '-';
}
// Allow switching between response of outer object with items/total, and
// direct array w/ Content-Range header representing total.
if ($_GET['useContentRange']) {
header('Content-Range: items ' . $start . '-' . $end . '/' . $total);
} else {
echo '{"' . $totalProperty . '":' . $total . ',"' . $itemsProperty . '":';
}
echo '[';
if ($_GET['id']) {
echo '{"id":' . $_GET['id'] . ',"name":"Item ' . $_GET['id'] . '"}';
} else {
// Iterate forwards or backwards depending on whether sorted descending.
for ($i = ($desc ? $end : $start); $desc ? $i >= $start : $i <= $end; $desc ? $i-- : $i++) {
if($i != ($desc ? $end : $start)){
echo ',';
}
echo '{"id":' . ($i + 1) . ',"name":"Item ' . ($i + 1) . '"}';
}
}
echo ']';
if (!$_GET['useContentRange']) {
echo '}'; // End the outer object.
}
?>
@@ -0,0 +1,12 @@
"id", "name", "quote"
"1","Garfield","Every time I think I've hit bottom, somebody throws me a shovel"
"2","Edgar Allan Poe","Then this ebony bird beguiling my sad fancy into smiling,
By the grave and stern decorum of the countenance it wore,
""Though thy crest be shorn and shaven, thou,"" I said, ""art sure no craven,
Ghastly grim and ancient Raven wandering from the Nightly shore —
Tell me what thy lordly name is on the Night's Plutonian shore!""
Quoth the Raven ""Nevermore."""
"3", "Steve" , """"""
"4", "Space Sphere", " S, P, ...ace! "
Can't render this file because it contains an unexpected character in line 2 and column 21.
@@ -0,0 +1,10 @@
define({
identifier: 'id',
items: [
{id: 1, name: 'one', prime: false, mappedTo: 'E'},
{id: 2, name: 'two', even: true, prime: true, mappedTo: 'D'},
{id: 3, name: 'three', prime: true, mappedTo: 'C'},
{id: 4, name: 'four', even: true, prime: false, mappedTo: null},
{id: 5, name: 'five', prime: true, mappedTo: 'A'}
]
});
@@ -0,0 +1,10 @@
[
{ "id": "node1", "name":"node1", "someProperty":"somePropertyA", "children":[
{ "$ref": "node1.1", "name": "node1.1", "children": true},
{ "$ref": "node1.2", "name": "node1.2"}
]},
{ "id": "node2", "name":"node2", "someProperty":"somePropertyB"},
{ "id": "node3", "name":"node3", "someProperty":"somePropertyC"},
{ "id": "node4", "name":"node4", "someProperty":"somePropertyA"},
{ "id": "node5", "name":"node5", "someProperty":"somePropertyB"}
]
@@ -0,0 +1,50 @@
define([
'intern!object',
'intern/chai!assert',
'dojo/_base/declare',
'dstore/extensions/RqlQuery',
'dstore/Memory'
], function (registerSuite, assert, declare, RqlQuery, Memory) {
var TestModel = declare(null, {
describe: function () {
return this.name + ' is ' + (this.prime ? '' : 'not ') + 'a prime';
}
});
var TestStore = declare([ Memory, RqlQuery ]);
var rqlMemory = new TestStore({
data: [
{id: 1, name: 'one', prime: false, mappedTo: 'E'},
{id: 2, name: 'two', prime: true, mappedTo: 'D', even: true},
{id: 3, name: 'three', prime: true, mappedTo: 'C'},
{id: 4, name: 'four', prime: false, mappedTo: null, even: true},
{id: 5, name: 'five', prime: true, mappedTo: 'A'}
],
Model: TestModel
});
registerSuite({
name: 'dstore RqlMemory',
'get': function () {
assert.strictEqual(rqlMemory.getSync(1).name, 'one');
assert.strictEqual(rqlMemory.getSync(4).name, 'four');
assert.strictEqual(rqlMemory.getSync(1).describe(), 'one is not a prime');
},
'filter': function () {
assert.strictEqual(rqlMemory.filter('prime=true').fetchSync().length, 3);
assert.strictEqual(rqlMemory.filter('prime=true').fetchRangeSync({start: 1, end: 2}).totalLength, 3);
assert.strictEqual(rqlMemory.filter('prime=true').fetchRangeSync({start: 1, end: 2}).length, 1);
assert.strictEqual(rqlMemory.filter({prime: true}).fetchSync().length, 3);
assert.strictEqual(rqlMemory.filter('prime=true&even!=true').fetchSync().length, 2);
assert.strictEqual(rqlMemory.filter('prime=true&id>3').fetchSync().length, 1);
assert.strictEqual(rqlMemory.filter('(prime=true|id>3)').fetchSync().length, 4);
assert.strictEqual(rqlMemory.filter('(prime=true|id>3)').fetchRangeSync({start: 1, end: 3}).length, 2);
}
});
});
@@ -0,0 +1,80 @@
// Learn more about configuring this file at <https://github.com/theintern/intern/wiki/Configuring-Intern>.
// These default settings work OK for most people. The options that *must* be changed below are the
// packages, suites, excludeInstrumentation, and (if you want functional tests) functionalSuites.
define({
// The port on which the instrumenting proxy will listen
proxyPort: 9000,
// A fully qualified URL to the Intern proxy
proxyUrl: 'http://localhost:9000/',
// Default desired capabilities for all environments. Individual capabilities can be overridden by any of the
// specified browser environments in the `environments` array below as well. See
// https://code.google.com/p/selenium/wiki/DesiredCapabilities for standard Selenium capabilities and
// https://saucelabs.com/docs/additional-config#desired-capabilities for Sauce Labs capabilities.
// Note that the `build` capability will be filled in with the current commit ID from the Travis CI environment
// automatically
capabilities: {
'selenium-version': '2.33.0'
},
// Browsers to run integration testing against. Note that version numbers must be strings if used with Sauce
// OnDemand. Options that will be permutated are browserName, version, platform, and platformVersion; any other
// capabilities options specified for an environment will be copied as-is
environments: [
{ browserName: 'internet explorer', version: '10', platform: 'Windows 8' },
{ browserName: 'internet explorer', version: '9', platform: 'Windows 7' },
{ browserName: 'firefox', version: '22', platform: [ 'Linux', 'Windows 7' ] },
{ browserName: 'firefox', version: '21', platform: 'Mac 10.6' },
{ browserName: 'chrome', platform: [ 'Linux', 'Mac 10.8', 'Windows 7' ] },
{ browserName: 'safari', version: '6', platform: 'Mac 10.8' }
],
// Maximum number of simultaneous integration tests that should be executed on the remote WebDriver service
maxConcurrency: 3,
// Whether or not to start Sauce Connect before running tests
useSauceConnect: true,
// Connection information for the remote WebDriver service. If using Sauce Labs, keep your username and password
// in the SAUCE_USERNAME and SAUCE_ACCESS_KEY environment variables unless you are sure you will NEVER be
// publishing this configuration file somewhere
webdriver: {
host: 'localhost',
port: 4444
},
// Configuration options for the module loader; any AMD configuration options supported by the Dojo loader can be
// used here
loader: {
baseUrl: typeof process === 'undefined' ?
// if we are using the full path to dstore, we assume we are running
// in a sibling path configuration
location.search.indexOf('config=dstore') > -1 ? '../..' : '..' :
'./node_modules',
// Packages that should be registered with the loader in each testing environment
requestProvider: 'dojo/request/registry',
packages: [
{ name: 'dojo', location: 'dojo' },
{
name: 'dstore',
location: typeof process === 'undefined' ?
location.search.indexOf('config=dstore') > -1 ? 'dstore' : '..' :
'..'
},
{ name: 'rql', location: 'rql' },
{ name: 'dmodel', location: 'dmodel' }
]
},
// Non-functional test suite(s) to run in each browser
suites: [ 'dstore/tests/all' ],
// Functional test suite(s) to run in each browser once non-functional tests are completed
functionalSuites: [],
// A regular expression matching URLs to files that should not be included in code coverage analysis
excludeInstrumentation: /^dojox?|^tests?\//
});
@@ -0,0 +1,224 @@
define([
'dojo/_base/declare',
'dojo/aspect',
'intern!object',
'intern/chai!assert',
'dstore/Memory',
'dstore/Trackable',
'../sorting',
'dstore/legacy/DstoreAdapter'
], function (declare, aspect, registerSuite, assert, Memory, Trackable, sorting, DstoreAdapter) {
var store;
registerSuite({
name: 'legacy dstore adapter - Memory',
beforeEach: function () {
store = new DstoreAdapter(new Memory({
data: [
{ id: 1, name: 'one', prime: false, mappedTo: 'E' },
{ id: 2, name: 'two', even: true, prime: true, mappedTo: 'D' },
{ id: 3, name: 'three', prime: true, mappedTo: 'C' },
{ id: 4, name: 'four', even: true, prime: false, mappedTo: null },
{ id: 5, name: 'five', prime: true, mappedTo: 'A' }
]
}));
},
'get': function () {
assert.strictEqual(store.get(1).name, 'one');
assert.strictEqual(store.get(4).name, 'four');
assert.strictEqual(store.get(3).id, 3);
assert.strictEqual(store.getIdentity(store.get(2)), 2);
assert.isTrue(store.get(5).prime);
},
'query': function () {
assert.strictEqual(store.query({prime: true}).length, 3);
assert.strictEqual(store.query({even: true})[1].name, 'four');
},
'query on Trackable with observe': function () {
store = new DstoreAdapter(new (declare([Memory, Trackable]))({
data: [
{ id: 2, name: 'two', prime: true }
]
}));
var observedEvents = [];
var notifyCalls = [];
aspect.after(store, 'notify', function (object, existingId) {
notifyCalls.push([object && object.name, existingId]);
}, true);
store.query({prime: true}, {sort: [{attribute: 'name'}]}).observe(function (object, previousIndex, index) {
observedEvents.push([object.name, previousIndex, index]);
}, true);
store.put({id: 5, name: 'five', prime: true});
store.put({id: 6, name: 'six', prime: true});
store.put({id: 5, name: 'z-five', prime: true});
store.put({id: 5, name: 'z-five', prime: false});
store.remove(2);
assert.deepEqual(observedEvents, [
['five', -1, 0],
['six', -1, 1],
['z-five', 0, 2],
['z-five', 2, -1],
['two', 1, -1]]);
assert.deepEqual(notifyCalls, [
['five', undefined],
['six', undefined],
['z-five', 5],
['z-five', 5],
[undefined, 2]]);
},
'query on plain store with observe': function () {
var observedEvents = [];
var notifyCalls = [];
aspect.after(store, 'notify', function (object, existingId) {
notifyCalls.push([object && object.name, existingId]);
}, true);
store.query({prime: true}).observe(function (object, previousIndex, index) {
observedEvents.push([object.name, previousIndex, index]);
}, true);
store.put({id: 11, name: 'eleven', prime: true});
store.remove(5);
assert.deepEqual(observedEvents, [
['eleven', -1, undefined],
['five', undefined, -1]]);
assert.deepEqual(notifyCalls, [
['eleven', undefined],
[undefined, 5]]);
},
'query with string': function () {
assert.strictEqual(store.query({name: 'two'}).length, 1);
assert.strictEqual(store.query({name: 'two'})[0].name, 'two');
},
'query with reg exp': function () {
assert.strictEqual(store.query({name: /^t/}).length, 2);
assert.strictEqual(store.query({name: /^t/})[1].name, 'three');
assert.strictEqual(store.query({name: /^o/}).length, 1);
assert.strictEqual(store.query({name: /o/}).length, 3);
},
'query with test function': function () {
assert.strictEqual(store.query({id: {test: function (id) {
return id < 4;
}}}).length, 3);
assert.strictEqual(store.query({even: {test: function (even, object) {
return even && object.id > 2;
}}}).length, 1);
},
'query with sort': function () {
assert.strictEqual(store.query({prime: true}, {sort: [
{attribute: 'name'}
]}).length, 3);
assert.strictEqual(store.query({even: true}, {sort: [
{attribute: 'name'}
]})[1].name, 'two');
assert.strictEqual(store.query({even: true}, {sort: function (a, b) {
return a.name < b.name ? -1 : 1;
}})[1].name, 'two');
assert.strictEqual(store.query(null, {sort: [
{attribute: 'mappedTo'}
]})[4].name, 'four');
},
'query with paging': function () {
assert.strictEqual(store.query({prime: true}, {start: 1, count: 1}).length, 1);
assert.strictEqual(store.query({even: true}, {start: 1, count: 1})[0].name, 'four');
},
'query result includes `total` property': function () {
var results = store.query();
assert.property(results, 'total');
assert.strictEqual(results.total, results.length);
},
'put update': function () {
var four = store.get(4);
four.square = true;
store.put(four);
four = store.get(4);
assert.isTrue(four.square);
},
'put new': function () {
store.put({
id: 6,
perfect: true
});
assert.isTrue(store.get(6).perfect);
},
'add duplicate': function () {
var threw;
try {
store.add({
id: 5,
perfect: true
});
} catch (e) {
threw = true;
}
assert.isTrue(threw);
},
'add new': function () {
store.add({
id: 7,
prime: true
});
assert.isTrue(store.get(7).prime);
},
'remove': function () {
assert.isTrue(store.remove(3));
assert.strictEqual(store.get(3), undefined);
},
'remove missing': function () {
assert.isFalse(!!store.remove(77));
// make sure nothing changed
assert.strictEqual(store.get(1).id, 1, 'Everything is undisturbed.');
},
'query after changes': function () {
store.add({ id: 7, prime: true });
assert.strictEqual(store.query({prime: true}).length, 4);
assert.strictEqual(store.query({perfect: true}).length, 0);
store.remove(3);
store.put({ id: 6, perfect: true });
assert.strictEqual(store.query({prime: true}).length, 3);
assert.strictEqual(store.query({perfect: true}).length, 1);
},
'ItemFileReadStore style data': function () {
var dstoreObj = new Memory({
data: {
items: [
{name: 'one', prime: false},
{name: 'two', even: true, prime: true},
{name: 'three', prime: true}
],
identifier: 'name'
}
});
var anotherStore = new DstoreAdapter(dstoreObj);
assert.strictEqual(anotherStore.get('one').name, 'one');
assert.strictEqual(anotherStore.query({name: 'one'})[0].name, 'one');
},
'add new id assignment': function () {
var object = {
random: true
};
store.add(object);
assert.isTrue(!!object.id);
},
nestedSuite: sorting('legacy dstore adapter sorting - dstore/Memory', function before(data) {
return function before() {
var dstoreObj = new Memory({data: data});
store = new DstoreAdapter(dstoreObj);
};
}, function sort() {
var sort = [];
for (var i = 0; i < arguments.length; i++) {
sort[i] = {
attribute: arguments[i].property,
descending: arguments[i].descending
};
}
return store.query({}, {
sort: sort
});
})
});
});
@@ -0,0 +1,126 @@
define([
'intern!object',
'intern/chai!assert',
'dstore/Rest',
'dojo/request/registry',
'dojo/_base/lang',
'dojo/aspect',
'../mockRequest',
'dstore/legacy/DstoreAdapter',
'dojo/text!../data/node1.1',
'dojo/text!../data/treeTestRoot'
], function (
registerSuite,
assert,
Rest,
request,
lang,
aspect,
mockRequest,
DstoreAdapter,
nodeData_1_1,
treeTestRootData
) {
var Model = function () {};
function runHeaderTest(method, args) {
return store[method].apply(store, args).then(function () {
mockRequest.assertRequestHeaders(requestHeaders);
mockRequest.assertRequestHeaders(globalHeaders);
});
}
var globalHeaders = {
'test-global-header-a': 'true',
'test-global-header-b': 'yes'
};
var requestHeaders = {
'test-local-header-a': 'true',
'test-local-header-b': 'yes',
'test-override': 'overridden'
};
var store;
registerSuite({
name: 'DstoreAdapter-Rest',
before: function () {
registryHandle = request.register(/.*mockRequest.*/, mockRequest);
},
after: function () {
registryHandle.remove();
},
beforeEach: function(){
store = new DstoreAdapter(new Rest({
target: '/mockRequest/',
headers: globalHeaders,
Model: Model
}));
},
'get': function () {
mockRequest.setResponseText(nodeData_1_1);
return store.get('data/node1.1').then(function (object) {
assert.strictEqual(object.name, 'node1.1');
});
},
'headers get 1': function () {
return runHeaderTest('get', [ 'mockRequest/1', requestHeaders ]);
},
'headers get 2': function () {
return runHeaderTest('get', [ 'mockRequest/2', { headers: requestHeaders } ]);
},
'headers remove': function () {
return runHeaderTest('remove', [ 'mockRequest/3', { headers: requestHeaders } ]);
},
'headers put': function () {
return runHeaderTest('put', [
{},
{
id: 'mockRequest/4',
headers: requestHeaders
}
]);
},
'headers add': function () {
return runHeaderTest('add', [
{},
{
id: 'mockRequest/5',
headers: requestHeaders
}
]);
},
'put object without ID': function () {
var objectWithoutId = { name: 'one' };
return store.put(objectWithoutId).then(function () {
mockRequest.assertHttpMethod('POST');
});
},
'query iterative': function () {
mockRequest.setResponseText(treeTestRootData);
var i = 0;
return store.query('data/treeTestRoot').forEach(function (object) {
i++;
assert.strictEqual(object.name, 'node' + i);
});
},
'query iterative with options': function () {
mockRequest.setResponseText(treeTestRootData);
var i = 0;
return store.query('data/treeTestRoot', { start: 0 }).map(function (object) {
i++;
assert.strictEqual(object.name, 'node' + i);
return object;
});
}
});
});
@@ -0,0 +1,148 @@
define([
'dojo/_base/declare',
'dojo/Deferred',
'intern!object',
'intern/chai!assert',
'dojo/store/Memory',
'dojo/store/util/QueryResults',
'../sorting',
'dstore/legacy/StoreAdapter'
], function (declare, Deferred, registerSuite, assert, Memory, QueryResults, sorting, StoreAdapter) {
var Model = function () {};
function makeAsync(query){
return function(){
var args = arguments;
var inherited = this.getInherited(args);
var def = new Deferred(function(){
clearTimeout(timer);
});
var self = this;
var timer = setTimeout(function(){
try {
def.resolve(inherited.apply(self, args));
} catch (e) {
def.reject(e);
}
}, 2);
return query ? new QueryResults(def.promise) : def.promise;
};
}
var AsyncMemory = declare(Memory, {
query: makeAsync(true),
get: makeAsync(),
put: makeAsync(),
remove: makeAsync()
});
var store;
registerSuite({
name: 'legacy dstore adapter - AsyncMemory',
beforeEach: function () {
store = new StoreAdapter({
objectStore: new AsyncMemory({
data: [
{id: 1, name: 'one', prime: false, mappedTo: 'E'},
{id: 2, name: 'two', even: true, prime: true, mappedTo: 'D'},
{id: 3, name: 'three', prime: true, mappedTo: 'C'},
{id: 4, name: 'four', even: true, prime: false, mappedTo: null},
{id: 5, name: 'five', prime: true, mappedTo: 'A'}
]
}),
Model: Model
});
store.Model.prototype.describe = function () {
return this.name + ' is ' + (this.prime ? '' : 'not ') + 'a prime';
};
},
'get': function () {
return store.get(1).then(function (object) {
assert.strictEqual(object.name, 'one');
assert.strictEqual(store.getIdentity(object), 1);
assert.strictEqual(object.describe(), 'one is not a prime');
});
},
'filter': function () {
return store.filter({prime: true}).fetch().then(function (results) {
assert.strictEqual(results.length, 3);
});
},
'filter with string': function () {
return store.filter({name: 'two'}).fetch().then(function (results) {
assert.strictEqual(results.length, 1);
assert.strictEqual(results[0].name, 'two');
});
},
'filter with paging': function () {
return store.filter({prime: true}).fetchRange({start: 1, end: 2}).then(function (results) {
assert.strictEqual(results.length, 1);
assert.strictEqual(results[0].name, 'three');
});
},
'put update': function () {
return store.get(4).then(function (four) {
four.square = true;
return store.put(four).then(function () {
return store.get(4).then(function (four) {
assert.isTrue(four.square);
});
});
});
},
'put new': function () {
return store.put({
id: 6,
perfect: true
}).then(function () {
return store.get(6).then(function (six) {
assert.isTrue(six.perfect);
});
});
},
'add duplicate': function () {
return store.add({
id: 5,
perfect: true
}).then(function () {
assert.fail('should throw');
}, function () {
// should fail
});
},
'add new': function () {
return store.add({
id: 7,
prime: true
}).then(function () {
return store.get(7).then(function (seven) {
assert.isTrue(seven.prime);
});
});
},
'remove': function () {
return store.remove(3).then(function (result) {
assert.isTrue(result);
return store.get(3);
}).then(function (three) {
assert.strictEqual(three, undefined);
});
},
'remove missing': function () {
return store.remove(77).then(function (result) {
assert.isFalse(!!result);
});
}
});
});
@@ -0,0 +1,148 @@
define([
'dojo/_base/declare',
'dojo/Deferred',
'dojo/data/ItemFileWriteStore',
'dojo/store/DataStore',
'intern!object',
'intern/chai!assert',
'dojo/store/Memory',
'dojo/_base/lang',
'dojo/when',
'dstore/legacy/StoreAdapter',
'../data/testData'
], function (declare, Deferred, ItemFileWriteStore, DataStore, registerSuite, assert, Memory, lang, when, StoreAdapter, testData) {
var Model = function () {};
function getResultsArray(store) {
var results = [];
store.forEach(function (data) {
results.push(data);
});
return results;
}
var store;
registerSuite(lang.mixin({
name: 'legacy dstore adapter - dojo data',
beforeEach: function () {
var dataStore = new DataStore({
store: new ItemFileWriteStore({
data: lang.clone(testData)
})
});
store = new StoreAdapter({
objectStore: dataStore,
Model: Model
});
store.Model.prototype.describe = function () {
return this.name + ' is ' + (this.prime ? '' : 'not ') + 'a prime';
};
},
'get': function () {
assert.strictEqual(store.get(1).name, 'one');
assert.strictEqual(store.get(4).name, 'four');
assert.isTrue(store.get(5).prime);
assert.strictEqual(store.getIdentity(store.get(1)), 1);
},
'Model': function () {
assert.strictEqual(store.get(1).describe(), 'one is not a prime');
assert.strictEqual(store.get(3).describe(), 'three is a prime');
var results = getResultsArray(store.filter({even: true}));
assert.strictEqual(results.length, 2, 'The length is 2');
assert.strictEqual(results[1].describe(), 'four is not a prime');
},
'filter': function () {
assert.strictEqual(getResultsArray(store.filter({prime: true})).length, 3);
assert.strictEqual(getResultsArray(store.filter({even: true}))[1].name, 'four');
},
'filter with string': function () {
assert.strictEqual(getResultsArray(store.filter({name: 'two'})).length, 1);
assert.strictEqual(getResultsArray(store.filter({name: 'two'}))[0].name, 'two');
},
'filter with regexp': function () {
assert.strictEqual(getResultsArray(store.filter({name: /^t/})).length, 2);
assert.strictEqual(getResultsArray(store.filter({name: /^t/}))[1].name, 'three');
assert.strictEqual(getResultsArray(store.filter({name: /^o/})).length, 1);
assert.strictEqual(getResultsArray(store.filter({name: /o/})).length, 3);
},
'filter with paging': function () {
when(store.filter({prime: true}).fetchRange({start: 1, end: 2}), function (results) {
assert.strictEqual(results.length, 1);
});
when(store.filter({even: true}).fetchRange({start: 1, end: 2}), function (results) {
assert.strictEqual(results[0].name, 'four');
});
},
'put new': function () {
store.put({
id: 6,
perfect: true
});
assert.isTrue(store.get(6).perfect);
}
// if we have the update DataStore (as of Dojo 1.10), we will use these tests as well
}, DataStore.prototype.add &&
{
'put update': function () {
var four = store.get(4);
four.square = true;
store.put(four);
four = store.get(4);
assert.isTrue(four.square);
},
'add duplicate': function () {
store.add({
id: 5,
perfect: true
}).then(function () {
assert.fail('add duplicate not rejected');
}, function () {
console.log('add duplicate failed as expected');
});
},
'add new': function () {
store.add({
id: 7,
prime: true
});
assert.isTrue(store.get(7).prime);
},
'remove': function () {
return store.remove(3).then(function (result) {
assert.isTrue(result);
assert.strictEqual(store.get(3), undefined);
});
},
'remove missing': function () {
return store.remove(30).then(function (result) {
assert.isFalse(result);
// make sure nothing changed
assert.strictEqual(store.get(1).id, 1);
});
},
'filter after changes': function () {
store.add({ id: 7, prime: true });
assert.strictEqual(getResultsArray(store.filter({prime: true})).length, 4);
assert.strictEqual(getResultsArray(store.filter({perfect: true})).length, 0);
store.remove(3);
store.put({ id: 6, perfect: true });
assert.strictEqual(getResultsArray(store.filter({prime: true})).length, 3);
assert.strictEqual(getResultsArray(store.filter({perfect: true})).length, 1);
}
}));
});
@@ -0,0 +1,111 @@
define([
'require',
'intern!object',
'intern/chai!assert',
'dojo/_base/declare',
'dojo/json',
'dojo/_base/lang',
'dojo/when',
'dojo/store/JsonRest',
'dstore/legacy/StoreAdapter'
], function (require, registerSuite, assert, declare, JSON, lang, when, JsonRest, StoreAdapter) {
var Model = function () {};
var legacyStore = new JsonRest({
target: require.toUrl('dstore/tests/x.y').match(/(.+)x\.y$/)[1],
remove: function () {
var result = this.inherited(arguments);
return result.then(function (response) {
return response && JSON.parse(response);
});
}
});
var adaptedStore = new StoreAdapter({
objectStore: legacyStore,
Model: Model
});
adaptedStore.Model.prototype.describe = function () {
return 'name is ' + this.name;
};
var store = new StoreAdapter({
objectStore: new JsonRest({
target: require.toUrl('dstore/tests/x.y').match(/(.+)x\.y$/)[1],
remove: function () {
var result = this.inherited(arguments);
return result.then(function (response) {
return response && JSON.parse(response);
});
}
}),
Model: Model
});
store.Model.prototype.describe = function () {
return 'name is ' + this.name;
};
registerSuite({
name: 'legacy dstore adapter - JsonRest',
'get': function () {
var d = this.async();
store.get('data/node1.1').then(d.callback(function (object) {
assert.strictEqual(object.name, 'node1.1');
assert.strictEqual(object.describe(), 'name is node1.1');
assert.strictEqual(object.someProperty, 'somePropertyA1');
assert.strictEqual(store.getIdentity(object), 'node1.1');
}));
},
'filter': function () {
return when(store.filter('data/treeTestRoot').fetch()).then(function (results) {
var object = results[0];
assert.strictEqual(object.name, 'node1');
assert.strictEqual(object.describe(), 'name is node1');
assert.strictEqual(object.someProperty, 'somePropertyA');
});
},
'filter iterative': function () {
var d = this.async();
var i = 0;
return store.filter('data/treeTestRoot').forEach(d.rejectOnError(function (object) {
i++;
assert.strictEqual(object.name, 'node' + i);
}));
}
});
registerSuite({
name: 'legacy dstore adapter - JsonRest - adapted obj',
'get': function () {
var d = this.async();
return adaptedStore.get('data/node1.1').then(d.callback(function (object) {
assert.strictEqual(object.name, 'node1.1');
assert.strictEqual(object.describe(), 'name is node1.1');
assert.strictEqual(object.someProperty, 'somePropertyA1');
assert.strictEqual(adaptedStore.getIdentity(object), 'node1.1');
}));
},
'filter': function () {
return when(adaptedStore.filter('data/treeTestRoot').fetch()).then(function (results) {
var object = results[0];
assert.strictEqual(object.name, 'node1');
assert.strictEqual(object.describe(), 'name is node1');
assert.strictEqual(object.someProperty, 'somePropertyA');
});
},
'filter iterative': function () {
var d = this.async();
var i = 0;
return adaptedStore.filter('data/treeTestRoot').forEach(d.rejectOnError(function (object) {
i++;
assert.strictEqual(object.name, 'node' + i);
}));
}
});
});
@@ -0,0 +1,198 @@
define([
'dojo/_base/declare',
'intern!object',
'intern/chai!assert',
'dojo/store/Memory',
'../sorting',
'dstore/legacy/StoreAdapter'
], function (declare, registerSuite, assert, Memory, sorting, StoreAdapter) {
var Model = function () {};
function getResultsArray(store) {
var results = [];
store.forEach(function (data) {
results.push(data);
});
return results;
}
var store;
registerSuite({
name: 'legacy dstore adapter - Memory',
beforeEach: function () {
store = new StoreAdapter({
objectStore: new Memory({
data: [
{id: 1, name: 'one', prime: false, mappedTo: 'E'},
{id: 2, name: 'two', even: true, prime: true, mappedTo: 'D'},
{id: 3, name: 'three', prime: true, mappedTo: 'C'},
{id: 4, name: 'four', even: true, prime: false, mappedTo: null},
{id: 5, name: 'five', prime: true, mappedTo: 'A'}
]
}),
Model: Model
});
store.Model.prototype.describe = function () {
return this.name + ' is ' + (this.prime ? '' : 'not ') + 'a prime';
};
},
'get': function () {
assert.strictEqual(store.get(1).name, 'one');
assert.strictEqual(store.get(4).name, 'four');
assert.isTrue(store.get(5).prime);
assert.strictEqual(store.getIdentity(store.get(1)), 1);
},
'Model': function () {
assert.strictEqual(store.get(1).describe(), 'one is not a prime');
assert.strictEqual(store.get(3).describe(), 'three is a prime');
var results = getResultsArray(store.filter({even: true}));
assert.strictEqual(results.length, 2, 'The length is 2');
assert.strictEqual(results[1].describe(), 'four is not a prime');
},
fetch: function () {
var totalLength = store.fetch().totalLength;
assert.isDefined(totalLength, 'totalLength should be defined on fetch results');
assert.strictEqual(typeof totalLength.then, 'function',
'totalLength should be a promise');
},
'filter': function () {
assert.strictEqual(getResultsArray(store.filter({prime: true})).length, 3);
assert.strictEqual(getResultsArray(store.filter({even: true}))[1].name, 'four');
},
'filter with string': function () {
assert.strictEqual(getResultsArray(store.filter({name: 'two'})).length, 1);
assert.strictEqual(getResultsArray(store.filter({name: 'two'}))[0].name, 'two');
},
'filter with regexp': function () {
assert.strictEqual(getResultsArray(store.filter({name: /^t/})).length, 2);
assert.strictEqual(getResultsArray(store.filter({name: /^t/}))[1].name, 'three');
assert.strictEqual(getResultsArray(store.filter({name: /^o/})).length, 1);
assert.strictEqual(getResultsArray(store.filter({name: /o/})).length, 3);
},
'filter with test function': function () {
assert.strictEqual(getResultsArray(store.filter({id: {test: function (id) {
return id < 4;
}}})).length, 3);
assert.strictEqual(getResultsArray(store.filter({even: {test: function (even, object) {
return even && object.id > 2;
}}})).length, 1);
},
'filter with sort': function () {
assert.strictEqual(getResultsArray(store.filter({prime: true}).sort('name')).length, 3);
assert.strictEqual(getResultsArray(store.filter({even: true}).sort('name'))[1].name, 'two');
assert.strictEqual(getResultsArray(store.filter({even: true}).sort(function (a, b) {
return a.name < b.name ? -1 : 1;
}))[1].name, 'two');
assert.strictEqual(getResultsArray(store.filter(null).sort('mappedTo'))[4].name, 'four');
},
'filter with paging': function () {
assert.strictEqual(store.filter({prime: true}).fetchRange({start: 1, end: 2}).length, 1);
assert.strictEqual(store.filter({even: true}).fetchRange({start: 1, end: 2})[0].name, 'four');
},
'put update': function () {
var four = store.get(4);
four.square = true;
store.put(four);
four = store.get(4);
assert.isTrue(four.square);
},
'put new': function () {
store.put({
id: 6,
perfect: true
});
assert.isTrue(store.get(6).perfect);
},
'add duplicate': function () {
var threw;
try {
store.add({
id: 5,
perfect: true
});
} catch (e) {
threw = true;
}
assert.isTrue(threw);
},
'add new': function () {
store.add({
id: 7,
prime: true
});
assert.isTrue(store.get(7).prime);
},
'remove': function () {
assert.isTrue(store.remove(3));
assert.strictEqual(store.get(3), undefined);
},
'remove missing': function () {
assert(!store.remove(77));
// make sure nothing changed
assert.strictEqual(store.get(1).id, 1);
},
'filter after changes': function () {
store.add({ id: 7, prime: true });
assert.strictEqual(getResultsArray(store.filter({prime: true})).length, 4);
assert.strictEqual(getResultsArray(store.filter({perfect: true})).length, 0);
store.remove(3);
store.put({ id: 6, perfect: true });
assert.strictEqual(getResultsArray(store.filter({prime: true})).length, 3);
assert.strictEqual(getResultsArray(store.filter({perfect: true})).length, 1);
},
'ItemFileReadStore style data': function () {
var anotherLegacy = new Memory({
data: {
items: [
{name: 'one', prime: false},
{name: 'two', even: true, prime: true},
{name: 'three', prime: true}
],
identifier: 'name'
}
});
var anotherStore = new StoreAdapter({ objectStore: anotherLegacy });
assert.strictEqual(anotherStore.get('one').name, 'one');
assert.strictEqual(anotherStore.getIdentity(anotherStore.get('one')), 'one');
assert.strictEqual(getResultsArray(anotherStore.filter({name: 'one'}))[0].name, 'one');
},
'add new id assignment': function () {
var object = {
random: true
};
store.add(object);
assert.isTrue(!!object.id);
},
nestedSuite: sorting('legacy dstore adapter sorting - dojo/store/Memory', function before(data) {
return function before() {
var legacyStore = new Memory({data: data});
store = new StoreAdapter({ objectStore: legacyStore });
};
}, function sort() {
return store.sort.apply(store, arguments).fetch();
})
});
});
@@ -0,0 +1,79 @@
define([
'dojo/_base/lang',
'dojo/Deferred',
'dojo/io-query',
'intern/chai!assert'
], function (lang, Deferred, ioQuery, assert) {
// A mock request handler for testing.
var latestUrl,
latestQuery,
latestRequestHeaders,
responseHeaders,
latestOptions,
responseText;
function mockRequest(url, options) {
latestUrl = url;
latestQuery = ioQuery.queryToObject(url.match(/[^?]*(?:\?([^#]*))?/)[1] || '');
latestOptions = options;
latestRequestHeaders = {};
var headers = options.headers;
for (var name in headers) {
latestRequestHeaders[name.toLowerCase()] = headers[name];
}
var dfd = new Deferred();
dfd.resolve(responseText);
var responseDfd = new Deferred();
responseDfd.resolve({
getHeader: function (name) {
return responseHeaders[name.toLowerCase()];
},
data: responseText
});
return lang.delegate(dfd.promise, {
response: responseDfd
});
}
mockRequest.setResponseText = function (text) {
responseText = text;
};
mockRequest.setResponseHeaders = function (headers) {
responseHeaders = {};
for (var name in headers) {
responseHeaders[name.toLowerCase()] = headers[name];
}
};
mockRequest.assertHttpMethod = function (expectedMethod) {
assert.strictEqual(latestOptions.method || 'GET', expectedMethod);
};
mockRequest.assertRequestHeaders = function (expectedHeaders) {
for (var name in expectedHeaders) {
var lowerCaseName = name.toLowerCase(),
value = expectedHeaders[name];
if (value === null) {
assert.isTrue(
!(lowerCaseName in latestRequestHeaders)
|| latestRequestHeaders[lowerCaseName] === null
);
} else {
assert.isTrue(lowerCaseName in latestRequestHeaders);
assert.strictEqual(latestRequestHeaders[lowerCaseName], expectedHeaders[name]);
}
}
};
mockRequest.assertQueryParams = function (expectedParams) {
for (var name in expectedParams) {
assert.isTrue(name in latestQuery);
assert.equal(expectedParams[name].toString(), latestQuery[name]);
}
};
return mockRequest;
});
@@ -0,0 +1,9 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>dstore Intern Unit Test Runner</title>
<meta http-equiv="refresh" content="0;url=../node_modules/intern-geezer/client.html?config=tests/intern">
<BODY>
Redirecting to Intern runner.
</BODY>
</HTML>
@@ -0,0 +1,98 @@
define(['intern/chai!assert'], function (assert) {
return function createSortTests(name, before, sort) {
return {
name: name,
beforeEach: before([
{id: 1, field1: 'one', field2: '1'},
{id: 2, field1: 'one', field2: '2'},
{id: 3, field1: 'two', field2: '5'},
{id: 4, field1: 'two', field2: '4'},
{id: 5, field1: 'two', field2: '3'},
{id: 6, field1: 'one', field2: '3'}
]),
'multiple sort fields - ascend + ascend': function () {
var results = sort({property: 'field1'},
{property: 'field2'});
/**
* {id: 1, field1: 'one', field2: '1'},
* {id: 2, field1: 'one', field2: '2'},
* {id: 6, field1: 'one', field2: '3'}
* {id: 5, field1: 'two', field2: '3'},
* {id: 4, field1: 'two', field2: '4'},
* {id: 3, field1: 'two', field2: '5'},
*/
assert.strictEqual(results.length, 6, 'Length is 6');
assert.strictEqual(results[0].id, 1);
assert.strictEqual(results[1].id, 2);
assert.strictEqual(results[2].id, 6);
assert.strictEqual(results[3].id, 5);
assert.strictEqual(results[4].id, 4);
assert.strictEqual(results[5].id, 3);
},
'multiple sort fields - ascend + descend': function () {
var results = sort({property: 'field1', descending: false},
{property: 'field2', descending: true});
assert.strictEqual(results.length, 6, 'Length is 6');
/**
* {id: 6, field1: 'one', field2: '3'}
* {id: 2, field1: 'one', field2: '2'},
* {id: 1, field1: 'one', field2: '1'},
* {id: 3, field1: 'two', field2: '5'},
* {id: 4, field1: 'two', field2: '4'},
* {id: 5, field1: 'two', field2: '3'},
*/
assert.strictEqual(results[0].id, 6);
assert.strictEqual(results[1].id, 2);
assert.strictEqual(results[2].id, 1);
assert.strictEqual(results[3].id, 3);
assert.strictEqual(results[4].id, 4);
assert.strictEqual(results[5].id, 5);
},
'multiple sort fields - descend + ascend': function () {
var results = sort({property: 'field1', descending: true},
{property: 'field2', descending: false});
/**
* {id: 5, field1: 'two', field2: '3'},
* {id: 4, field1: 'two', field2: '4'},
* {id: 3, field1: 'two', field2: '5'},
* {id: 1, field1: 'one', field2: '1'},
* {id: 2, field1: 'one', field2: '2'},
* {id: 6, field1: 'one', field2: '3'}
*/
assert.strictEqual(results.length, 6, 'Length is 6');
assert.strictEqual(results[0].id, 5);
assert.strictEqual(results[1].id, 4);
assert.strictEqual(results[2].id, 3);
assert.strictEqual(results[3].id, 1);
assert.strictEqual(results[4].id, 2);
assert.strictEqual(results[5].id, 6);
},
'multiple sort fields - descend + descend': function () {
var results = sort({property: 'field1',descending: true},
{property: 'field2', descending: true});
/**
* {id: 3, field1: 'two', field2: '5'},
* {id: 4, field1: 'two', field2: '4'},
* {id: 5, field1: 'two', field2: '3'},
* {id: 6, field1: 'one', field2: '3'}
* {id: 2, field1: 'one', field2: '2'},
* {id: 1, field1: 'one', field2: '1'},
*/
assert.strictEqual(results.length, 6, 'Length is 6');
assert.strictEqual(results[0].id, 3);
assert.strictEqual(results[1].id, 4);
assert.strictEqual(results[2].id, 5);
assert.strictEqual(results[3].id, 6);
assert.strictEqual(results[4].id, 2);
assert.strictEqual(results[5].id, 1);
}
};
};
});
@@ -0,0 +1,351 @@
/// <reference path="../dojo/dojo.d.ts" />
declare module dstore {
export interface FetchArray<T> extends Array<T> {
totalLength: number;
}
export interface FetchPromise<T> extends IPromise<T[]> {
forEach(callback: (value: T, index?: number, array?: T[]) => void, thisObject?: any): FetchPromise<T>;
response?: FetchResponse<T>;
totalLength: IPromise<number>;
}
export interface FetchResponse<T> {
data: T;
options: { [key: string]: any; };
status?: number;
text: string;
url: string;
getHeader(name: string): string;
}
export interface ChangeEvent<T> {
id: any;
index?: number;
previousIndex?: number;
target: T;
totalLength: number;
type: string;
}
export interface ICollection<T> {
idProperty: string;
Model: { new (...args: any[]): T; };
tracking?: { remove(): void; };
add(object: T, options?: {}): IPromise<T>;
emit(eventName: string, event: ChangeEvent<T>): boolean;
fetch(): dstore.FetchPromise<T>;
fetchRange(kwArgs: { start?: number; end?: number; }): dstore.FetchPromise<T>;
filter(query: string | {} | { (item: T, index: number): boolean; }): ICollection<T>;
forEach(callback: (item: T, index: number) => void, thisObject?: any): IPromise<T[]>;
get(id: any): IPromise<T>;
getIdentity(object: T): any;
on(eventName: string, listener: (event: ChangeEvent<T>) => void): IHandle;
put(object: T, options?: {}): IPromise<T>;
remove(id: any): IPromise<Object>;
sort(property: string | { (a: T, b: T): number; }, descending?: boolean): ICollection<T>;
track?(): ICollection<T>;
}
export interface ISyncCollection<T> extends ICollection<T> {
addSync(object: T, options?: {}): T;
fetchSync(): dstore.FetchArray<T>;
fetchRangeSync(kwArgs: { start?: number; end?: number; }): dstore.FetchArray<T>;
filter(query: string | {} | { (item: T, index: number): boolean; }): ISyncCollection<T>;
getSync(id: any): T;
putSync(object: T, options?: {}): T;
removeSync(id: any): boolean;
sort(property: string | { (a: T, b: T): number; }, descending?: boolean): ISyncCollection<T>;
track?(): ISyncCollection<T>;
}
}
declare module 'dstore/Csv' {
interface IStringifyOptions {
alwaysQuote: boolean;
trailingNewline: boolean;
}
class Csv<T> {
fieldNames: string[];
delimiter: string;
newline: string;
trim: boolean;
parse(value: string): string[];
toCsv(options: IStringifyOptions): string;
stringify(data: any, options: IStringifyOptions): string;
}
export = Csv;
}
declare module 'dstore/Cache' {
import Store = require('dstore/Store');
class Cache<T> extends Store<T> {
cachingStore: dstore.ICollection<T>;
evict(id: any): void;
invalidate(): void;
isAvailableInCache(): void;
}
export = Cache;
}
declare module 'dstore/legacy/StoreAdapter' {
import Store = require('dstore/Store');
class StoreAdapter<T> extends Store<T> {}
export = StoreAdapter;
}
declare module 'dstore/legacy/DstoreAdapter' {
class DstoreAdapter<T> {
constructor(collection: dstore.ICollection<T>);
get(id: any): any;
put(object: T, options?: {}): any;
remove(id: any): any;
query(query: any, options?: {}): any;
}
export = DstoreAdapter;
}
declare module 'dstore/Memory' {
import Store = require('dstore/Store');
class Memory<T> extends Store<T> implements dstore.ISyncCollection<T> {
data: T[];
constructor(kwArgs?: Memory.KwArgs<T>);
addSync(object: T, options?: {}): T;
fetchSync(): dstore.FetchArray<T>;
fetchRangeSync(kwArgs: { start?: number; end?: number; }): dstore.FetchArray<T>;
filter(query: string | {} | { (item: T, index: number): boolean; }): Memory<T>;
getSync(id: any): T;
putSync(object: T, options?: {}): T;
removeSync(id: any): boolean;
setData(data: T[]): void;
sort(property: string | { (a: T, b: T): number; }, descending?: boolean): Memory<T>;
track(): Memory<T>;
}
module Memory {
export interface KwArgs<T> extends Store.KwArgs {
data?: T[];
}
}
export = Memory;
}
declare module 'dstore/charting/StoreSeries' {
class StoreSeries<T> {
constructor(collection: dstore.ICollection<T> | dstore.ISyncCollection<T>, value: any);
destroy(): void;
setSeriesObject(series: any): void;
fetch(): void;
}
export = StoreSeries;
}
declare module 'dstore/Tree' {
import Store = require('dstore/Store');
class Tree<T> extends Store<T> {
mayHaveChildren(parent: T): boolean;
getRootCollection(): Tree<T>;
getChildren(parent: T): Tree<T>;
}
export = Tree;
}
declare module 'dstore/SimpleQuery' {
import Store = require('dstore/Store');
class SimpleQuery<T> extends Store<T> {}
export = SimpleQuery;
}
declare module 'dstore/extensions/RqlQuery' {
import SimpleQuery = require('dstore/SimpleQuery');
class RqlQuery<T> extends SimpleQuery<T> {}
export = RqlQuery;
}
declare module 'dstore/LocalDB' {
import Store = require('dstore/Store');
class LocalDB<T> extends Store<T> {
constructor(kwArgs: LocalDB.KwArgs<T>);
transaction(): LocalDB.ITransaction;
}
module LocalDB {
export interface IConfigurationProperty {
autoIncrement?: boolean;
indexed?: boolean;
multiEntry?: boolean;
preference: number;
}
export interface IDBConfig {
stores: { [key: string]: IDBStoreConfig };
version: number;
}
export interface IDBStoreConfig {
[key: string]: number | IConfigurationProperty;
}
export interface ITransaction {
abort(): void;
commit(): void;
}
export interface KwArgs<T> extends Store.KwArgs {
dbConfig: IDBConfig;
storeName: string;
}
}
export = LocalDB;
}
declare module 'dstore/db/IndexedDB' {
import LocalDB = require('dstore/LocalDB');
class IndexedDB<T> extends LocalDB<T> {}
export = IndexedDB;
}
declare module 'dstore/db/SQL' {
import LocalDB = require('dstore/LocalDB');
class SQL<T> extends LocalDB<T> {}
export = SQL;
}
declare module 'dstore/db/LocalStorage' {
import LocalDB = require('dstore/LocalDB');
class LocalStorageDB<T> extends LocalDB<T> {}
export = LocalStorageDB;
}
declare module 'dstore/Trackable' {
class Trackable<T> {
currentRange: any[];
track(): dstore.ICollection<T>;
}
export = Trackable;
}
declare module 'dstore/Request' {
import Store = require('dstore/Store');
class Request<T> extends Store<T> {
headers: {};
parse: (serializedObject: string) => {};
target: string;
ascendingPrefix: string;
descendingPrefix: string;
accepts: string;
constructor(kwArgs?: Request.KwArgs);
filter(query: string | {} | { (item: T, index: number): boolean; }): Request<T>;
sort(property: string | { (a: T, b: T): number; }, descending?: boolean): Request<T>;
track(): Request<T>;
}
module Request {
export interface KwArgs extends Store.KwArgs {
headers?: typeof Request.prototype.headers;
parse?: typeof Request.prototype.parse;
target?: typeof Request.prototype.target;
ascendingPrefix?: typeof Request.prototype.ascendingPrefix;
descendingPrefix?: typeof Request.prototype.descendingPrefix;
accepts?: typeof Request.prototype.accepts;
}
}
export = Request;
}
declare module 'dstore/RequestMemory' {
import Request = require('dstore/Request');
import Cache = require('dstore/Cache');
class RequestMemory<T> extends Request<T> implements Cache<T> {
cachingStore: dstore.ICollection<T>;
evict(id: any): void;
filter(query: string | {} | { (item: T, index: number): boolean; }): RequestMemory<T>;
invalidate(): void;
isAvailableInCache(): void;
sort(property: string | { (a: T, b: T): number; }, descending?: boolean): RequestMemory<T>;
track(): RequestMemory<T>;
}
export = RequestMemory;
}
declare module 'dstore/Rest' {
import Request = require('dstore/Request');
class Rest<T> extends Request<T> {
filter(query: string | {} | { (item: T, index: number): boolean; }): Rest<T>;
sort(property: string | { (a: T, b: T): number; }, descending?: boolean): Rest<T>;
track(): Rest<T>;
}
export = Rest;
}
declare module 'dstore/Store' {
import decl = require('dojo/_base/declare');
class Store<T> extends decl.Base implements dstore.ICollection<T> {
idProperty: string;
Model: { new (...args: any[]): T; };
total: IPromise<number>;
constructor(kwArgs?: Store.KwArgs);
add(object: T, options?: {}): IPromise<T>;
emit(eventName: string, event: dstore.ChangeEvent<T>): boolean;
fetch(): dstore.FetchPromise<T>;
fetchRange(kwArgs: { start?: number; end?: number; }): dstore.FetchPromise<T>;
filter(query: string | {} | { (item: T, index: number): boolean; }): Store<T>;
forEach(callback: (item: T, index: number) => void, thisObject?: any): IPromise<T[]>;
get(id: any): IPromise<T>;
getIdentity(object: T): any;
on(eventName: string, listener: (event: dstore.ChangeEvent<T>) => void): IHandle;
put(object: T, options?: {}): IPromise<T>;
remove(id: any): IPromise<{}>;
sort(property: string | { (a: T, b: T): number; }, descending?: boolean): Store<T>;
}
module Store {
export interface KwArgs {
idProperty?: typeof Store.prototype.idProperty;
Model?: typeof Store.prototype.Model;
}
}
export = Store;
}
@@ -10,8 +10,16 @@
#include <Poco/Net/HTMLForm.h>
#include <Poco/URI.h>
#include <Variant/Variant.h>
#include <ros/ros.h>
#include <boost/format.hpp>
#include <boost/asio.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
#include <functional>
#include <memory>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
@@ -41,6 +49,211 @@ public:
};
template<class Observer, class HandlerFactory, class Handler>
class WebResourceEventQueue
: public virtual Observer {
public:
class TimeoutException
: public std::exception {};
const std::string clientID;
Handler* handler = nullptr;
HandlerFactory* handlerFactory;
Poco::Net::HTTPServerRequest* req = nullptr;
Poco::Net::HTTPServerResponse* resp = nullptr;
size_t popTimeout;
size_t deleteTimeout;
bool registeredToEvent = false;
bool handlerConnected = false;
bool breakPopLoop = false;
std::queue<libvariant::Variant> _eventQueue;
std::mutex _mutex;
std::condition_variable _cv;
boost::asio::deadline_timer timer;
WebResourceEventQueue( const std::string& clientID,
HandlerFactory* handlerFactory,
size_t popTimeout = 0,
size_t deleteTimeout = 10 )
: clientID(clientID), handler(handler), handlerFactory(handlerFactory),
timer(handlerFactory->getIOService()), popTimeout(popTimeout), deleteTimeout(deleteTimeout) {
std::cout << "WebResourceEventQueue \"" << clientID << "\" created" << std::endl;
}
virtual ~WebResourceEventQueue() {
std::cout << "WebResourceEventQueue \"" << clientID << "\" destroyed" << std::endl;
}
void pushEvent(const libvariant::Variant& event) {
{
std::lock_guard<std::mutex> lock(_mutex);
_eventQueue.push(event);
}
_cv.notify_one();
}
libvariant::Variant popEvent() {
std::unique_lock<std::mutex> lock(_mutex);
libvariant::Variant retval;
breakPopLoop = false;
while (_eventQueue.empty() && !breakPopLoop) {
if (popTimeout == 0) {
_cv.wait(lock);
} else {
if (_cv.wait_for(lock, std::chrono::seconds(popTimeout)) == std::cv_status::timeout) {
throw TimeoutException();
}
}
}
if (!_eventQueue.empty() && !breakPopLoop) {
retval = _eventQueue.front();
_eventQueue.pop();
}
return retval;
}
void cancelPopEvent() {
breakPopLoop = true;
_cv.notify_all();
}
template<class... Args>
bool setHandler(Handler* handler, Poco::Net::HTTPServerRequest* req, Poco::Net::HTTPServerResponse* resp, Args... args) {
if (!handlerConnected) {
timer.cancel();
this->handler = handler;
this->req = req;
this->resp =resp;
bool retval = __setHandler(args...);
handlerConnected = true;
return true;
}
return false;
}
bool __setHandler() {
return true;
}
void unsetHandler() {
if (handlerConnected) {
this->handler = nullptr;
this->req = nullptr;
this->resp =nullptr;
handlerConnected = false;
std::cout << "WebResourceEventQueue \"" << clientID << "\" has no open connections." << std::endl;
if (deleteTimeout > 0) {
timer.expires_from_now(boost::posix_time::seconds(deleteTimeout));
timer.async_wait([this](const boost::system::error_code& error) {
if (error != boost::asio::error::operation_aborted) {
std::cout << "WebResourceEventQueue \"" << clientID << "\" is about to be deleted." << std::endl;
handlerFactory->deleteDeviceEventQueue(clientID);
}
});
}
}
}
};
template<class ImplClass, bool SkipAuthentication, class ...Args>
class WebResourceHandlerFactoryImpl;
template<class ImplClass, bool SkipAuthentication>
class WebResourceHandlerFactoryImpl<ImplClass, SkipAuthentication>
: public virtual webserver::WebResourceHandlerFactory,
public implementation::Service<ImplClass> {
private:
webserver::WebServerService* service = nullptr;
public:
virtual void setWebServerService(webserver::WebServerService* service) noexcept override {
this->service = service;
}
virtual bool excludeFromAuthentication() noexcept override {
return SkipAuthentication;
}
};
template<class ImplClass, bool SkipAuthentication, class EventQueue>
class WebResourceHandlerFactoryImpl<ImplClass, SkipAuthentication, EventQueue>
: public virtual webserver::WebResourceHandlerFactory,
public implementation::Service<ImplClass> {
private:
webserver::WebServerService* service = nullptr;
boost::asio::io_service io_service;
std::unique_ptr<std::thread> io_service_thread;
std::map<std::string, std::shared_ptr<EventQueue>> eventQueues;
public:
WebResourceHandlerFactoryImpl() {
io_service_thread.reset(new std::thread([this] {
try {
std::cout << "Boost IO service starting" << std::endl;
boost::asio::io_service::work work(io_service);
this->io_service.run();
std::cout << "Boost IO service stopped" << std::endl;
} catch (std::exception& e) {
ROS_ERROR_STREAM("IO-Service thread caught exception: " << e.what());
} catch(...) {
ROS_ERROR("IO-Service thread caught unknown exception");
}
}));
}
~WebResourceHandlerFactoryImpl() {
if (io_service_thread) {
this->io_service.stop();
io_service_thread->join();
}
}
virtual void setWebServerService(webserver::WebServerService* service) noexcept override {
this->service = service;
}
virtual bool excludeFromAuthentication() noexcept override {
return SkipAuthentication;
}
boost::asio::io_service& getIOService() {
return io_service;
}
template<class... Args>
std::shared_ptr<EventQueue> getEventQueue( const std::string& clientID, Args... args) {
if (clientID.empty()) {
return std::make_shared<EventQueue>(clientID, static_cast<ImplClass*>(this), args...);
} else {
auto i = eventQueues.find(clientID);
if (i == eventQueues.end()) {
auto q = std::make_shared<EventQueue>(clientID, static_cast<ImplClass*>(this), args...);
eventQueues.insert({clientID, q});
return q;
} else {
return i->second;
}
}
}
virtual void deleteDeviceEventQueue( const std::string& clientID ) {
eventQueues.erase(clientID);
}
};
/*
HOS_SERVICE_IMPL_INFO( GenericWebResourceHandlerFactory, // Implementation
@@ -50,12 +263,10 @@ HOS_SERVICE_IMPL_INFO( GenericWebResourceHandlerFactory, // Implementation
class GenericWebResourceHandlerFactory
: public virtual WebResourceHandlerFactory,
public implementation::Service<GenericWebResourceHandlerFactory> {
: public WebResourceHandlerFactoryImpl<GenericWebResourceHandlerFactory, false> {
public:
typedef std::function<Poco::Net::HTTPRequestHandler*(Poco::URI matchUri, const Poco::Net::HTTPServerRequest&, bool)> factory_t;
private:
webserver::WebServerService* service = nullptr;
factory_t _factory;
public:
GenericWebResourceHandlerFactory(factory_t factory)
@@ -63,19 +274,11 @@ public:
virtual Poco::Net::HTTPRequestHandler* createRequestHandler( Poco::URI matchUri, const Poco::Net::HTTPServerRequest& request, bool ssl ) noexcept override {
return _factory(matchUri, request, ssl);
}
virtual void setWebServerService(webserver::WebServerService* service) noexcept override {
this->service = service;
}
virtual bool excludeFromAuthentication() noexcept override {
return false;
}
};
enum class ResponseDataFormat : int {
JSON,
JSONP,
@@ -7,6 +7,8 @@
#include <Poco/Net/KeyConsoleHandler.h>
#include <Poco/Net/ConsoleCertificateHandler.h>
#include <Poco/Net/SSLException.h>
#include <Poco/Net/ServerSocket.h>
#include <Poco/Net/SecureServerSocket.h>
#include <Poco/Net/HTTPBasicCredentials.h>
#include "webresource/WebSocketTest.h"